ast-pattern-engine 1.0.1__tar.gz → 1.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/.github/workflows/ci.yml +86 -69
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/PKG-INFO +8 -1
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/README.md +7 -0
- ast_pattern_engine-1.0.2/docs/guide.md +57 -0
- ast_pattern_engine-1.0.2/docs/index.md +46 -0
- ast_pattern_engine-1.0.2/docs/reference/core.md +11 -0
- ast_pattern_engine-1.0.2/docs/reference/nodes.md +19 -0
- ast_pattern_engine-1.0.2/docs/reference/templates.md +7 -0
- ast_pattern_engine-1.0.2/docs/reference/visitors.md +9 -0
- ast_pattern_engine-1.0.2/docs/stylesheets/extra.css +26 -0
- ast_pattern_engine-1.0.2/mkdocs.yaml +50 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/pyproject.toml +6 -4
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/nodes/basic.py +52 -10
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/nodes/sequences.py +27 -21
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/visitors.py +20 -6
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/visitors/test_single_occurrence_finder.py +1 -1
- ast_pattern_engine-1.0.1/src/ast_pattern_engine/plumbing.py +0 -2
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/.gitignore +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/LICENSE +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/examples/dict_get_rewrite.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/__init__.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/core.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/engine.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/nodes/__init__.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/py.typed +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/templates.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/__init__.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_all_of.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_any_of.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_bind.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_collect.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_contains.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_filter.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_not.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_one_of.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_optional.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_pattern_group.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_repetition.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_templates.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/test_engine.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/visitors/test_bottom_up_pattern_transformer.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/visitors/test_pattern_finder.py +0 -0
- {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/visitors/test_pattern_transformer.py +0 -0
|
@@ -1,69 +1,86 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [ "main" ]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [ "main" ]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
format:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
if: github.event_name == 'push'
|
|
13
|
-
permissions:
|
|
14
|
-
contents: write
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v4
|
|
17
|
-
|
|
18
|
-
- name: Install uv
|
|
19
|
-
uses: astral-sh/setup-uv@v5
|
|
20
|
-
with:
|
|
21
|
-
enable-cache: true
|
|
22
|
-
|
|
23
|
-
- name: Auto-format with Ruff
|
|
24
|
-
run: uv run ruff format
|
|
25
|
-
|
|
26
|
-
- name:
|
|
27
|
-
run: uv run ruff check --
|
|
28
|
-
|
|
29
|
-
- name: Commit formatting changes
|
|
30
|
-
uses: stefanzweifel/git-auto-commit-action@v5
|
|
31
|
-
with:
|
|
32
|
-
commit_message: "style: auto-format with ruff"
|
|
33
|
-
|
|
34
|
-
test:
|
|
35
|
-
runs-on: ubuntu-latest
|
|
36
|
-
needs: format
|
|
37
|
-
if: always()
|
|
38
|
-
strategy:
|
|
39
|
-
matrix:
|
|
40
|
-
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
41
|
-
|
|
42
|
-
steps:
|
|
43
|
-
- uses: actions/checkout@v4
|
|
44
|
-
with:
|
|
45
|
-
ref: ${{ github.ref }}
|
|
46
|
-
|
|
47
|
-
- name: Set up Python ${{ matrix.python-version }}
|
|
48
|
-
uses: actions/setup-python@v5
|
|
49
|
-
with:
|
|
50
|
-
python-version: ${{ matrix.python-version }}
|
|
51
|
-
|
|
52
|
-
- name: Install uv
|
|
53
|
-
uses: astral-sh/setup-uv@v5
|
|
54
|
-
with:
|
|
55
|
-
enable-cache: true
|
|
56
|
-
|
|
57
|
-
- name: Install dependencies
|
|
58
|
-
run: uv sync --all-extras --all-groups
|
|
59
|
-
|
|
60
|
-
- name:
|
|
61
|
-
run: uv run
|
|
62
|
-
|
|
63
|
-
- name:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ "main", "dev" ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ "main", "dev" ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
format-and-lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
if: github.event_name == 'push'
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v5
|
|
20
|
+
with:
|
|
21
|
+
enable-cache: true
|
|
22
|
+
|
|
23
|
+
- name: Auto-format with Ruff
|
|
24
|
+
run: uv run ruff format
|
|
25
|
+
|
|
26
|
+
- name: Lint with Ruff
|
|
27
|
+
run: uv run ruff check --exit-zero
|
|
28
|
+
|
|
29
|
+
- name: Commit formatting changes
|
|
30
|
+
uses: stefanzweifel/git-auto-commit-action@v5
|
|
31
|
+
with:
|
|
32
|
+
commit_message: "style: auto-format with ruff"
|
|
33
|
+
|
|
34
|
+
test:
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
needs: format-and-lint
|
|
37
|
+
if: always()
|
|
38
|
+
strategy:
|
|
39
|
+
matrix:
|
|
40
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
41
|
+
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v4
|
|
44
|
+
with:
|
|
45
|
+
ref: ${{ github.ref }}
|
|
46
|
+
|
|
47
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
48
|
+
uses: actions/setup-python@v5
|
|
49
|
+
with:
|
|
50
|
+
python-version: ${{ matrix.python-version }}
|
|
51
|
+
|
|
52
|
+
- name: Install uv
|
|
53
|
+
uses: astral-sh/setup-uv@v5
|
|
54
|
+
with:
|
|
55
|
+
enable-cache: true
|
|
56
|
+
|
|
57
|
+
- name: Install dependencies
|
|
58
|
+
run: uv sync --all-extras --all-groups
|
|
59
|
+
|
|
60
|
+
- name: Run tests with pytest
|
|
61
|
+
run: uv run pytest --cov=src --cov-report=xml
|
|
62
|
+
|
|
63
|
+
- name: Upload coverage reports to Codecov
|
|
64
|
+
uses: codecov/codecov-action@v5
|
|
65
|
+
env:
|
|
66
|
+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
67
|
+
|
|
68
|
+
deploy-docs:
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
needs: test
|
|
71
|
+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
72
|
+
permissions:
|
|
73
|
+
contents: write
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/checkout@v4
|
|
76
|
+
|
|
77
|
+
- name: Install uv
|
|
78
|
+
uses: astral-sh/setup-uv@v5
|
|
79
|
+
with:
|
|
80
|
+
enable-cache: true
|
|
81
|
+
|
|
82
|
+
- name: Install Docs Dependencies
|
|
83
|
+
run: uv sync --only-group docs
|
|
84
|
+
|
|
85
|
+
- name: Deploy Docs
|
|
86
|
+
run: uv run mkdocs gh-deploy --force
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ast-pattern-engine
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: A library for regex-inspired fine-grained AST pattern matching and replacing
|
|
5
5
|
Project-URL: Homepage, https://github.com/80sVectorz/ast_pattern_engine
|
|
6
6
|
Project-URL: Repository, https://github.com/80sVectorz/ast_pattern_engine
|
|
@@ -21,6 +21,13 @@ Provides-Extra: dev
|
|
|
21
21
|
Requires-Dist: pytest; extra == 'dev'
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
24
|
+

|
|
25
|
+

|
|
26
|
+

|
|
27
|
+

|
|
28
|
+

|
|
29
|
+

|
|
30
|
+
|
|
24
31
|
# AST Pattern Engine
|
|
25
32
|
|
|
26
33
|
A powerful, programmatic, regex-inspired AST pattern matching and manipulation library for Python.
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
1
8
|
# AST Pattern Engine
|
|
2
9
|
|
|
3
10
|
A powerful, programmatic, regex-inspired AST pattern matching and manipulation library for Python.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Guide & Philosophy
|
|
2
|
+
|
|
3
|
+
## Philosophy & Design
|
|
4
|
+
|
|
5
|
+
Instead of having to rely on fragile direct source-code manipulation or slightly better magic string-expression-based AST manipulation engines, `ast_pattern_engine` provides an internal DSL for building explicit, structural patterns.
|
|
6
|
+
|
|
7
|
+
It explicitly avoids string-based expressions because they simply don't scale well. For robust code-analysis you must have a grasp of the underlying AST structure.
|
|
8
|
+
|
|
9
|
+
The package started as a component for another project and was later spun off into it's own clean package.
|
|
10
|
+
Early experiences made it clear that large expressions aren't the way to go for AST manipulation.
|
|
11
|
+
A staged approach is much more robust and easier to reason about.
|
|
12
|
+
|
|
13
|
+
The package is intentionally kept somewhat limited to encourage using custom logic for more advanced filtering and analysis. Rather than building extremely nested gigantic expressions, it's encouraged to build small, focused patterns and visitors.
|
|
14
|
+
Like casting a wide net and progressively filtering down in stages.
|
|
15
|
+
|
|
16
|
+
## Primitives
|
|
17
|
+
|
|
18
|
+
The library provides several primitives to build robust sequences:
|
|
19
|
+
|
|
20
|
+
- `NodePattern`: Match specific AST node types and assert on their fields.
|
|
21
|
+
- `Collect` / `Bind`: Extract sub-trees out of a matched pattern to use in your handlers. `Bind` assigns a name to a matched node so that it can be processed during transformation.
|
|
22
|
+
- `OneOf`: Match one of several possible patterns (similar to regex `|`).
|
|
23
|
+
- `Repetition`: Match a pattern sequentially 1 or more times (similar to regex `*` and `+`).
|
|
24
|
+
- `Optional`: Match a pattern 0 or 1 times (similar to regex `?`).
|
|
25
|
+
- `Filter`: Apply arbitrary Python lambdas to check node states during matching.
|
|
26
|
+
|
|
27
|
+
## Building Patterns
|
|
28
|
+
|
|
29
|
+
A typical pattern is a sequence of `Pattern` objects. For instance, finding a sequence of assignments:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from ast_pattern_engine import NodePattern, Bind
|
|
33
|
+
import ast
|
|
34
|
+
|
|
35
|
+
# Matches an assignment of any value to "x"
|
|
36
|
+
assign_to_x = NodePattern(
|
|
37
|
+
ast.Assign,
|
|
38
|
+
targets=[NodePattern(ast.Name, id="x")],
|
|
39
|
+
value=Bind("x_value")
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Transformers and Finders
|
|
44
|
+
|
|
45
|
+
Once a pattern is matched, you often want to act on it.
|
|
46
|
+
|
|
47
|
+
- **Transformers** (`PatternTransformer`, `BottomUpPatternTransformer`): These visitors replace matched subtrees with new nodes generated by your handlers. A handler receives the bound variables (E.G `Bind("x")`, `Collect()) and returns the replacement nodes.
|
|
48
|
+
- **Finders** (`PatternFinder`, `SingleOccurrenceFinder`): These visitors just locate where patterns occur in the tree without modifying it. Useful for analysis or linting.
|
|
49
|
+
|
|
50
|
+
## Templates
|
|
51
|
+
|
|
52
|
+
To reduce boilerplate when building patterns, the library includes a `templates` module with helpers for common operations:
|
|
53
|
+
- `match_call(func_name, **kwargs)`
|
|
54
|
+
- `match_assign(target_name, value)`
|
|
55
|
+
- `match_in_expr(pattern)`
|
|
56
|
+
|
|
57
|
+
Using templates allows you to write dense, readable matching rules quickly.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# AST Pattern Engine
|
|
2
|
+
|
|
3
|
+
A powerful, programmatic, regex-inspired AST pattern matching and manipulation library for Python.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
Here is a simple pipeline that rewrites `dict.get("key")` calls into direct subscript access `dict["key"]`:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from typing import Any
|
|
11
|
+
import ast
|
|
12
|
+
from ast_pattern_engine import BottomUpPatternTransformer, Bind, NodePattern
|
|
13
|
+
|
|
14
|
+
source = "value = my_dict.get(other_dict.get('foo'))"
|
|
15
|
+
tree = ast.parse(source)
|
|
16
|
+
|
|
17
|
+
# 1. Build the explicit structural pattern
|
|
18
|
+
# Matches: <obj>.get(<key>)
|
|
19
|
+
pattern = [
|
|
20
|
+
NodePattern(
|
|
21
|
+
ast.Call,
|
|
22
|
+
func=NodePattern(ast.Attribute, attr="get", value=Bind("obj")),
|
|
23
|
+
args=Bind("key"),
|
|
24
|
+
)
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# 2. Define the rewrite logic
|
|
28
|
+
def rewrite_dict_get(bindings: dict[str, Any]) -> list[ast.AST]:
|
|
29
|
+
obj = bindings["obj"]
|
|
30
|
+
key = bindings["key"][0] # args is a list
|
|
31
|
+
|
|
32
|
+
# Return the new node to replace the matched node
|
|
33
|
+
new_node = ast.Subscript(value=obj, slice=key, ctx=ast.Load())
|
|
34
|
+
return [new_node]
|
|
35
|
+
|
|
36
|
+
# 3. Apply the transformer
|
|
37
|
+
# We use BottomUpPatternTransformer so nested `.get()` calls
|
|
38
|
+
# are safely transformed from the inside-out.
|
|
39
|
+
transformer = BottomUpPatternTransformer(pattern, {"key": rewrite_dict_get})
|
|
40
|
+
transformer.visit(tree)
|
|
41
|
+
|
|
42
|
+
print(ast.unparse(tree))
|
|
43
|
+
# Output: value = my_dict[other_dict['foo']]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For more in-depth examples, refer to the [Guide](guide.md) or the [API Reference](reference/core.md).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Pattern Nodes
|
|
2
|
+
|
|
3
|
+
Pattern nodes are the basic building blocks to construct matching logic over an AST.
|
|
4
|
+
|
|
5
|
+
## Basic Nodes
|
|
6
|
+
|
|
7
|
+
Basic nodes allow matching structural properties, capturing values, and simple boolean combinations.
|
|
8
|
+
|
|
9
|
+
::: ast_pattern_engine.nodes.basic
|
|
10
|
+
options:
|
|
11
|
+
show_root_heading: true
|
|
12
|
+
|
|
13
|
+
## Sequence Nodes
|
|
14
|
+
|
|
15
|
+
Sequence nodes allow matching groups of adjacent nodes, similar to regex patterns.
|
|
16
|
+
|
|
17
|
+
::: ast_pattern_engine.nodes.sequences
|
|
18
|
+
options:
|
|
19
|
+
show_root_heading: true
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* Add a simple visual divider above each mkdocstrings API object */
|
|
2
|
+
.doc-object {
|
|
3
|
+
border-top: 1px solid var(--md-default-fg-color--lightest);
|
|
4
|
+
margin-top: 3rem;
|
|
5
|
+
padding-top: 1rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* Remove the divider from the very first item on the page */
|
|
9
|
+
.doc-object:first-child {
|
|
10
|
+
border-top: none;
|
|
11
|
+
margin-top: 0;
|
|
12
|
+
padding-top: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Ensure the heading sits neatly below the divider */
|
|
16
|
+
.doc-heading,
|
|
17
|
+
.doc-symbol-heading {
|
|
18
|
+
margin-top: 0 !important;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Indent the contents (docstrings, parameters, methods) of classes and functions */
|
|
22
|
+
.doc-contents {
|
|
23
|
+
margin-left: 1.5rem;
|
|
24
|
+
padding-left: 1rem;
|
|
25
|
+
border-left: 2px solid var(--md-default-fg-color--lightest);
|
|
26
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
site_name: AST Pattern Engine
|
|
2
|
+
site_url: https://80svectorz.github.io/ast_pattern_engine/
|
|
3
|
+
repo_url: https://github.com/80sVectorz/ast_pattern_engine
|
|
4
|
+
|
|
5
|
+
theme:
|
|
6
|
+
name: material
|
|
7
|
+
palette:
|
|
8
|
+
- scheme: slate
|
|
9
|
+
primary: teal
|
|
10
|
+
accent: teal
|
|
11
|
+
features:
|
|
12
|
+
- navigation.tabs
|
|
13
|
+
- content.code.copy
|
|
14
|
+
- toc.follow
|
|
15
|
+
|
|
16
|
+
plugins:
|
|
17
|
+
- search
|
|
18
|
+
- mkdocstrings:
|
|
19
|
+
default_handler: python
|
|
20
|
+
handlers:
|
|
21
|
+
python:
|
|
22
|
+
options:
|
|
23
|
+
docstring_style: google
|
|
24
|
+
show_root_heading: true
|
|
25
|
+
show_source: true
|
|
26
|
+
separate_signature: true
|
|
27
|
+
show_signature_annotations: true
|
|
28
|
+
show_symbol_type_heading: true
|
|
29
|
+
show_symbol_type_toc: true
|
|
30
|
+
|
|
31
|
+
markdown_extensions:
|
|
32
|
+
- pymdownx.highlight:
|
|
33
|
+
anchor_linenums: true
|
|
34
|
+
line_spans: __span
|
|
35
|
+
pygments_style: material
|
|
36
|
+
- pymdownx.inlinehilite
|
|
37
|
+
- pymdownx.snippets
|
|
38
|
+
- pymdownx.superfences
|
|
39
|
+
|
|
40
|
+
extra_css:
|
|
41
|
+
- stylesheets/extra.css
|
|
42
|
+
|
|
43
|
+
nav:
|
|
44
|
+
- Home: index.md
|
|
45
|
+
- Guide & Philosophy: guide.md
|
|
46
|
+
- API Reference:
|
|
47
|
+
- Core & Engine: reference/core.md
|
|
48
|
+
- Pattern Nodes: reference/nodes.md
|
|
49
|
+
- AST Visitors: reference/visitors.md
|
|
50
|
+
- QoL Templates: reference/templates.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ast-pattern-engine"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.2"
|
|
4
4
|
description = "A library for regex-inspired fine-grained AST pattern matching and replacing"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -42,6 +42,8 @@ dev = [
|
|
|
42
42
|
"pytest-cov>=7.1.0",
|
|
43
43
|
"ruff>=0.11.0",
|
|
44
44
|
]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
docs = [
|
|
46
|
+
"mkdocs>=1.6.1",
|
|
47
|
+
"mkdocs-material>=9.7.6",
|
|
48
|
+
"mkdocstrings[python]>=1.0.4",
|
|
49
|
+
]
|
|
@@ -11,10 +11,10 @@ from ast_pattern_engine.visitors import SingleOccurrenceFinder
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class Bind(Pattern):
|
|
14
|
-
"""Bind the current node to `
|
|
14
|
+
"""Bind the current node to `key`.
|
|
15
15
|
|
|
16
16
|
This is syntactic sugar for:
|
|
17
|
-
|
|
17
|
+
>>> Collect(WildCard(), "x")
|
|
18
18
|
|
|
19
19
|
Args:
|
|
20
20
|
key: The key to bind the node(s) or value(s) to.
|
|
@@ -23,6 +23,11 @@ class Bind(Pattern):
|
|
|
23
23
|
key: str
|
|
24
24
|
|
|
25
25
|
def __init__(self, key: str):
|
|
26
|
+
"""Bind node.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
key: The key to bind the node(s) or value(s) to.
|
|
30
|
+
"""
|
|
26
31
|
self.key = key
|
|
27
32
|
|
|
28
33
|
def match_node(
|
|
@@ -66,7 +71,16 @@ class NodePattern(Pattern):
|
|
|
66
71
|
**field_patterns: Patterns or exact values to match against the node's fields.
|
|
67
72
|
"""
|
|
68
73
|
|
|
74
|
+
node_type: type[ast.AST]
|
|
75
|
+
field_patterns: dict[str, Pattern | Any]
|
|
76
|
+
|
|
69
77
|
def __init__(self, node_type: type[ast.AST], **field_patterns: Pattern | Any):
|
|
78
|
+
"""NodePattern node.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
node_type: The AST node class to match (e.g., ast.Assign).
|
|
82
|
+
**field_patterns: Patterns or exact values to match against the node's fields.
|
|
83
|
+
"""
|
|
70
84
|
self.node_type = node_type
|
|
71
85
|
self.field_patterns = field_patterns
|
|
72
86
|
|
|
@@ -119,12 +133,15 @@ class Collect(Pattern):
|
|
|
119
133
|
"""
|
|
120
134
|
|
|
121
135
|
pattern: Pattern
|
|
122
|
-
"""The pattern to match."""
|
|
123
|
-
|
|
124
136
|
key: str
|
|
125
|
-
"""The key to bind the pattern result to."""
|
|
126
137
|
|
|
127
138
|
def __init__(self, pattern: Pattern, key: str):
|
|
139
|
+
"""Collect node.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
pattern: The pattern to match.
|
|
143
|
+
key: The key to bind the pattern result to.
|
|
144
|
+
"""
|
|
128
145
|
self.pattern = pattern
|
|
129
146
|
self.key = key
|
|
130
147
|
|
|
@@ -178,12 +195,15 @@ class Filter(Pattern):
|
|
|
178
195
|
"""
|
|
179
196
|
|
|
180
197
|
predicate: Callable[[Any], bool]
|
|
181
|
-
"""The predicate to match."""
|
|
182
|
-
|
|
183
198
|
key: str | None
|
|
184
|
-
"""The key to bind the node to."""
|
|
185
199
|
|
|
186
200
|
def __init__(self, predicate: Callable[[Any], bool], key: str | None = None):
|
|
201
|
+
"""Filter node.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
predicate: A callable that returns True if the node matches.
|
|
205
|
+
key: Optional key to bind the matched node to.
|
|
206
|
+
"""
|
|
187
207
|
self.predicate = predicate
|
|
188
208
|
self.key = key
|
|
189
209
|
|
|
@@ -215,7 +235,14 @@ class Not(Pattern):
|
|
|
215
235
|
pattern: The pattern that must fail for this to match.
|
|
216
236
|
"""
|
|
217
237
|
|
|
238
|
+
pattern: Pattern
|
|
239
|
+
|
|
218
240
|
def __init__(self, pattern: Pattern):
|
|
241
|
+
"""Not node.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
pattern: The pattern that must fail for this to match.
|
|
245
|
+
"""
|
|
219
246
|
self.pattern = pattern
|
|
220
247
|
|
|
221
248
|
def match_node(
|
|
@@ -243,7 +270,14 @@ class Contains(Pattern):
|
|
|
243
270
|
pattern: The pattern or sequence of patterns to search for in the sub-tree.
|
|
244
271
|
"""
|
|
245
272
|
|
|
273
|
+
pattern: Sequence[Pattern]
|
|
274
|
+
|
|
246
275
|
def __init__(self, pattern: Sequence[Pattern]):
|
|
276
|
+
"""Contains node.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
pattern: The pattern or sequence of patterns to search for in the sub-tree.
|
|
280
|
+
"""
|
|
247
281
|
self.pattern = list(pattern)
|
|
248
282
|
|
|
249
283
|
def match_node(
|
|
@@ -278,9 +312,13 @@ class AllOf(Pattern):
|
|
|
278
312
|
"""
|
|
279
313
|
|
|
280
314
|
patterns: Sequence[Pattern]
|
|
281
|
-
"""The patterns to match."""
|
|
282
315
|
|
|
283
316
|
def __init__(self, patterns: Sequence[Pattern]):
|
|
317
|
+
"""AllOf node.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
patterns: Sequence of patterns that must all match the node.
|
|
321
|
+
"""
|
|
284
322
|
self.patterns = list(patterns)
|
|
285
323
|
|
|
286
324
|
def match_node(
|
|
@@ -310,9 +348,13 @@ class AnyOf(Pattern):
|
|
|
310
348
|
"""
|
|
311
349
|
|
|
312
350
|
patterns: list[Pattern]
|
|
313
|
-
"""The patterns to match."""
|
|
314
351
|
|
|
315
352
|
def __init__(self, patterns: Sequence[Pattern]):
|
|
353
|
+
"""AnyOf node.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
patterns: Sequence of patterns where at least one must match.
|
|
357
|
+
"""
|
|
316
358
|
self.patterns = list(patterns)
|
|
317
359
|
|
|
318
360
|
def match_node(
|
{ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/nodes/sequences.py
RENAMED
|
@@ -22,16 +22,16 @@ class Repetition(SequencePattern):
|
|
|
22
22
|
"""Matches a single pattern zero or more times.
|
|
23
23
|
|
|
24
24
|
Also supports specifying min and max match count thresholds
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
pattern: The pattern to match.
|
|
28
|
+
min_matches: Minimum number of matches required. Default is 1.
|
|
29
|
+
max_matches: Maximum number of allowed matches. Defaults to None.
|
|
25
30
|
"""
|
|
26
31
|
|
|
27
32
|
pattern: Pattern
|
|
28
|
-
"""The pattern to match."""
|
|
29
|
-
|
|
30
33
|
min_matches: int
|
|
31
|
-
"""Minimum number of matches required. Defaults to 1."""
|
|
32
|
-
|
|
33
34
|
max_matches: int | None
|
|
34
|
-
"""Maximum number of allowed matches. Defaults to None."""
|
|
35
35
|
|
|
36
36
|
def __init__(
|
|
37
37
|
self,
|
|
@@ -42,9 +42,9 @@ class Repetition(SequencePattern):
|
|
|
42
42
|
"""Repetition node.
|
|
43
43
|
|
|
44
44
|
Args:
|
|
45
|
-
pattern: The
|
|
46
|
-
min_matches:
|
|
47
|
-
max_matches:
|
|
45
|
+
pattern: The pattern to match.
|
|
46
|
+
min_matches: Minimum number of matches required. Default is 1.
|
|
47
|
+
max_matches: Maximum number of allowed matches. Defaults to None.
|
|
48
48
|
"""
|
|
49
49
|
self.pattern = pattern
|
|
50
50
|
self.min_matches = min_matches
|
|
@@ -52,13 +52,15 @@ class Repetition(SequencePattern):
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
class PatternGroup(SequencePattern):
|
|
55
|
-
"""Matches a compound pattern/pattern group to an AST node sequence.
|
|
55
|
+
"""Matches a compound pattern/pattern group to an AST node sequence.
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
Args:
|
|
58
|
+
pattern: The compound pattern to match.
|
|
59
|
+
key: Optional key to bind the matched pattern to.
|
|
60
|
+
"""
|
|
59
61
|
|
|
62
|
+
pattern: Sequence[Pattern]
|
|
60
63
|
key: str | None
|
|
61
|
-
"""Optional key to bind the matched pattern to."""
|
|
62
64
|
|
|
63
65
|
def __init__(self, pattern: Sequence[Pattern], key: str | None = None) -> None:
|
|
64
66
|
"""PatternGroup node.
|
|
@@ -84,30 +86,34 @@ class OneOf(SequencePattern):
|
|
|
84
86
|
"""
|
|
85
87
|
|
|
86
88
|
patterns: list[Pattern]
|
|
87
|
-
"""The patterns to match."""
|
|
88
|
-
|
|
89
89
|
strict: bool
|
|
90
|
-
"""Whether to be strict and only match if *exactly* one pattern matches."""
|
|
91
|
-
|
|
92
90
|
key: str | None
|
|
93
|
-
"""Optional key to bind the matched pattern to."""
|
|
94
91
|
|
|
95
92
|
def __init__(
|
|
96
93
|
self, patterns: Sequence[Pattern], strict: bool = False, key: str | None = None
|
|
97
94
|
) -> None:
|
|
95
|
+
"""OneOf node.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
patterns: The patterns to match
|
|
99
|
+
strict: Whether to be strict and only match if *exactly* one pattern matches
|
|
100
|
+
key: Optional key to bind the matched pattern to
|
|
101
|
+
"""
|
|
98
102
|
self.patterns = list(patterns)
|
|
99
103
|
self.strict = strict
|
|
100
104
|
self.key = key
|
|
101
105
|
|
|
102
106
|
|
|
103
107
|
class Optional(SequencePattern):
|
|
104
|
-
"""Matches a pattern zero or one times.
|
|
108
|
+
"""Matches a pattern zero or one times.
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
Args:
|
|
111
|
+
pattern: The pattern to match.
|
|
112
|
+
key: Optional key to bind the matched pattern to.
|
|
113
|
+
"""
|
|
108
114
|
|
|
115
|
+
pattern: Pattern
|
|
109
116
|
key: str | None
|
|
110
|
-
"""Optional key to bind the matched pattern to."""
|
|
111
117
|
|
|
112
118
|
def __init__(self, pattern: Pattern, key: str | None = None) -> None:
|
|
113
119
|
"""Optional node.
|
|
@@ -29,6 +29,9 @@ class PatternTransformer(ast.NodeTransformer):
|
|
|
29
29
|
Args:
|
|
30
30
|
pattern: Sequence of pattern nodes matched against each candidate span.
|
|
31
31
|
actions: Mapping from collect keys to replacement handlers or None.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
matches: List of bindings for each match.
|
|
32
35
|
"""
|
|
33
36
|
|
|
34
37
|
def __init__(
|
|
@@ -359,6 +362,9 @@ class BottomUpPatternTransformer(ast.NodeTransformer):
|
|
|
359
362
|
Args:
|
|
360
363
|
pattern: Sequence of pattern nodes matched against each candidate span.
|
|
361
364
|
actions: Mapping from collect keys to replacement handlers or None.
|
|
365
|
+
|
|
366
|
+
Attributes:
|
|
367
|
+
matches: List of bindings for each match.
|
|
362
368
|
"""
|
|
363
369
|
|
|
364
370
|
def __init__(
|
|
@@ -442,6 +448,10 @@ class PatternFinder(ast.NodeVisitor):
|
|
|
442
448
|
|
|
443
449
|
Args:
|
|
444
450
|
pattern: Sequence of pattern nodes to search for.
|
|
451
|
+
|
|
452
|
+
Attributes:
|
|
453
|
+
matches: List of bindings for each match.
|
|
454
|
+
visited: Set of visited node IDs.
|
|
445
455
|
"""
|
|
446
456
|
|
|
447
457
|
def __init__(self, pattern: Sequence[Pattern]):
|
|
@@ -487,6 +497,10 @@ class SingleOccurrenceFinder(ast.NodeVisitor):
|
|
|
487
497
|
|
|
488
498
|
Args:
|
|
489
499
|
pattern: Sequence of pattern nodes to search for.
|
|
500
|
+
|
|
501
|
+
Attributes:
|
|
502
|
+
match_node: The first match node, if any.
|
|
503
|
+
match_bindings: The bindings for the first match, if any.
|
|
490
504
|
"""
|
|
491
505
|
|
|
492
506
|
match_node: ast.AST | None
|
|
@@ -497,15 +511,15 @@ class SingleOccurrenceFinder(ast.NodeVisitor):
|
|
|
497
511
|
self.match_node = None
|
|
498
512
|
self.match_bindings = {}
|
|
499
513
|
self.pattern = pattern
|
|
500
|
-
self.
|
|
514
|
+
self._found_match = False
|
|
501
515
|
|
|
502
516
|
def visit(self, node: ast.AST):
|
|
503
|
-
if self.
|
|
517
|
+
if self._found_match:
|
|
504
518
|
return # short-circuit: we've already found a match
|
|
505
519
|
|
|
506
520
|
res = _match_patterns(self.pattern, [node], 0, {})
|
|
507
521
|
if res:
|
|
508
|
-
self.
|
|
522
|
+
self._found_match = True
|
|
509
523
|
self.match_node = node
|
|
510
524
|
self.match_bindings = res[0][0]
|
|
511
525
|
return
|
|
@@ -516,12 +530,12 @@ class SingleOccurrenceFinder(ast.NodeVisitor):
|
|
|
516
530
|
for item in val:
|
|
517
531
|
if isinstance(item, ast.AST):
|
|
518
532
|
self.visit(item)
|
|
519
|
-
if self.
|
|
533
|
+
if self._found_match:
|
|
520
534
|
return
|
|
521
535
|
elif isinstance(val, ast.AST):
|
|
522
536
|
self.visit(val)
|
|
523
|
-
if self.
|
|
537
|
+
if self._found_match:
|
|
524
538
|
return
|
|
525
539
|
|
|
526
540
|
def found_match(self) -> bool:
|
|
527
|
-
return self.
|
|
541
|
+
return self._found_match
|
|
@@ -31,6 +31,6 @@ def test_single_occurrence_finder_early_exit():
|
|
|
31
31
|
finder = SingleOccurrenceFinder(pattern)
|
|
32
32
|
|
|
33
33
|
# We manually set found to True to test early exit in visit
|
|
34
|
-
finder.
|
|
34
|
+
finder._found_match = True
|
|
35
35
|
finder.visit(tree)
|
|
36
36
|
assert finder.match_node is None # Didn't actually match because it early exited
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/nodes/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/visitors/test_pattern_transformer.py
RENAMED
|
File without changes
|