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.
Files changed (43) hide show
  1. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/.github/workflows/ci.yml +86 -69
  2. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/PKG-INFO +8 -1
  3. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/README.md +7 -0
  4. ast_pattern_engine-1.0.2/docs/guide.md +57 -0
  5. ast_pattern_engine-1.0.2/docs/index.md +46 -0
  6. ast_pattern_engine-1.0.2/docs/reference/core.md +11 -0
  7. ast_pattern_engine-1.0.2/docs/reference/nodes.md +19 -0
  8. ast_pattern_engine-1.0.2/docs/reference/templates.md +7 -0
  9. ast_pattern_engine-1.0.2/docs/reference/visitors.md +9 -0
  10. ast_pattern_engine-1.0.2/docs/stylesheets/extra.css +26 -0
  11. ast_pattern_engine-1.0.2/mkdocs.yaml +50 -0
  12. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/pyproject.toml +6 -4
  13. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/nodes/basic.py +52 -10
  14. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/nodes/sequences.py +27 -21
  15. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/visitors.py +20 -6
  16. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/visitors/test_single_occurrence_finder.py +1 -1
  17. ast_pattern_engine-1.0.1/src/ast_pattern_engine/plumbing.py +0 -2
  18. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/.gitignore +0 -0
  19. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/LICENSE +0 -0
  20. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/examples/dict_get_rewrite.py +0 -0
  21. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/__init__.py +0 -0
  22. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/core.py +0 -0
  23. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/engine.py +0 -0
  24. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/nodes/__init__.py +0 -0
  25. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/py.typed +0 -0
  26. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/src/ast_pattern_engine/templates.py +0 -0
  27. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/__init__.py +0 -0
  28. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_all_of.py +0 -0
  29. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_any_of.py +0 -0
  30. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_bind.py +0 -0
  31. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_collect.py +0 -0
  32. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_contains.py +0 -0
  33. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_filter.py +0 -0
  34. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_not.py +0 -0
  35. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_one_of.py +0 -0
  36. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_optional.py +0 -0
  37. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_pattern_group.py +0 -0
  38. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_repetition.py +0 -0
  39. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/patterns/test_templates.py +0 -0
  40. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/test_engine.py +0 -0
  41. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/visitors/test_bottom_up_pattern_transformer.py +0 -0
  42. {ast_pattern_engine-1.0.1 → ast_pattern_engine-1.0.2}/tests/visitors/test_pattern_finder.py +0 -0
  43. {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: Auto-fix lint with Ruff
27
- run: uv run ruff check --fix
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: Lint with Ruff
61
- run: uv run ruff check
62
-
63
- - name: Run tests with pytest
64
- run: uv run pytest --cov=src --cov-report=xml
65
-
66
- - name: Upload coverage reports to Codecov
67
- uses: codecov/codecov-action@v4
68
- env:
69
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
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.1
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
+ ![PyPI - Version](https://img.shields.io/pypi/v/ast-pattern-engine)
25
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ast-pattern-engine)
26
+ ![Pytest](https://img.shields.io/badge/pytest-tested-orange)
27
+ ![MIT License](https://img.shields.io/badge/License-MIT-blue)
28
+ ![Codecov](https://codecov.io/gh/80sVectorz/ast_pattern_engine/branch/main/graph/badge.svg)
29
+ ![GitHub Workflow Status](https://github.com/80sVectorz/ast_pattern_engine/actions/workflows/ci.yml/badge.svg)
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
+ ![PyPI - Version](https://img.shields.io/pypi/v/ast-pattern-engine)
2
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ast-pattern-engine)
3
+ ![Pytest](https://img.shields.io/badge/pytest-tested-orange)
4
+ ![MIT License](https://img.shields.io/badge/License-MIT-blue)
5
+ ![Codecov](https://codecov.io/gh/80sVectorz/ast_pattern_engine/branch/main/graph/badge.svg)
6
+ ![GitHub Workflow Status](https://github.com/80sVectorz/ast_pattern_engine/actions/workflows/ci.yml/badge.svg)
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,11 @@
1
+ # Core & Engine
2
+
3
+ This page documents the core base classes and the engine used to match sequences.
4
+
5
+ ::: ast_pattern_engine.core
6
+ options:
7
+ show_root_heading: true
8
+
9
+ ::: ast_pattern_engine.engine
10
+ options:
11
+ show_root_heading: true
@@ -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,7 @@
1
+ ## Templates
2
+
3
+ Templates provide convenient helper functions for generating common patterns.
4
+
5
+ ::: ast_pattern_engine.templates
6
+ options:
7
+ show_root_heading: true
@@ -0,0 +1,9 @@
1
+ # AST Visitors
2
+
3
+ Visitors use the pattern matching engine to find or transform abstract syntax trees.
4
+
5
+ ## Finders and Transformers
6
+
7
+ ::: ast_pattern_engine.visitors
8
+ options:
9
+ 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.1"
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
- [tool.ruff.lint]
47
- ignore = ["F841"] # Ignore unused variable assignments
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 `name`.
14
+ """Bind the current node to `key`.
15
15
 
16
16
  This is syntactic sugar for:
17
- >>> Collect(WildCard(), "x")
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(
@@ -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 AST pattern.
46
- min_matches: Min number of matches required. Default is 1.
47
- max_matches: Max number of allowed matches. Defaults to None.
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
- pattern: Sequence[Pattern]
58
- """The compound pattern to match."""
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
- pattern: Pattern
107
- """The pattern to match."""
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.found = False
514
+ self._found_match = False
501
515
 
502
516
  def visit(self, node: ast.AST):
503
- if self.found:
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.found = True
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.found:
533
+ if self._found_match:
520
534
  return
521
535
  elif isinstance(val, ast.AST):
522
536
  self.visit(val)
523
- if self.found:
537
+ if self._found_match:
524
538
  return
525
539
 
526
540
  def found_match(self) -> bool:
527
- return self.found
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.found = True
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
@@ -1,2 +0,0 @@
1
- # This module previously contained the Message/AnnounceBinding plumbing.
2
- # That system has been replaced by stateless _force_list context passing.