Skip to main content

Incremental change of CST

Project description

Downloads Downloads Coverage Status Lines of code Hits-of-Code Test-Package Python versions PyPI version Checked with mypy Ruff DeepWiki

logo

Many source code tools, such as linters and formatters, work with CST, a tree-structured representation of source code (like AST, but it also retains nodes such as whitespace and comments). This library is a wrapper around these trees, designed for convenient iterative traversal and node replacement.

It is built on top of libcst.

Table of Contents

Installation

You can install cstvis with pip:

pip install cstvis

You can also use instld to quickly try this package and others without installing them.

Changing nodes

The basic workflow is very simple:

  • Create a Changer instance.
  • Register converter functions with the @<changer object>.converter decorator. Each function takes a CST node as its first argument and returns a replacement node.
  • If needed, register filters to prevent changes to certain nodes.
  • Iterate over individual changes and apply them as needed.

Let me show you a simple example:

from libcst import Subtract, Add
from cstvis import Changer
from pathlib import Path

# Content of the file:
# a = 4 + 5
# b = 15 - a
# c = b + a # kek
changer = Changer(Path('tests/some_code/simple_sum.py').read_text())

@changer.converter
def change_add(node: Add):
    return Subtract(
        whitespace_before=node.whitespace_before,
        whitespace_after=node.whitespace_after,
    )

for x in changer.iterate_coordinates():
    print(x)
    print(changer.apply_coordinate(x))

#> Coordinate(file=None, class_name='Add', start_line=1, start_column=6, end_line=1, end_column=7, converter_id='__main__:change_add:11')
#> a = 4 - 5
#> b = 15 - a
#> c = b + a # kek
#>
#> Coordinate(file=None, class_name='Add', start_line=3, start_column=6, end_line=3, end_column=7, converter_id='__main__:change_add:11')
#> a = 4 + 5
#> b = 15 - a
#> c = b - a # kek

As you can see in the example, the converter function takes an argument with a type hint. You don’t need to write type-checking if statements because the system determines which node types to convert based on this hint. You can omit the annotation entirely, specify Any, or specify libcst.CSTNode, in which case the converter will be applied to all nodes. If you specify a more specific type, such as libcst.Add, the converter will be applied only to those nodes. You can also specify multiple nodes using the | syntax or Union. Finally, several shortcuts are supported: str -> libcst.SimpleString, int -> libcst.Integer, and float -> libcst.Float.

The key part of this example is the last two lines, where we iterate over the coordinates. What does that mean? This library performs each code change in two stages: identify the coordinates of the change and then apply it. This separation makes it possible to distribute the work across multiple threads or even multiple machines. However, this design also has limitations. If you apply one coordinate change, the resulting code will differ from the original and the remaining coordinates will no longer be valid. You can only apply one change at a time.

Filters

A filter is a special function registered with the @<changer object>.filter decorator. It decides whether a specific CST node should be changed, and returns True if yes, or False if no. As with converters, the filter's type hint determines which nodes it is applied to.

Here is another example (part of the code is omitted):

count_adds = 0

@changer.filter
def only_first(node: Add) -> bool:
    global count_adds
    
    count_adds += 1
    
    return True if count_adds <= 1 else False

for x in changer.iterate_coordinates():
    print(x)
    print(changer.apply_coordinate(x))

#> Coordinate(file=None, class_name='Add', start_line=1, start_column=6, end_line=1, end_column=7, converter_id='__main__:change_add:11')
#> a = 4 - 5
#> b = 15 - a
#> c = b + a # kek

As you can see, now the iteration yields only the first possible change, the rest are filtered out automatically because the filter returns False for them.

Separating registration from execution

In some cases, you may want to separate converter and filter registration from execution. In this case, a special type of objects — Collector — can help you. A collector object has the same decorators as Changer objects, and they can be used in exactly the same way. When creating a Changer object, you can pass a collector object to it:

from cstvis import Collector

collector = Collector()

@collector.converter
def change_add(node: Add):
    return Subtract(
        whitespace_before=node.whitespace_before,
        whitespace_after=node.whitespace_after,
    )

changer = Changer(Path('tests/some_code/simple_sum.py').read_text(), collector=collector)

Context

By default, each converter or filter takes a single argument: the node to which it is applied. However, you can also specify a second argument: the context. The system analyzes the signatures of your functions, detects that they expect a second argument, and passes it to them:

from cstvis import Context

@changer.converter
def change_add(node: Add, context: Context):  # <- The function takes a second argument.
    return Subtract(
        whitespace_before=node.whitespace_before,
        whitespace_after=node.whitespace_after,
    )

The context object has two necessary fields and one useful method:

  • coordinate with fields start_line: int, start_column: int, end_line: int, end_column: int and some others. This identifies the current location in the code.
  • comment - the comment on the first line of the node, if there is one, without the leading #, or None if there is no comment.
  • get_metacodes(key: Union[str, List[str]]) -> List[ParsedComment] - a method that returns a list of parsed comments in metacode format associated with this line of code.

You can also pass an arbitrary dictionary to any decorator in this library; a copy of that dictionary will be passed as a meta attribute of the context object:

from libcst import SimpleString
from cstvis import Changer, Context
from pathlib import Path

# Content of the file:
# a = "old string"

changer = Changer(Path('tests/some_code/simple_string.py').read_text())

@changer.converter(meta={'new_value': '"new string"'})
def change_string(node: SimpleString, context: Context):
    return SimpleString(value=context.meta['new_value'])

for x in changer.iterate_coordinates():
    print(x)
    print(changer.apply_coordinate(x))

#> Coordinate(file=None, class_name='SimpleString', start_line=1, start_column=4, end_line=1, end_column=9, converter_id='__main__:change_add:13')
#> a = "new string"

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cstvis-0.0.5.tar.gz (16.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cstvis-0.0.5-py3-none-any.whl (12.9 kB view details)

Uploaded Python 3

File details

Details for the file cstvis-0.0.5.tar.gz.

File metadata

  • Download URL: cstvis-0.0.5.tar.gz
  • Upload date:
  • Size: 16.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for cstvis-0.0.5.tar.gz
Algorithm Hash digest
SHA256 989ab6b95259fd0f7e2c826cf91f6abc8467070917458a287dc48273f54cd407
MD5 88ccd852424832f34d7cb5595823c110
BLAKE2b-256 9668973972e2db5e2046cd491b10eecccbae3924d03fb9d9bb451a93d2f89fb1

See more details on using hashes here.

Provenance

The following attestation bundles were made for cstvis-0.0.5.tar.gz:

Publisher: release.yml on mutating/cstvis

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cstvis-0.0.5-py3-none-any.whl.

File metadata

  • Download URL: cstvis-0.0.5-py3-none-any.whl
  • Upload date:
  • Size: 12.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for cstvis-0.0.5-py3-none-any.whl
Algorithm Hash digest
SHA256 005c44e7ddfc5e0907709e8559b9478ef810ace63aba9c9c52ceee1e33d86b14
MD5 69d0b9a4caf4334b6adf3dca3cc57ab2
BLAKE2b-256 e19c77e4caa7f8b01a1c3bd0134d2e814c8201d9ad1318d707e0894c070add8b

See more details on using hashes here.

Provenance

The following attestation bundles were made for cstvis-0.0.5-py3-none-any.whl:

Publisher: release.yml on mutating/cstvis

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page