ast-pattern-engine 1.0.0__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.2/.github/workflows/ci.yml +86 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/PKG-INFO +8 -1
- {ast_pattern_engine-1.0.0 → 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.0 → ast_pattern_engine-1.0.2}/pyproject.toml +7 -1
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/core.py +10 -2
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/engine.py +18 -6
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/nodes/basic.py +117 -23
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/nodes/sequences.py +32 -22
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/visitors.py +22 -7
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/patterns/test_any_of.py +8 -18
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/patterns/test_collect.py +1 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/patterns/test_filter.py +4 -3
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/patterns/test_one_of.py +4 -3
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/patterns/test_pattern_group.py +1 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/patterns/test_repetition.py +1 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/test_engine.py +1 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/visitors/test_bottom_up_pattern_transformer.py +2 -1
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/visitors/test_pattern_finder.py +4 -1
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/visitors/test_pattern_transformer.py +14 -10
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/visitors/test_single_occurrence_finder.py +3 -3
- ast_pattern_engine-1.0.0/.github/workflows/ci.yml +0 -44
- ast_pattern_engine-1.0.0/src/ast_pattern_engine/plumbing.py +0 -2
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/.gitignore +0 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/LICENSE +0 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/examples/dict_get_rewrite.py +0 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/__init__.py +0 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/nodes/__init__.py +0 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/py.typed +0 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/templates.py +0 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/__init__.py +0 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/patterns/test_all_of.py +0 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/patterns/test_bind.py +0 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/patterns/test_contains.py +0 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/patterns/test_not.py +0 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/patterns/test_optional.py +0 -0
- {ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/patterns/test_templates.py +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
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 = [
|
|
@@ -40,4 +40,10 @@ pythonpath = ["src"]
|
|
|
40
40
|
[dependency-groups]
|
|
41
41
|
dev = [
|
|
42
42
|
"pytest-cov>=7.1.0",
|
|
43
|
+
"ruff>=0.11.0",
|
|
43
44
|
]
|
|
45
|
+
docs = [
|
|
46
|
+
"mkdocs>=1.6.1",
|
|
47
|
+
"mkdocs-material>=9.7.6",
|
|
48
|
+
"mkdocstrings[python]>=1.0.4",
|
|
49
|
+
]
|
|
@@ -8,7 +8,11 @@ class Pattern(ast.AST):
|
|
|
8
8
|
|
|
9
9
|
# public API
|
|
10
10
|
def match_node(
|
|
11
|
-
self,
|
|
11
|
+
self,
|
|
12
|
+
node: object,
|
|
13
|
+
bindings: dict[str, object] | None = None,
|
|
14
|
+
*,
|
|
15
|
+
_force_list: bool = False,
|
|
12
16
|
):
|
|
13
17
|
"""Match *node* and return updated *bindings* or *None*."""
|
|
14
18
|
raise NotImplementedError
|
|
@@ -21,7 +25,11 @@ class Pattern(ast.AST):
|
|
|
21
25
|
|
|
22
26
|
class SequencePattern(Pattern):
|
|
23
27
|
def match_node(
|
|
24
|
-
self,
|
|
28
|
+
self,
|
|
29
|
+
node: object,
|
|
30
|
+
bindings: dict[str, object] | None = None,
|
|
31
|
+
*,
|
|
32
|
+
_force_list: bool = False,
|
|
25
33
|
):
|
|
26
34
|
# Matching is handled by engine._match_sequence
|
|
27
35
|
raise NotImplementedError(
|
|
@@ -26,7 +26,9 @@ def _match_patterns(
|
|
|
26
26
|
|
|
27
27
|
match first:
|
|
28
28
|
case PatternGroup(pattern=sub_pattern, key=key):
|
|
29
|
-
res = _match_patterns(
|
|
29
|
+
res = _match_patterns(
|
|
30
|
+
sub_pattern, nodes, pos, dict(bindings), _force_list=_force_list
|
|
31
|
+
)
|
|
30
32
|
if res:
|
|
31
33
|
new_bindings = res[-1][0]
|
|
32
34
|
if key is not None:
|
|
@@ -40,7 +42,9 @@ def _match_patterns(
|
|
|
40
42
|
new_bindings = dict(bindings)
|
|
41
43
|
n_reps = 0
|
|
42
44
|
while n_reps < (max_matches or len(nodes)) and pos < len(nodes):
|
|
43
|
-
res = _match_patterns(
|
|
45
|
+
res = _match_patterns(
|
|
46
|
+
[sub_pattern], nodes, pos, dict(new_bindings), _force_list=True
|
|
47
|
+
)
|
|
44
48
|
if not res:
|
|
45
49
|
break
|
|
46
50
|
new_bindings, pos = res[-1]
|
|
@@ -54,7 +58,9 @@ def _match_patterns(
|
|
|
54
58
|
new_pos = pos
|
|
55
59
|
n_matches = 0
|
|
56
60
|
for pattern in sub_patterns:
|
|
57
|
-
res = _match_patterns(
|
|
61
|
+
res = _match_patterns(
|
|
62
|
+
[pattern], nodes, pos, dict(bindings), _force_list=_force_list
|
|
63
|
+
)
|
|
58
64
|
if res:
|
|
59
65
|
n_matches += 1
|
|
60
66
|
if n_matches == 1:
|
|
@@ -74,7 +80,9 @@ def _match_patterns(
|
|
|
74
80
|
out.append((new_bindings, pos))
|
|
75
81
|
|
|
76
82
|
case Optional(pattern=sub_pattern, key=key):
|
|
77
|
-
res = _match_patterns(
|
|
83
|
+
res = _match_patterns(
|
|
84
|
+
[sub_pattern], nodes, pos, dict(bindings), _force_list=_force_list
|
|
85
|
+
)
|
|
78
86
|
if res:
|
|
79
87
|
new_bindings, new_pos = res[-1]
|
|
80
88
|
if key is not None:
|
|
@@ -88,13 +96,17 @@ def _match_patterns(
|
|
|
88
96
|
case _:
|
|
89
97
|
# single node pattern
|
|
90
98
|
if pos < len(nodes):
|
|
91
|
-
res = first.match_node(
|
|
99
|
+
res = first.match_node(
|
|
100
|
+
nodes[pos], dict(bindings), _force_list=_force_list
|
|
101
|
+
)
|
|
92
102
|
if res is not None:
|
|
93
103
|
out.append((res, pos + 1))
|
|
94
104
|
|
|
95
105
|
# Match remaining patterns
|
|
96
106
|
if out and remaining:
|
|
97
|
-
rem_res = _match_patterns(
|
|
107
|
+
rem_res = _match_patterns(
|
|
108
|
+
remaining, nodes, out[-1][1], out[-1][0], _force_list=_force_list
|
|
109
|
+
)
|
|
98
110
|
if not rem_res:
|
|
99
111
|
return []
|
|
100
112
|
out.extend(rem_res)
|
|
@@ -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,9 +23,20 @@ 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
|
-
def match_node(
|
|
33
|
+
def match_node(
|
|
34
|
+
self,
|
|
35
|
+
node: Any,
|
|
36
|
+
bindings: dict[str, Any] | None = None,
|
|
37
|
+
*,
|
|
38
|
+
_force_list: bool = False,
|
|
39
|
+
):
|
|
29
40
|
bindings = bindings or {}
|
|
30
41
|
if self.key in bindings:
|
|
31
42
|
if not _force_list:
|
|
@@ -41,7 +52,13 @@ class WildCard(Pattern):
|
|
|
41
52
|
|
|
42
53
|
def __init__(self): ...
|
|
43
54
|
|
|
44
|
-
def match_node(
|
|
55
|
+
def match_node(
|
|
56
|
+
self,
|
|
57
|
+
node: Any,
|
|
58
|
+
bindings: dict[str, Any] | None = None,
|
|
59
|
+
*,
|
|
60
|
+
_force_list: bool = False,
|
|
61
|
+
):
|
|
45
62
|
bindings = bindings or {}
|
|
46
63
|
return bindings
|
|
47
64
|
|
|
@@ -54,11 +71,26 @@ class NodePattern(Pattern):
|
|
|
54
71
|
**field_patterns: Patterns or exact values to match against the node's fields.
|
|
55
72
|
"""
|
|
56
73
|
|
|
74
|
+
node_type: type[ast.AST]
|
|
75
|
+
field_patterns: dict[str, Pattern | Any]
|
|
76
|
+
|
|
57
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
|
+
"""
|
|
58
84
|
self.node_type = node_type
|
|
59
85
|
self.field_patterns = field_patterns
|
|
60
86
|
|
|
61
|
-
def match_node(
|
|
87
|
+
def match_node(
|
|
88
|
+
self,
|
|
89
|
+
node: Any,
|
|
90
|
+
bindings: dict[str, Any] | None = None,
|
|
91
|
+
*,
|
|
92
|
+
_force_list: bool = False,
|
|
93
|
+
):
|
|
62
94
|
bindings = bindings or {}
|
|
63
95
|
if not isinstance(node, self.node_type):
|
|
64
96
|
return None
|
|
@@ -85,9 +117,7 @@ class NodePattern(Pattern):
|
|
|
85
117
|
return None
|
|
86
118
|
merged[k] = self._to_list(merged[k]) + self._to_list(v)
|
|
87
119
|
else:
|
|
88
|
-
merged[k] = (
|
|
89
|
-
self._to_list(v) if _force_list else v
|
|
90
|
-
)
|
|
120
|
+
merged[k] = self._to_list(v) if _force_list else v
|
|
91
121
|
else:
|
|
92
122
|
if val != pat:
|
|
93
123
|
return None
|
|
@@ -103,17 +133,24 @@ class Collect(Pattern):
|
|
|
103
133
|
"""
|
|
104
134
|
|
|
105
135
|
pattern: Pattern
|
|
106
|
-
"""The pattern to match."""
|
|
107
|
-
|
|
108
136
|
key: str
|
|
109
|
-
"""The key to bind the pattern result to."""
|
|
110
137
|
|
|
111
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
|
+
"""
|
|
112
145
|
self.pattern = pattern
|
|
113
146
|
self.key = key
|
|
114
147
|
|
|
115
148
|
def match_node(
|
|
116
|
-
self,
|
|
149
|
+
self,
|
|
150
|
+
node: Any,
|
|
151
|
+
bindings: dict[str, Any] | None = None,
|
|
152
|
+
*,
|
|
153
|
+
_force_list: bool = False,
|
|
117
154
|
) -> None | dict[str, Any]:
|
|
118
155
|
bindings = bindings or {}
|
|
119
156
|
# Collect is a binding boundary — inner patterns always see _force_list=False
|
|
@@ -158,16 +195,25 @@ class Filter(Pattern):
|
|
|
158
195
|
"""
|
|
159
196
|
|
|
160
197
|
predicate: Callable[[Any], bool]
|
|
161
|
-
"""The predicate to match."""
|
|
162
|
-
|
|
163
198
|
key: str | None
|
|
164
|
-
"""The key to bind the node to."""
|
|
165
199
|
|
|
166
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
|
+
"""
|
|
167
207
|
self.predicate = predicate
|
|
168
208
|
self.key = key
|
|
169
209
|
|
|
170
|
-
def match_node(
|
|
210
|
+
def match_node(
|
|
211
|
+
self,
|
|
212
|
+
node: Any,
|
|
213
|
+
bindings: dict[str, Any] | None = None,
|
|
214
|
+
*,
|
|
215
|
+
_force_list: bool = False,
|
|
216
|
+
):
|
|
171
217
|
bindings = bindings or {}
|
|
172
218
|
if not self.predicate(node):
|
|
173
219
|
return None
|
|
@@ -189,10 +235,23 @@ class Not(Pattern):
|
|
|
189
235
|
pattern: The pattern that must fail for this to match.
|
|
190
236
|
"""
|
|
191
237
|
|
|
238
|
+
pattern: Pattern
|
|
239
|
+
|
|
192
240
|
def __init__(self, pattern: Pattern):
|
|
241
|
+
"""Not node.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
pattern: The pattern that must fail for this to match.
|
|
245
|
+
"""
|
|
193
246
|
self.pattern = pattern
|
|
194
247
|
|
|
195
|
-
def match_node(
|
|
248
|
+
def match_node(
|
|
249
|
+
self,
|
|
250
|
+
node: Any,
|
|
251
|
+
bindings: dict[str, Any] | None = None,
|
|
252
|
+
*,
|
|
253
|
+
_force_list: bool = False,
|
|
254
|
+
):
|
|
196
255
|
bindings = bindings or {}
|
|
197
256
|
|
|
198
257
|
# Use _match_patterns so that SequencePatterns (like OneOf) don't
|
|
@@ -211,10 +270,23 @@ class Contains(Pattern):
|
|
|
211
270
|
pattern: The pattern or sequence of patterns to search for in the sub-tree.
|
|
212
271
|
"""
|
|
213
272
|
|
|
273
|
+
pattern: Sequence[Pattern]
|
|
274
|
+
|
|
214
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
|
+
"""
|
|
215
281
|
self.pattern = list(pattern)
|
|
216
282
|
|
|
217
|
-
def match_node(
|
|
283
|
+
def match_node(
|
|
284
|
+
self,
|
|
285
|
+
node: Any,
|
|
286
|
+
bindings: dict[str, Any] | None = None,
|
|
287
|
+
*,
|
|
288
|
+
_force_list: bool = False,
|
|
289
|
+
):
|
|
218
290
|
bindings = bindings or {}
|
|
219
291
|
finder = SingleOccurrenceFinder(self.pattern)
|
|
220
292
|
finder.visit(node)
|
|
@@ -240,17 +312,29 @@ class AllOf(Pattern):
|
|
|
240
312
|
"""
|
|
241
313
|
|
|
242
314
|
patterns: Sequence[Pattern]
|
|
243
|
-
"""The patterns to match."""
|
|
244
315
|
|
|
245
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
|
+
"""
|
|
246
322
|
self.patterns = list(patterns)
|
|
247
323
|
|
|
248
|
-
def match_node(
|
|
324
|
+
def match_node(
|
|
325
|
+
self,
|
|
326
|
+
node: Any,
|
|
327
|
+
bindings: dict[str, Any] | None = None,
|
|
328
|
+
*,
|
|
329
|
+
_force_list: bool = False,
|
|
330
|
+
):
|
|
249
331
|
bindings = bindings or {}
|
|
250
332
|
new_bindings = dict(bindings)
|
|
251
333
|
|
|
252
334
|
for pattern in self.patterns:
|
|
253
|
-
new_bindings = pattern.match_node(
|
|
335
|
+
new_bindings = pattern.match_node(
|
|
336
|
+
node, new_bindings, _force_list=_force_list
|
|
337
|
+
)
|
|
254
338
|
if new_bindings is None:
|
|
255
339
|
return None
|
|
256
340
|
return new_bindings
|
|
@@ -264,12 +348,22 @@ class AnyOf(Pattern):
|
|
|
264
348
|
"""
|
|
265
349
|
|
|
266
350
|
patterns: list[Pattern]
|
|
267
|
-
"""The patterns to match."""
|
|
268
351
|
|
|
269
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
|
+
"""
|
|
270
358
|
self.patterns = list(patterns)
|
|
271
359
|
|
|
272
|
-
def match_node(
|
|
360
|
+
def match_node(
|
|
361
|
+
self,
|
|
362
|
+
node: Any,
|
|
363
|
+
bindings: dict[str, Any] | None = None,
|
|
364
|
+
*,
|
|
365
|
+
_force_list: bool = False,
|
|
366
|
+
):
|
|
273
367
|
bindings = bindings or {}
|
|
274
368
|
merged = dict(bindings)
|
|
275
369
|
matched_any = False
|
{ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/nodes/sequences.py
RENAMED
|
@@ -6,7 +6,11 @@ from ast_pattern_engine.core import Pattern
|
|
|
6
6
|
|
|
7
7
|
class SequencePattern(Pattern):
|
|
8
8
|
def match_node(
|
|
9
|
-
self,
|
|
9
|
+
self,
|
|
10
|
+
node: object,
|
|
11
|
+
bindings: dict[str, object] | None = None,
|
|
12
|
+
*,
|
|
13
|
+
_force_list: bool = False,
|
|
10
14
|
):
|
|
11
15
|
# Matching is handled by engine._match_sequence
|
|
12
16
|
raise NotImplementedError(
|
|
@@ -18,16 +22,16 @@ class Repetition(SequencePattern):
|
|
|
18
22
|
"""Matches a single pattern zero or more times.
|
|
19
23
|
|
|
20
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.
|
|
21
30
|
"""
|
|
22
31
|
|
|
23
32
|
pattern: Pattern
|
|
24
|
-
"""The pattern to match."""
|
|
25
|
-
|
|
26
33
|
min_matches: int
|
|
27
|
-
"""Minimum number of matches required. Defaults to 1."""
|
|
28
|
-
|
|
29
34
|
max_matches: int | None
|
|
30
|
-
"""Maximum number of allowed matches. Defaults to None."""
|
|
31
35
|
|
|
32
36
|
def __init__(
|
|
33
37
|
self,
|
|
@@ -38,9 +42,9 @@ class Repetition(SequencePattern):
|
|
|
38
42
|
"""Repetition node.
|
|
39
43
|
|
|
40
44
|
Args:
|
|
41
|
-
pattern: The
|
|
42
|
-
min_matches:
|
|
43
|
-
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.
|
|
44
48
|
"""
|
|
45
49
|
self.pattern = pattern
|
|
46
50
|
self.min_matches = min_matches
|
|
@@ -48,13 +52,15 @@ class Repetition(SequencePattern):
|
|
|
48
52
|
|
|
49
53
|
|
|
50
54
|
class PatternGroup(SequencePattern):
|
|
51
|
-
"""Matches a compound pattern/pattern group to an AST node sequence.
|
|
55
|
+
"""Matches a compound pattern/pattern group to an AST node sequence.
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
Args:
|
|
58
|
+
pattern: The compound pattern to match.
|
|
59
|
+
key: Optional key to bind the matched pattern to.
|
|
60
|
+
"""
|
|
55
61
|
|
|
62
|
+
pattern: Sequence[Pattern]
|
|
56
63
|
key: str | None
|
|
57
|
-
"""Optional key to bind the matched pattern to."""
|
|
58
64
|
|
|
59
65
|
def __init__(self, pattern: Sequence[Pattern], key: str | None = None) -> None:
|
|
60
66
|
"""PatternGroup node.
|
|
@@ -80,30 +86,34 @@ class OneOf(SequencePattern):
|
|
|
80
86
|
"""
|
|
81
87
|
|
|
82
88
|
patterns: list[Pattern]
|
|
83
|
-
"""The patterns to match."""
|
|
84
|
-
|
|
85
89
|
strict: bool
|
|
86
|
-
"""Whether to be strict and only match if *exactly* one pattern matches."""
|
|
87
|
-
|
|
88
90
|
key: str | None
|
|
89
|
-
"""Optional key to bind the matched pattern to."""
|
|
90
91
|
|
|
91
92
|
def __init__(
|
|
92
93
|
self, patterns: Sequence[Pattern], strict: bool = False, key: str | None = None
|
|
93
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
|
+
"""
|
|
94
102
|
self.patterns = list(patterns)
|
|
95
103
|
self.strict = strict
|
|
96
104
|
self.key = key
|
|
97
105
|
|
|
98
106
|
|
|
99
107
|
class Optional(SequencePattern):
|
|
100
|
-
"""Matches a pattern zero or one times.
|
|
108
|
+
"""Matches a pattern zero or one times.
|
|
101
109
|
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
Args:
|
|
111
|
+
pattern: The pattern to match.
|
|
112
|
+
key: Optional key to bind the matched pattern to.
|
|
113
|
+
"""
|
|
104
114
|
|
|
115
|
+
pattern: Pattern
|
|
105
116
|
key: str | None
|
|
106
|
-
"""Optional key to bind the matched pattern to."""
|
|
107
117
|
|
|
108
118
|
def __init__(self, pattern: Pattern, key: str | None = None) -> None:
|
|
109
119
|
"""Optional node.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from typing import TypeAlias
|
|
1
2
|
import ast
|
|
2
3
|
from typing import Any
|
|
3
4
|
from collections.abc import Sequence, Callable
|
|
@@ -5,7 +6,7 @@ from collections.abc import Sequence, Callable
|
|
|
5
6
|
from ast_pattern_engine.core import Pattern
|
|
6
7
|
from ast_pattern_engine.engine import _match_patterns
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
ReplaceResult: TypeAlias = ast.AST | list[ast.AST] | None
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class PatternTransformer(ast.NodeTransformer):
|
|
@@ -28,6 +29,9 @@ class PatternTransformer(ast.NodeTransformer):
|
|
|
28
29
|
Args:
|
|
29
30
|
pattern: Sequence of pattern nodes matched against each candidate span.
|
|
30
31
|
actions: Mapping from collect keys to replacement handlers or None.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
matches: List of bindings for each match.
|
|
31
35
|
"""
|
|
32
36
|
|
|
33
37
|
def __init__(
|
|
@@ -358,6 +362,9 @@ class BottomUpPatternTransformer(ast.NodeTransformer):
|
|
|
358
362
|
Args:
|
|
359
363
|
pattern: Sequence of pattern nodes matched against each candidate span.
|
|
360
364
|
actions: Mapping from collect keys to replacement handlers or None.
|
|
365
|
+
|
|
366
|
+
Attributes:
|
|
367
|
+
matches: List of bindings for each match.
|
|
361
368
|
"""
|
|
362
369
|
|
|
363
370
|
def __init__(
|
|
@@ -441,6 +448,10 @@ class PatternFinder(ast.NodeVisitor):
|
|
|
441
448
|
|
|
442
449
|
Args:
|
|
443
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.
|
|
444
455
|
"""
|
|
445
456
|
|
|
446
457
|
def __init__(self, pattern: Sequence[Pattern]):
|
|
@@ -486,6 +497,10 @@ class SingleOccurrenceFinder(ast.NodeVisitor):
|
|
|
486
497
|
|
|
487
498
|
Args:
|
|
488
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.
|
|
489
504
|
"""
|
|
490
505
|
|
|
491
506
|
match_node: ast.AST | None
|
|
@@ -496,15 +511,15 @@ class SingleOccurrenceFinder(ast.NodeVisitor):
|
|
|
496
511
|
self.match_node = None
|
|
497
512
|
self.match_bindings = {}
|
|
498
513
|
self.pattern = pattern
|
|
499
|
-
self.
|
|
514
|
+
self._found_match = False
|
|
500
515
|
|
|
501
516
|
def visit(self, node: ast.AST):
|
|
502
|
-
if self.
|
|
517
|
+
if self._found_match:
|
|
503
518
|
return # short-circuit: we've already found a match
|
|
504
519
|
|
|
505
520
|
res = _match_patterns(self.pattern, [node], 0, {})
|
|
506
521
|
if res:
|
|
507
|
-
self.
|
|
522
|
+
self._found_match = True
|
|
508
523
|
self.match_node = node
|
|
509
524
|
self.match_bindings = res[0][0]
|
|
510
525
|
return
|
|
@@ -515,12 +530,12 @@ class SingleOccurrenceFinder(ast.NodeVisitor):
|
|
|
515
530
|
for item in val:
|
|
516
531
|
if isinstance(item, ast.AST):
|
|
517
532
|
self.visit(item)
|
|
518
|
-
if self.
|
|
533
|
+
if self._found_match:
|
|
519
534
|
return
|
|
520
535
|
elif isinstance(val, ast.AST):
|
|
521
536
|
self.visit(val)
|
|
522
|
-
if self.
|
|
537
|
+
if self._found_match:
|
|
523
538
|
return
|
|
524
539
|
|
|
525
540
|
def found_match(self) -> bool:
|
|
526
|
-
return self.
|
|
541
|
+
return self._found_match
|
|
@@ -1,32 +1,22 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
from ast_pattern_engine.nodes.basic import AnyOf, NodePattern, Collect
|
|
3
3
|
|
|
4
|
+
|
|
4
5
|
def test_any_of_matches_and_conflicts():
|
|
5
6
|
node = ast.parse("1").body[0].value
|
|
6
|
-
|
|
7
|
+
|
|
7
8
|
# First matches
|
|
8
|
-
pattern1 = AnyOf([
|
|
9
|
-
NodePattern(ast.Constant),
|
|
10
|
-
NodePattern(ast.Name)
|
|
11
|
-
])
|
|
9
|
+
pattern1 = AnyOf([NodePattern(ast.Constant), NodePattern(ast.Name)])
|
|
12
10
|
assert pattern1.match_node(node, {"existing": 1}) == {"existing": 1}
|
|
13
|
-
|
|
11
|
+
|
|
14
12
|
# Second matches
|
|
15
|
-
pattern2 = AnyOf([
|
|
16
|
-
NodePattern(ast.Name),
|
|
17
|
-
NodePattern(ast.Constant)
|
|
18
|
-
])
|
|
13
|
+
pattern2 = AnyOf([NodePattern(ast.Name), NodePattern(ast.Constant)])
|
|
19
14
|
assert pattern2.match_node(node, {}) == {}
|
|
20
|
-
|
|
15
|
+
|
|
21
16
|
# None matches
|
|
22
|
-
pattern3 = AnyOf([
|
|
23
|
-
NodePattern(ast.Name),
|
|
24
|
-
NodePattern(ast.Assign)
|
|
25
|
-
])
|
|
17
|
+
pattern3 = AnyOf([NodePattern(ast.Name), NodePattern(ast.Assign)])
|
|
26
18
|
assert pattern3.match_node(node, {}) is None
|
|
27
19
|
|
|
28
20
|
# Match produces conflicting key
|
|
29
|
-
pattern4 = AnyOf([
|
|
30
|
-
Collect(NodePattern(ast.Constant), "c")
|
|
31
|
-
])
|
|
21
|
+
pattern4 = AnyOf([Collect(NodePattern(ast.Constant), "c")])
|
|
32
22
|
assert pattern4.match_node(node, {"c": "conflict"}) is None
|
|
@@ -3,6 +3,7 @@ from ast_pattern_engine.nodes.basic import Filter
|
|
|
3
3
|
from ast_pattern_engine.nodes.sequences import Repetition
|
|
4
4
|
from ast_pattern_engine.engine import match_sequence
|
|
5
5
|
|
|
6
|
+
|
|
6
7
|
def test_filter_no_key():
|
|
7
8
|
node = ast.parse("1").body[0].value
|
|
8
9
|
# Matches, no key bound
|
|
@@ -17,14 +18,14 @@ def test_filter_no_key():
|
|
|
17
18
|
def test_filter_with_key_and_conflicts():
|
|
18
19
|
node = ast.parse("1").body[0].value
|
|
19
20
|
pattern = Filter(lambda x: isinstance(x, ast.Constant), key="my_filter")
|
|
20
|
-
|
|
21
|
+
|
|
21
22
|
# Matches and binds
|
|
22
23
|
res = pattern.match_node(node, {})
|
|
23
24
|
assert res == {"my_filter": node}
|
|
24
|
-
|
|
25
|
+
|
|
25
26
|
# Conflict on duplicate key without force
|
|
26
27
|
assert pattern.match_node(node, {"my_filter": "existing"}) is None
|
|
27
|
-
|
|
28
|
+
|
|
28
29
|
# When inside a Repetition, ancestor forces list
|
|
29
30
|
rep_pattern = Repetition(pattern)
|
|
30
31
|
# Match node directly against the filter while simulating the Repetition context
|
|
@@ -3,6 +3,7 @@ from ast_pattern_engine.nodes.basic import Collect, WildCard, NodePattern
|
|
|
3
3
|
from ast_pattern_engine.nodes.sequences import OneOf
|
|
4
4
|
from ast_pattern_engine.engine import _match_patterns, match_sequence
|
|
5
5
|
|
|
6
|
+
|
|
6
7
|
def test_one_of_non_strict_returns_first_match():
|
|
7
8
|
nodes = [ast.parse(src).body[0] for src in ("1", "2")]
|
|
8
9
|
pattern = [
|
|
@@ -49,9 +50,9 @@ def test_one_of_strict_matches_exactly_one_pattern():
|
|
|
49
50
|
]
|
|
50
51
|
|
|
51
52
|
result = _match_patterns(pattern, nodes, 0, {})
|
|
52
|
-
assert (
|
|
53
|
-
|
|
54
|
-
)
|
|
53
|
+
assert len(result) == 0, (
|
|
54
|
+
"Expected strict OneOf not to match because multiple sub patterns match"
|
|
55
|
+
)
|
|
55
56
|
|
|
56
57
|
# Section B: Test strict mode with exactly one matching pattern for each line
|
|
57
58
|
nodes = [ast.parse(src).body[0] for src in ("1", "x=2")]
|
|
@@ -3,6 +3,7 @@ from ast_pattern_engine.nodes.basic import Collect, WildCard, NodePattern
|
|
|
3
3
|
from ast_pattern_engine.nodes.sequences import PatternGroup
|
|
4
4
|
from ast_pattern_engine.engine import _match_patterns, match_sequence
|
|
5
5
|
|
|
6
|
+
|
|
6
7
|
def test_pattern_group_collects_inner_bindings_under_key():
|
|
7
8
|
nodes = [ast.parse(src).body[0].value for src in ("1", "2")] # type: ignore
|
|
8
9
|
pattern = [
|
|
@@ -3,6 +3,7 @@ from ast_pattern_engine.nodes.basic import Collect, WildCard
|
|
|
3
3
|
from ast_pattern_engine.nodes.sequences import Repetition
|
|
4
4
|
from ast_pattern_engine.engine import _match_patterns
|
|
5
5
|
|
|
6
|
+
|
|
6
7
|
def test_collect_inside_one_or_more_accumulates_nodes():
|
|
7
8
|
nodes = [ast.parse(str(i)).body[0].value for i in range(3)] # type: ignore
|
|
8
9
|
pattern = [Repetition(Collect(WildCard(), "item"))]
|
|
@@ -2,6 +2,7 @@ import ast
|
|
|
2
2
|
from ast_pattern_engine.nodes.basic import Collect, WildCard
|
|
3
3
|
from ast_pattern_engine.engine import match_sequence
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
def test_match_sequence_returns_non_overlapping_bindings():
|
|
6
7
|
nodes = [ast.parse(text).body[0] for text in ("a = 1", "b = 2", "c = 3")]
|
|
7
8
|
pattern = [Collect(WildCard(), "assign")]
|
|
@@ -14,7 +14,7 @@ def _constant(value, template):
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def test_bottom_up_pattern_transformer_collapses_children_before_parent():
|
|
17
|
-
source = "def foo():\n
|
|
17
|
+
source = "def foo():\n return (1 + 2) + (3 + 4)\n"
|
|
18
18
|
tree = ast.parse(source)
|
|
19
19
|
pattern = [Collect(NodePattern(ast.BinOp), "expr")]
|
|
20
20
|
|
|
@@ -60,6 +60,7 @@ def test_bu_transformer_list_manipulation():
|
|
|
60
60
|
# Return multiple nodes (expands)
|
|
61
61
|
def expand(_):
|
|
62
62
|
return [_parse_stmt("pass"), _parse_stmt("pass")]
|
|
63
|
+
|
|
63
64
|
transformer3 = BottomUpPatternTransformer(pattern, {"a": expand})
|
|
64
65
|
res3 = transformer3.visit(ast.parse("x = 1"))
|
|
65
66
|
assert len(res3.body) == 2
|
|
@@ -17,7 +17,10 @@ def test_pattern_finder_collects_node_matches():
|
|
|
17
17
|
def test_pattern_finder_scan_list():
|
|
18
18
|
tree = ast.parse("a = 1\nb = 2\nc = 3")
|
|
19
19
|
# A sequence of length 2 to trigger `_scan_list` len(self.pattern) > 1 branch
|
|
20
|
-
pattern = [
|
|
20
|
+
pattern = [
|
|
21
|
+
Collect(NodePattern(ast.Assign), "a"),
|
|
22
|
+
Collect(NodePattern(ast.Assign), "b"),
|
|
23
|
+
]
|
|
21
24
|
finder = PatternFinder(pattern)
|
|
22
25
|
finder.visit(tree)
|
|
23
26
|
# Print the matches to debug
|
{ast_pattern_engine-1.0.0 → ast_pattern_engine-1.0.2}/tests/visitors/test_pattern_transformer.py
RENAMED
|
@@ -123,7 +123,7 @@ def test_pt_nonlist_replace_errors():
|
|
|
123
123
|
|
|
124
124
|
# Error: handler returns non-list
|
|
125
125
|
def bad_handler(_):
|
|
126
|
-
return _constant_val(2)
|
|
126
|
+
return _constant_val(2) # type: ignore
|
|
127
127
|
|
|
128
128
|
transformer2 = PatternTransformer(pattern, {"c": bad_handler})
|
|
129
129
|
with pytest.raises(TypeError, match="Handler must return list"):
|
|
@@ -133,7 +133,7 @@ def test_pt_nonlist_replace_errors():
|
|
|
133
133
|
def test_pt_plan_errors():
|
|
134
134
|
tree = ast.parse("a = 1\nb = 2")
|
|
135
135
|
pattern = [Collect(NodePattern(ast.Assign), "a")]
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
# key not in bindings (should silently continue)
|
|
138
138
|
transformer1 = PatternTransformer(pattern, {"missing_key": lambda b: []})
|
|
139
139
|
transformer1.visit(ast.parse("a = 1"))
|
|
@@ -142,11 +142,12 @@ def test_pt_plan_errors():
|
|
|
142
142
|
transformer2 = PatternTransformer(pattern, {"a": lambda b: []})
|
|
143
143
|
# override matching to return empty list for testing
|
|
144
144
|
b = {"a": []}
|
|
145
|
-
transformer2.matches.append(b)
|
|
146
|
-
|
|
145
|
+
transformer2.matches.append(b)
|
|
146
|
+
|
|
147
147
|
# handler returns non-list
|
|
148
148
|
def bad_plan_handler(_):
|
|
149
149
|
return "not a list"
|
|
150
|
+
|
|
150
151
|
transformer3 = PatternTransformer(pattern, {"a": bad_plan_handler})
|
|
151
152
|
with pytest.raises(TypeError, match="must return `list"):
|
|
152
153
|
transformer3.visit(ast.parse("a = 1"))
|
|
@@ -166,6 +167,7 @@ def test_pt_nested_replace_and_delete():
|
|
|
166
167
|
# Replace nested nodes
|
|
167
168
|
def replace_with_99(_):
|
|
168
169
|
return [_constant_val(99)]
|
|
170
|
+
|
|
169
171
|
transformer2 = PatternTransformer(pattern, {"c": replace_with_99})
|
|
170
172
|
res2 = transformer2.visit(ast.parse("x = [1, 2, 3]"))
|
|
171
173
|
assert all(elt.value == 99 for elt in res2.body[0].value.elts)
|
|
@@ -174,18 +176,20 @@ def test_pt_nested_replace_and_delete():
|
|
|
174
176
|
def test_pt_dict_as_nodes():
|
|
175
177
|
tree = ast.parse("x = 1")
|
|
176
178
|
pattern = [Collect(NodePattern(ast.Assign), "a")]
|
|
177
|
-
|
|
179
|
+
|
|
178
180
|
# We force the binding to be a dict to trigger dict handling in _as_nodes
|
|
179
181
|
class DictTransformer(PatternTransformer):
|
|
180
182
|
def _plan(self, seq):
|
|
181
183
|
# Intercept and mutate bindings
|
|
182
184
|
res = super()._plan(seq)
|
|
183
185
|
return res
|
|
184
|
-
|
|
186
|
+
|
|
185
187
|
transformer = DictTransformer(pattern, {"a": lambda b: [_parse_stmt("x = 2")]})
|
|
186
188
|
# Override match manually
|
|
187
|
-
mtch = transformer._match_patterns = lambda p, s, i, b:
|
|
188
|
-
|
|
189
|
+
mtch = transformer._match_patterns = lambda p, s, i, b: (
|
|
190
|
+
[({"a": {"nested": s[i]}}, i + 1)] if i < len(s) else []
|
|
191
|
+
)
|
|
192
|
+
|
|
189
193
|
res = transformer.visit(tree)
|
|
190
194
|
assert isinstance(res.body[0], ast.Assign)
|
|
191
195
|
assert res.body[0].value.value == 2
|
|
@@ -195,14 +199,14 @@ def test_pt_generic_visit_list_field_error():
|
|
|
195
199
|
tree = ast.parse("a = 1")
|
|
196
200
|
pattern = [Collect(NodePattern(ast.Assign), "a")]
|
|
197
201
|
transformer = PatternTransformer(pattern, {})
|
|
198
|
-
|
|
202
|
+
|
|
199
203
|
# Force a child to not be an AST node
|
|
200
204
|
class BadTransformer(PatternTransformer):
|
|
201
205
|
def visit(self, node):
|
|
202
206
|
if isinstance(node, ast.Assign):
|
|
203
207
|
return "Not an AST node"
|
|
204
208
|
return super().visit(node)
|
|
205
|
-
|
|
209
|
+
|
|
206
210
|
bad_transformer = BadTransformer(pattern, {})
|
|
207
211
|
with pytest.raises(TypeError, match="must contain AST nodes"):
|
|
208
212
|
bad_transformer.visit(ast.parse("a = 1"))
|
|
@@ -29,8 +29,8 @@ def test_single_occurrence_finder_early_exit():
|
|
|
29
29
|
tree = ast.parse("a = 1\nb = 2")
|
|
30
30
|
pattern = [NodePattern(ast.Assign)]
|
|
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
|
-
assert finder.match_node is None
|
|
36
|
+
assert finder.match_node is None # Didn't actually match because it early exited
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [ "main" ]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [ "main" ]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
test:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
strategy:
|
|
13
|
-
matrix:
|
|
14
|
-
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
15
|
-
|
|
16
|
-
steps:
|
|
17
|
-
- uses: actions/checkout@v4
|
|
18
|
-
|
|
19
|
-
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
-
uses: actions/setup-python@v5
|
|
21
|
-
with:
|
|
22
|
-
python-version: ${{ matrix.python-version }}
|
|
23
|
-
|
|
24
|
-
- name: Install uv
|
|
25
|
-
uses: astral-sh/setup-uv@v5
|
|
26
|
-
with:
|
|
27
|
-
enable-cache: true
|
|
28
|
-
|
|
29
|
-
- name: Install dependencies
|
|
30
|
-
run: uv sync --all-extras
|
|
31
|
-
|
|
32
|
-
- name: Check formatting with Ruff
|
|
33
|
-
run: uv run ruff format --check
|
|
34
|
-
|
|
35
|
-
- name: Lint with Ruff
|
|
36
|
-
run: uv run ruff check
|
|
37
|
-
|
|
38
|
-
- name: Run tests with pytest
|
|
39
|
-
run: uv run pytest --cov=src --cov-report=xml
|
|
40
|
-
|
|
41
|
-
- name: Upload coverage reports to Codecov
|
|
42
|
-
uses: codecov/codecov-action@v4
|
|
43
|
-
env:
|
|
44
|
-
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ast_pattern_engine-1.0.0 → 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
|