{"id":47611271,"url":"https://github.com/mutating/cstvis","last_synced_at":"2026-04-01T20:23:17.289Z","repository":{"id":345827786,"uuid":"1104764019","full_name":"mutating/cstvis","owner":"mutating","description":"Incremental change of CST","archived":false,"fork":false,"pushed_at":"2026-03-20T22:34:57.000Z","size":2354,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-21T12:56:58.462Z","etag":null,"topics":["cst","visitor"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mutating.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-26T16:50:34.000Z","updated_at":"2026-03-20T22:38:03.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mutating/cstvis","commit_stats":null,"previous_names":["mutating/cstvis"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/mutating/cstvis","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mutating%2Fcstvis","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mutating%2Fcstvis/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mutating%2Fcstvis/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mutating%2Fcstvis/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mutating","download_url":"https://codeload.github.com/mutating/cstvis/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mutating%2Fcstvis/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31291537,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cst","visitor"],"created_at":"2026-04-01T20:23:11.929Z","updated_at":"2026-04-01T20:23:17.273Z","avatar_url":"https://github.com/mutating.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdetails\u003e\n  \u003csummary\u003eⓘ\u003c/summary\u003e\n\n[![Downloads](https://static.pepy.tech/badge/cstvis/month)](https://pepy.tech/project/cstvis)\n[![Downloads](https://static.pepy.tech/badge/cstvis)](https://pepy.tech/project/cstvis)\n[![Coverage Status](https://coveralls.io/repos/github/mutating/cstvis/badge.svg?branch=main)](https://coveralls.io/github/mutating/cstvis?branch=main)\n[![Lines of code](https://sloc.xyz/github/mutating/cstvis/?category=code)](https://github.com/boyter/scc/)\n[![Hits-of-Code](https://hitsofcode.com/github/mutating/cstvis?branch=main\u0026label=Hits-of-Code\u0026exclude=docs/)](https://hitsofcode.com/github/mutating/cstvis/view?branch=main)\n[![Test-Package](https://github.com/mutating/cstvis/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/mutating/cstvis/actions/workflows/tests_and_coverage.yml)\n[![Python versions](https://img.shields.io/pypi/pyversions/cstvis.svg)](https://pypi.python.org/pypi/cstvis)\n[![PyPI version](https://badge.fury.io/py/cstvis.svg)](https://badge.fury.io/py/cstvis)\n[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)\n[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\n[![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/mutating/cstvis)\n\n\u003c/details\u003e\n\n![logo](https://raw.githubusercontent.com/mutating/cstvis/develop/docs/assets/logo_1.svg)\n\nMany source code tools, such as linters and formatters, work with [CST](https://en.wikipedia.org/wiki/Parse_tree), a tree-structured representation of source code (like [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree), 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.\n\nIt is built on top of [`libcst`](https://pypi.org/project/libcst/).\n\n\n## Table of Contents\n\n- [**Installation**](#installation)\n- [**Changing nodes**](#changing-nodes)\n- [**Filters**](#filters)\n- [**Separating registration from execution**](#separating-registration-from-execution)\n- [**Context**](#context)\n\n\n## Installation\n\nYou can install [`cstvis`](https://pypi.org/project/cstvis) with `pip`:\n\n```bash\npip install cstvis\n```\n\nYou can also use [`instld`](https://github.com/pomponchik/instld) to quickly try this package and others without installing them.\n\n\n## Changing nodes\n\nThe basic workflow is very simple:\n\n- Create a `Changer` instance.\n- Register converter functions with the `@\u003cchanger object\u003e.converter` decorator. Each function takes a `CST` node as its first argument and returns a replacement node.\n- If needed, register [filters](#filters) to prevent changes to certain nodes.\n- Iterate over individual changes and apply them as needed.\n\nLet me show you a simple example:\n\n```python\nfrom libcst import Subtract, Add\nfrom cstvis import Changer\nfrom pathlib import Path\n\n# Content of the file:\n# a = 4 + 5\n# b = 15 - a\n# c = b + a # kek\nchanger = Changer(Path('tests/some_code/simple_sum.py').read_text())\n\n@changer.converter\ndef change_add(node: Add):\n    return Subtract(\n        whitespace_before=node.whitespace_before,\n        whitespace_after=node.whitespace_after,\n    )\n\nfor x in changer.iterate_coordinates():\n    print(x)\n    print(changer.apply_coordinate(x))\n\n#\u003e Coordinate(file=None, class_name='Add', start_line=1, start_column=6, end_line=1, end_column=7, converter_id='__main__:change_add:11')\n#\u003e a = 4 - 5\n#\u003e b = 15 - a\n#\u003e c = b + a # kek\n#\u003e\n#\u003e Coordinate(file=None, class_name='Add', start_line=3, start_column=6, end_line=3, end_column=7, converter_id='__main__:change_add:11')\n#\u003e a = 4 + 5\n#\u003e b = 15 - a\n#\u003e c = b - a # kek\n```\n\nAs 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`](https://docs.python.org/3/library/typing.html#the-any-type), or specify [`libcst.CSTNode`](https://libcst.readthedocs.io/en/latest/nodes.html#libcst.CSTNode), in which case the converter will be applied to all nodes. If you specify a more specific type, such as [`libcst.Add`](https://libcst.readthedocs.io/en/latest/nodes.html#libcst.Add), the converter will be applied only to those nodes. You can also specify multiple node types using the `|` [syntax](https://docs.python.org/3/library/stdtypes.html#types-union) or [`Union`](https://docs.python.org/3/library/typing.html#typing.Union). Finally, several shortcuts are supported: `str` -\u003e [`libcst.SimpleString`](https://libcst.readthedocs.io/en/latest/nodes.html#libcst.SimpleString), `int` -\u003e [`libcst.Integer`](https://libcst.readthedocs.io/en/latest/nodes.html#libcst.Integer), and `float` -\u003e [`libcst.Float`](https://libcst.readthedocs.io/en/latest/nodes.html#libcst.Float).\n\nThe 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: identifying the coordinates of the change and then applying 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.\n\n\n## Filters\n\nA filter is a special function registered with the `@\u003cchanger object\u003e.filter` decorator. It returns `True` if the node should be changed and `False` otherwise. As with converters, the filter's type hint determines which nodes it is applied to.\n\nHere is another example (part of the code is omitted):\n\n```python\ncount_adds = 0\n\n@changer.filter\ndef only_first(node: Add) -\u003e bool:\n    global count_adds\n    \n    count_adds += 1\n    \n    return True if count_adds \u003c= 1 else False\n\nfor x in changer.iterate_coordinates():\n    print(x)\n    print(changer.apply_coordinate(x))\n\n#\u003e Coordinate(file=None, class_name='Add', start_line=1, start_column=6, end_line=1, end_column=7, converter_id='__main__:change_add:11')\n#\u003e a = 4 - 5\n#\u003e b = 15 - a\n#\u003e c = b + a # kek\n```\n\nAs you can see, the iteration now yields only the first possible change; the rest are filtered out automatically because the filter returns `False` for them.\n\n\n## Separating registration from execution\n\nIn some cases, you may want to separate converter and filter registration from execution. For this purpose, a special object type — `Collector` — can help you. A collector object has the same decorators as `Changer` objects, and it can be used in the same way. When creating a `Changer`, you can pass a `Collector` instance:\n\n```python\nfrom cstvis import Collector\n\ncollector = Collector()\n\n@collector.converter\ndef change_add(node: Add):\n    return Subtract(\n        whitespace_before=node.whitespace_before,\n        whitespace_after=node.whitespace_after,\n    )\n\nchanger = Changer(Path('tests/some_code/simple_sum.py').read_text(), collector=collector)\n```\n\nIf you need to combine collectors defined in different parts of your program, you can do so using the `+` operator:\n\n```python\ncollector_1 = Collector()\ncollector_2 = Collector()\n\n...\n\ncollector_3 = collector_1 + collector_2\n```\n\n\u003e ↑ The resulting collector will contain all the filters and converters from its components.\n\n## Context\n\nBy 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 your function signatures, detects that a function expects a second argument, and passes the context to it:\n\n```python\nfrom cstvis import Context\n\n@changer.converter\ndef change_add(node: Add, context: Context):  # \u003c- The function takes a second argument.\n    return Subtract(\n        whitespace_before=node.whitespace_before,\n        whitespace_after=node.whitespace_after,\n    )\n```\n\nThe context object has two main fields and one useful method:\n\n- `coordinate` with fields `start_line: int`, `start_column: int`, `end_line: int`, `end_column: int` and some others — identifies the current location in the code.\n- `comment` — the comment on the node’s first line, if there is one, without the leading `#`, or `None` if there is no comment.\n- `get_metacodes(key: Union[str, List[str]]) -\u003e List[ParsedComment]` — a method that returns a list of parsed comments in [metacode format](https://github.com/mutating/metacode) associated with the current line of code.\n\nYou 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:\n\n```python\nfrom libcst import SimpleString\nfrom cstvis import Changer, Context\nfrom pathlib import Path\n\n# Content of the file:\n# a = \"old string\"\n\nchanger = Changer(Path('tests/some_code/simple_string.py').read_text())\n\n@changer.converter(meta={'new_value': '\"new string\"'})\ndef change_string(node: SimpleString, context: Context):\n    return SimpleString(value=context.meta['new_value'])\n\nfor x in changer.iterate_coordinates():\n    print(x)\n    print(changer.apply_coordinate(x))\n\n#\u003e Coordinate(file=None, class_name='SimpleString', start_line=1, start_column=4, end_line=1, end_column=9, converter_id='__main__:change_string:13')\n#\u003e a = \"new string\"\n```\n\nYou can also pass a meta dictionary to the [`Collector`](#separating-registration-from-execution) class constructor. If you do this but also pass another dictionary to the `@\u003ccollector object\u003e.converter` or `@\u003ccollector object\u003e.filter` decorators, the dictionaries will be merged before being passed to the wrapped functions (if keys match, the values passed to the decorator will take precedence):\n\n```python\ncollector = Collector(meta={'key 1': 'value 1'})\n\n@collector.converter(meta={'key 2': 'value 2'})\ndef change_add(node: Add, context: Context):\n    print(context.meta)\n    #\u003e {'key 1': 'value 1', 'key 2': 'value 2'}\n    ...\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmutating%2Fcstvis","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmutating%2Fcstvis","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmutating%2Fcstvis/lists"}