Metadata-Version: 2.1
Name: cstq
Version: 0.0.6
Project-URL: Documentation, https://github.com/aleivag/cstq#readme
Project-URL: Issues, https://github.com/aleivag/cstq/issues
Project-URL: Source, https://github.com/aleivag/cstq
Author-email: Alvaro Leiva <aleivag@gmail.com>
License: MIT License
        
        Copyright (c) 2023-present Alvaro Leiva <aleivag@gmail.com>
        
        Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
License-File: LICENSE.txt
Classifier: Development Status :: 4 - Beta
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Python: >=3.10
Requires-Dist: libcst
Description-Content-Type: text/markdown

# cstq

[![PyPI - Version](https://img.shields.io/pypi/v/cstq.svg)](https://pypi.org/project/cstq)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/cstq.svg)](https://pypi.org/project/cstq)

-----

A very simple and, at least according to the author, intuitive library to navigate Python source code, and code modeling.

## Enough said, I need some action!

In code as in screenwriting, it's better to show rather than tell. So, here are a couple of examples that scratch the surface of 
this library. 

### Installation

First things first, let's install the library

```console
pip install cstq
```

### Example base code 

To start working with cstq, you can pass the path to a Python file or pass the module directly by:


```python
from cstq import Query

q = Query("""
import sys

def main() -> None:
    import os
    print('hello world' if os.environ.get("USER") else "who are you?")

if __name__ == "__main__":
    main()
""")
```

Now lets get down to business

## Basic Navigation

There are 3 types of basic navigation:

### 1. Direct Attribute Access

You can navigate by referencing attributes directly.


```python
>>> q
<CollectionOfNodes nodes=['$(Module)']>

>>> q.body
<CollectionOfNodes nodes=['$(Module).body']>

>>> q.body[:]
<CollectionOfNodes nodes=['$(Module).body[0](SimpleStatementLine)', '$(Module).body[1](FunctionDef)', '$(Module).body[2](If)']>

>>> q.body[0].body[:]
<CollectionOfNodes nodes=['$(Module).body[0](SimpleStatementLine).body[0](Import)']>

```
and then get the node (and the code) back as

```python
>>> q.body[0].body[0].node()
Import(
    names=[
        ImportAlias(
            name=Name(
                value='sys',
                lpar=[],
                rpar=[],
            ),
            asname=None,
            comma=MaybeSentinel.DEFAULT,
        ),
    ],
    semicolon=MaybeSentinel.DEFAULT,
    whitespace_after_import=SimpleWhitespace(
        value=' ',
    ),
)


>>> q.body[0].body[0].code_for_node()
'import sys'

>>> q.body[0].body[:].names[0].name.node() 
Name(
    value='sys',
    lpar=[],
    rpar=[],
)
```
### 2. Filtering

Filtering allows you to "filter" the current selection of nodes to specific ones. Each one of these accepts either a 
[`libcst.matchers`](https://libcst.readthedocs.io/en/latest/matchers_tutorial.html) or a callback (more on callbacks later). 
`libcst.matchers` presents a very powerful query language, and when that's not enough, you can always fall back to a custom callback.


```python

>>> import libcst.matchers as m

# main root nodes
>>> q.body[:]
<CollectionOfNodes nodes=['$(Module).body[0](SimpleStatementLine)', '$(Module).body[1](FunctionDef)', '$(Module).body[2](If)']>

# filter out function definitions using explicit filter
>>> q.body[:].filter(m.FunctionDef())
<CollectionOfNodes nodes=['$(Module).body[1](FunctionDef)']>

# filter out function definitions using implicit filter
>>> q.body[m.FunctionDef()]
<CollectionOfNodes nodes=['$(Module).body[1](FunctionDef)']>

```

By using the `.filter` method, you can filter any selection. For instance, `q.body[:]` represents the elements of the body of 
the module. However, you can also filter out by using the `__getitem__` operation (`[.. add your filter here ...]`), 
making it a bit more compact.

### 3. Searching

If you want to search, not only in the current selection but also at every level, you can use `.search`. Similar 
to `.filter`, it accepts a `libcst.matchers` or a callback.

```python
>>> q.search(m.Import())
<CollectionOfNodes nodes=['$(Module).body[0](SimpleStatementLine).body[0](Import)', '$(Module).body[1](FunctionDef).body(IndentedBlock).body[0](SimpleStatementLine).body[0](Import)']>

# get the __name__ == "__main__" using search and filter
>>> q.search(m.If()).filter(lambda n: n.test.code() == '__name__ == "__main__"')
<CollectionOfNodes nodes=['$(Module).body[2](If)']>

# combining multiple search and filters into a single statement 
>>> q.search(m.If(), lambda n: n.test.code() == '__name__ == "__main__"')
<CollectionOfNodes nodes=['$(Module).body[2](If)']>
```

### callbacks

`.filter` and `.search` can take a callback method that takes an _extended version_ of a CSTNode and returns true or false.
The extended version of the CSTNode its the regular CSTNode with a couple of extra methods like `.parent()` to give your the parent,
and `.code()` to generate the code that node represents, soo far I haven't added anything else.

### codemod (changes)

If you want to change nodes, it's not hard to do, for instance removing the first import would be as easy as finding it
(using any combination of `.search` and/or `.filter`) and then call `.remove()`

```python
>>> q.body[0].body[0].remove()
<CollectionOfNodes nodes=[]>

# print the code on top
>>> print(q.code())


def main() -> None:
    import os
    print('hello world' if os.environ.get("USER") else "who are you?")

if __name__ == "__main__":
    main()
```

As you can see, there are several empty lines at the top. This is because of 2 things, first the module object is 
defined to have 1 empty line as a header, and the funtion def has the leading_lines attribute set to also have a empty line

```python
>>> q.header[:].node()
EmptyLine(
    indent=True,
    whitespace=SimpleWhitespace(
        value='',
    ),
    comment=None,
    newline=Newline(
        value=None,
    ),
)

>>> q.body[0]
<CollectionOfNodes nodes=['$(Module).body[0](FunctionDef)']>

>>>  q.body[0].leading_lines[:].node()
EmptyLine(
    indent=True,
    whitespace=SimpleWhitespace(
        value='',
    ),
    comment=None,
    newline=Newline(
        value=None,
    ),
)
```


Let's address this by simply changing the attribute `leading_lines` in that function definition using the method `.change`

```python

>>> q.body[0].change(leading_lines=[])
<CollectionOfNodes nodes=['$(Module).body[0](FunctionDef)']>

>>> print(q.code())

def main() -> None:
    import os
    print('hello world' if os.environ.get("USER") else "who are you?")

if __name__ == "__main__":
    main()

```

you can replace a node with another one


```python
# creates a node from text
>>> import_from = cst.parse_statement("from python_wrapper import os")

>>> q.search(m.Import()).replace(import_from)

>>> print(q.code())

def main() -> None:
    from python_wrapper import os
    print('hello world' if os.environ.get("USER") else "who are you?")

if __name__ == "__main__":
    main()

```


```python

# Let's add the import at the top level
>>> q.change(lambda n: n.with_changes(body=[cst.SimpleStatementLine(body=[import_from]), *n.body]))
<CollectionOfNodes nodes=['$(Module)']>

# Let's remove the inner import
>>> q.search(m.FunctionDef()).search(m.ImportFrom()).remove()
<CollectionOfNodes nodes=[]>

# Let's print the result
>>> print(q.code())

from python_wrapper import os
def main() -> None:
    print('hello world' if os.environ.get("USER") else "who are you?")

if __name__ == "__main__":
    main()


```

## License

`cstq` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
