astToolkit 0.7.0__tar.gz → 0.7.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.
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/PKG-INFO +3 -3
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/README.md +1 -1
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolkitAST.py +6 -10
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/transformationTools.py +97 -57
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit.egg-info/PKG-INFO +3 -3
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit.egg-info/SOURCES.txt +0 -3
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit.egg-info/requires.txt +1 -1
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/pyproject.toml +2 -2
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/tests/conftest.py +0 -7
- asttoolkit-0.7.0/tests/test_Find.py +0 -15
- asttoolkit-0.7.0/tests/test_joinClassmethod.py +0 -103
- asttoolkit-0.7.0/tests/test_nodeVisitors.py +0 -377
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/LICENSE +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/__init__.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_astTypes.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_dumpFunctionDef.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_dumpHandmade.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_prototypeFind.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_theSSOT.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolBe.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolDOT.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolGrab.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolIfThis.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolMake.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolThen.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolkitContainers.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolkitNodeVisitor.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/py.typed +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit.egg-info/dependency_links.txt +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit.egg-info/top_level.txt +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/setup.cfg +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/tests/__init__.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/tests/dataSamples/Make.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/tests/dataSamples/__init__.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/tests/dataSamples/createTestData.py +0 -0
- {asttoolkit-0.7.0 → asttoolkit-0.7.2}/tests/test_Be.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: astToolkit
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: A powerfully composable, type-safe toolkit for Python abstract syntax tree (AST) manipulation, analysis, transformation, and code generation with a layered architecture designed for building sophisticated code processing assembly-lines.
|
|
5
5
|
Author-email: Hunter Hogan <HunterHogan@pm.me>
|
|
6
6
|
License: CC-BY-NC-4.0
|
|
@@ -28,7 +28,7 @@ Requires-Python: >=3.12
|
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
License-File: LICENSE
|
|
30
30
|
Requires-Dist: autoflake
|
|
31
|
-
Requires-Dist: hunterMakesPy
|
|
31
|
+
Requires-Dist: hunterMakesPy>=0.2.1
|
|
32
32
|
Requires-Dist: typing_extensions>=4.10.0
|
|
33
33
|
Provides-Extra: development
|
|
34
34
|
Requires-Dist: mypy; extra == "development"
|
|
@@ -354,4 +354,4 @@ Coding One Step at a Time:
|
|
|
354
354
|
2. Write good code.
|
|
355
355
|
3. When revising, write better code.
|
|
356
356
|
|
|
357
|
-
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
|
@@ -312,4 +312,4 @@ Coding One Step at a Time:
|
|
|
312
312
|
2. Write good code.
|
|
313
313
|
3. When revising, write better code.
|
|
314
314
|
|
|
315
|
-
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
|
@@ -71,23 +71,19 @@ def extractClassDef(astAST: ast.AST, identifier: str) -> ast.ClassDef | None:
|
|
|
71
71
|
|
|
72
72
|
def extractFunctionDef(astAST: ast.AST, identifier: str) -> ast.FunctionDef | None:
|
|
73
73
|
"""
|
|
74
|
-
Extract
|
|
75
|
-
|
|
76
|
-
(AI generated docstring)
|
|
77
|
-
|
|
78
|
-
This function searches through an AST module for a function definition that matches the provided identifier and returns it if found.
|
|
74
|
+
Extract the function from `astAST` with `ast.FunctionDef.name == identifier`.
|
|
79
75
|
|
|
80
76
|
Parameters
|
|
81
77
|
----------
|
|
82
78
|
astAST : ast.AST
|
|
83
|
-
|
|
79
|
+
(abstract syntax tree) An `ast.AST` subclass from which to extract the function.
|
|
84
80
|
identifier : str
|
|
85
|
-
The
|
|
81
|
+
The identifier in `ast.FunctionDef.name`.
|
|
86
82
|
|
|
87
83
|
Returns
|
|
88
84
|
-------
|
|
89
|
-
astFunctionDef | None
|
|
90
|
-
The
|
|
85
|
+
astFunctionDef : ast.FunctionDef | None
|
|
86
|
+
The target function, or `None` if `extractFunctionDef` does not find `ast.FunctionDef.name == identifier`.
|
|
91
87
|
|
|
92
88
|
"""
|
|
93
89
|
return NodeTourist(IfThis.isFunctionDefIdentifier(identifier), Then.extractIt).captureLastMatch(astAST)
|
|
@@ -99,7 +95,7 @@ def parseLogicalPath2astModule(logicalPath: identifierDotAttribute, package: str
|
|
|
99
95
|
(AI generated docstring)
|
|
100
96
|
|
|
101
97
|
This function imports a module using its logical path (e.g., 'scipy.signal.windows') and converts its source code into an
|
|
102
|
-
`ast.Module` (abstract syntax tree) object
|
|
98
|
+
`ast.Module` (abstract syntax tree) `object`. Supports all relevant `ast.parse` parameters.
|
|
103
99
|
|
|
104
100
|
Parameters
|
|
105
101
|
----------
|
|
@@ -35,6 +35,7 @@ from os import PathLike
|
|
|
35
35
|
from pathlib import PurePath
|
|
36
36
|
from typing import Any
|
|
37
37
|
import ast
|
|
38
|
+
import io
|
|
38
39
|
|
|
39
40
|
def makeDictionaryAsyncFunctionDef(astAST: ast.AST) -> dict[str, ast.AsyncFunctionDef]:
|
|
40
41
|
"""
|
|
@@ -127,48 +128,60 @@ def makeDictionaryMosDef(astAST: ast.AST) -> dict[str, ast.AsyncFunctionDef | as
|
|
|
127
128
|
dictionaryIdentifier2MosDef.update(makeDictionaryFunctionDef(astAST))
|
|
128
129
|
return dictionaryIdentifier2MosDef
|
|
129
130
|
|
|
130
|
-
def inlineFunctionDef(identifierToInline: str,
|
|
131
|
+
def inlineFunctionDef(identifierToInline: str, astModule: ast.Module) -> ast.FunctionDef:
|
|
131
132
|
"""
|
|
132
|
-
|
|
133
|
+
Synthesize a new function from `identifierToInline` with the logic "inlined" from functions called by `identifierToInline`.
|
|
133
134
|
|
|
134
|
-
|
|
135
|
+
`inlineFunctionDef` starts with the `ast.FunctionDef` in `astModule` whose `ast.FunctionDef.name == identifierToInline`, then
|
|
136
|
+
`inlineFunctionDef` searches through `identifierToInline` for calls to functions defined in `astModule`.
|
|
137
|
+
|
|
138
|
+
More specifically, `inlineFunctionDef` searches for an `ast.Call` to an `ast.Name` (*e.g.*, `Path`, but not the
|
|
139
|
+
`ast.Attribute`, `pathlib.Path`). The `ast.Name` identifier is in `ast.Name.id`, for example, `ast.Name.id = "Path"`.
|
|
140
|
+
`inlineFunctionDef` then searches in `astModule` for an `ast.FunctionDef.name` that is the same as the `ast.Name.id`. If
|
|
141
|
+
`inlineFunctionDef` finds a matching `ast.FunctionDef`, it will replace `ast.Call` with the logic from the called function.
|
|
142
|
+
|
|
143
|
+
`inlineFunctionDef` repeats the process until it gets bored or until it cannot find any more functions to inline. Therefore,
|
|
144
|
+
`inlineFunctionDef` will inline functions that were not called directly by `identifierToInline` in the original `astModule`.
|
|
135
145
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
implementation bodies. This produces a fully inlined function that doesn't depend
|
|
139
|
-
on other function definitions from the module.
|
|
146
|
+
`inlineFunctionDef`, however, does not inline an `ast.FunctionDef` if the `ast.FunctionDef` calls itself or if a "child" of
|
|
147
|
+
the `ast.FunctionDef` calls the original `ast.FunctionDef`.
|
|
140
148
|
|
|
141
149
|
Parameters
|
|
142
150
|
----------
|
|
143
151
|
identifierToInline : str
|
|
144
|
-
The
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
The identifier of the target function; the `str` must match an `ast.FunctionDef.name` in `astModule`.
|
|
153
|
+
astModule : ast.Module
|
|
154
|
+
(abstract syntax tree Module) An `ast.Module` with `ast.FunctionDef.name == identifierToInline` and zero or more other
|
|
155
|
+
`ast.FunctionDef` to inline.
|
|
147
156
|
|
|
148
157
|
Returns
|
|
149
158
|
-------
|
|
150
159
|
FunctionDefToInline : ast.FunctionDef
|
|
151
|
-
The
|
|
160
|
+
The synthesized function with inlined logic from other functions defined in `astModule`.
|
|
152
161
|
|
|
153
162
|
Raises
|
|
154
163
|
------
|
|
155
|
-
ValueError: If
|
|
164
|
+
ValueError: If `identifierToInline` does not match any `ast.FunctionDef.name` in `astModule`.
|
|
156
165
|
"""
|
|
157
|
-
dictionaryFunctionDef: dict[str, ast.FunctionDef] = makeDictionaryFunctionDef(
|
|
166
|
+
dictionaryFunctionDef: dict[str, ast.FunctionDef] = makeDictionaryFunctionDef(astModule)
|
|
158
167
|
try:
|
|
159
168
|
FunctionDefToInline = dictionaryFunctionDef[identifierToInline]
|
|
160
169
|
except KeyError as ERRORmessage:
|
|
161
|
-
message = f"
|
|
170
|
+
message = f"I was unable to find an `ast.FunctionDef` with name {identifierToInline = } in {astModule = }."
|
|
162
171
|
raise ValueError(message) from ERRORmessage
|
|
163
172
|
|
|
164
173
|
listIdentifiersCalledFunctions: list[str] = []
|
|
174
|
+
# TODO I probably have the skill now to expand this from only `IfThis.isCallToName` to include more options for `ast.Call.func`,
|
|
175
|
+
# such as attribute and subscript access. `IfThis.isNestedNameIdentifier` might be perfect. BUT, I think this will affect how I
|
|
176
|
+
# key `dictionaryFunctionDef`.
|
|
165
177
|
findIdentifiersToInline = NodeTourist[ast.Call, ast.expr](IfThis.isCallToName
|
|
166
178
|
, Grab.funcAttribute(Grab.idAttribute(Then.appendTo(listIdentifiersCalledFunctions))))
|
|
167
179
|
findIdentifiersToInline.visit(FunctionDefToInline)
|
|
168
180
|
|
|
169
181
|
dictionary4Inlining: dict[str, ast.FunctionDef] = {}
|
|
170
182
|
for identifier in sorted(set(listIdentifiersCalledFunctions).intersection(dictionaryFunctionDef.keys())):
|
|
171
|
-
|
|
183
|
+
# TODO Learn how real programmers avoid infinite loops but still inline recursive functions.
|
|
184
|
+
if NodeTourist(IfThis.matchesMeButNotAnyDescendant(IfThis.isCallIdentifier(identifier)), Then.extractIt).captureLastMatch(astModule) is not None:
|
|
172
185
|
dictionary4Inlining[identifier] = dictionaryFunctionDef[identifier]
|
|
173
186
|
|
|
174
187
|
keepGoing = True
|
|
@@ -181,7 +194,7 @@ def inlineFunctionDef(identifierToInline: str, module: ast.Module) -> ast.Functi
|
|
|
181
194
|
if len(listIdentifiersCalledFunctions) > 0:
|
|
182
195
|
keepGoing = True
|
|
183
196
|
for identifier in listIdentifiersCalledFunctions:
|
|
184
|
-
if NodeTourist(IfThis.matchesMeButNotAnyDescendant(IfThis.isCallIdentifier(identifier)), Then.extractIt).captureLastMatch(
|
|
197
|
+
if NodeTourist(IfThis.matchesMeButNotAnyDescendant(IfThis.isCallIdentifier(identifier)), Then.extractIt).captureLastMatch(astModule) is not None:
|
|
185
198
|
FunctionDefTarget = dictionaryFunctionDef[identifier]
|
|
186
199
|
if len(FunctionDefTarget.body) == 1:
|
|
187
200
|
replacement = NodeTourist(Be.Return, Then.extractIt(DOT.value)).captureLastMatch(FunctionDefTarget)
|
|
@@ -205,6 +218,32 @@ def inlineFunctionDef(identifierToInline: str, module: ast.Module) -> ast.Functi
|
|
|
205
218
|
ast.fix_missing_locations(FunctionDefToInline)
|
|
206
219
|
return FunctionDefToInline
|
|
207
220
|
|
|
221
|
+
def pythonCode2ast_expr(string: str) -> ast.expr:
|
|
222
|
+
"""PROTOTYPE Convert *one* expression-as-a-string of Python code to an `ast.expr`.
|
|
223
|
+
|
|
224
|
+
TODO add list of applicable subclasses. Until then, see `Make.expr` for an approximate list.
|
|
225
|
+
|
|
226
|
+
Note well
|
|
227
|
+
---------
|
|
228
|
+
This prototype "shortcut" function has approximately 482 *implied* constraints and pitfalls. If you can't get it to do what
|
|
229
|
+
you want, I recommend saving yourself a bunch of stress and not using this shortcut.
|
|
230
|
+
"""
|
|
231
|
+
return raiseIfNone(NodeTourist(Be.Expr, Then.extractIt(DOT.value)).captureLastMatch(ast.parse(string)))
|
|
232
|
+
|
|
233
|
+
def pythonCode2ast_stmt(string: str):
|
|
234
|
+
"""PROTOTYPE Convert *one* statement-as-a-string of Python code to an `ast.stmt`.
|
|
235
|
+
|
|
236
|
+
TODO add list of applicable subclasses. Until then, see `Make.stmt` for an approximate list.
|
|
237
|
+
|
|
238
|
+
Note well
|
|
239
|
+
---------
|
|
240
|
+
This prototype "shortcut" function has approximately eleventy gazillion *implied* constraints and pitfalls. If you can't get
|
|
241
|
+
it to do what you want, I recommend saving yourself a bunch of stress and not using this shortcut.
|
|
242
|
+
"""
|
|
243
|
+
astModule: ast.Module = ast.parse(string)
|
|
244
|
+
findThis = Be.Module
|
|
245
|
+
doThat = "not today; I can't think through the pain."
|
|
246
|
+
|
|
208
247
|
def removeUnusedParameters(ingredientsFunction: IngredientsFunction) -> IngredientsFunction:
|
|
209
248
|
"""
|
|
210
249
|
Remove unused parameters from `IngredientsFunction.astFunctionDef`, but not from `import` statements.
|
|
@@ -255,6 +294,46 @@ def removeUnusedParameters(ingredientsFunction: IngredientsFunction) -> Ingredie
|
|
|
255
294
|
|
|
256
295
|
return ingredientsFunction
|
|
257
296
|
|
|
297
|
+
def unjoinBinOP(astAST: ast.AST, operator: type[ast.operator] = ast.operator) -> list[ast.expr]:
|
|
298
|
+
"""
|
|
299
|
+
Unjoin a binary operation AST node into a list of expressions.
|
|
300
|
+
|
|
301
|
+
(AI generated docstring)
|
|
302
|
+
|
|
303
|
+
This function takes an AST node representing a binary operation and recursively
|
|
304
|
+
unjoins it into a flat list of expressions. It handles both binary operations
|
|
305
|
+
and unary operations, ensuring that all nested expressions are extracted.
|
|
306
|
+
|
|
307
|
+
Parameters
|
|
308
|
+
----------
|
|
309
|
+
astAST : ast.AST
|
|
310
|
+
The AST node to unjoin.
|
|
311
|
+
operator : type[ast.operator] = ast.operator
|
|
312
|
+
The type of binary operator to look for in the AST. Defaults to `ast.operator`.
|
|
313
|
+
|
|
314
|
+
Returns
|
|
315
|
+
-------
|
|
316
|
+
list[ast.expr]
|
|
317
|
+
A list of expressions extracted from the binary operation AST node.
|
|
318
|
+
"""
|
|
319
|
+
list_ast_expr: list[ast.expr] = []
|
|
320
|
+
workbench: list[ast.expr] = []
|
|
321
|
+
|
|
322
|
+
findThis = Be.BinOp.opIs(lambda this_op: isinstance(this_op, operator))
|
|
323
|
+
doThat = Grab.andDoAllOf([Grab.leftAttribute(Then.appendTo(workbench)), Grab.rightAttribute(Then.appendTo(list_ast_expr))])
|
|
324
|
+
breakingBinOp = NodeTourist(findThis, doThat)
|
|
325
|
+
|
|
326
|
+
breakingBinOp.visit(astAST)
|
|
327
|
+
|
|
328
|
+
while workbench:
|
|
329
|
+
ast_expr = workbench.pop()
|
|
330
|
+
if isinstance(ast_expr, ast.BinOp):
|
|
331
|
+
breakingBinOp.visit(ast_expr)
|
|
332
|
+
else:
|
|
333
|
+
list_ast_expr.append(ast_expr)
|
|
334
|
+
|
|
335
|
+
return list_ast_expr
|
|
336
|
+
|
|
258
337
|
def unparseFindReplace(astTree: 木, mappingFindReplaceNodes: Mapping[ast.AST, ast.AST]) -> 木:
|
|
259
338
|
"""
|
|
260
339
|
Recursively replace AST (Abstract Syntax Tree) nodes based on a mapping of find-replace pairs.
|
|
@@ -295,7 +374,7 @@ def unparseFindReplace(astTree: 木, mappingFindReplaceNodes: Mapping[ast.AST, a
|
|
|
295
374
|
astTree = deepcopy(newTree)
|
|
296
375
|
return newTree
|
|
297
376
|
|
|
298
|
-
def write_astModule(ingredients: IngredientsModule, pathFilename: PathLike[Any] | PurePath, packageName: str | None = None) -> None:
|
|
377
|
+
def write_astModule(ingredients: IngredientsModule, pathFilename: PathLike[Any] | PurePath | io.TextIOBase, packageName: str | None = None) -> None:
|
|
299
378
|
"""
|
|
300
379
|
Convert an IngredientsModule to Python source code and write it to a file.
|
|
301
380
|
|
|
@@ -326,49 +405,10 @@ def write_astModule(ingredients: IngredientsModule, pathFilename: PathLike[Any]
|
|
|
326
405
|
"""
|
|
327
406
|
astModule = Make.Module(ingredients.body, ingredients.type_ignores)
|
|
328
407
|
ast.fix_missing_locations(astModule)
|
|
329
|
-
pythonSource: str =
|
|
408
|
+
pythonSource: str = ast.unparse(astModule)
|
|
330
409
|
autoflake_additional_imports: list[str] = ingredients.imports.exportListModuleIdentifiers()
|
|
331
410
|
if packageName:
|
|
332
411
|
autoflake_additional_imports.append(packageName)
|
|
333
412
|
pythonSource = autoflake_fix_code(pythonSource, autoflake_additional_imports, expand_star_imports=False, remove_all_unused_imports=True, remove_duplicate_keys = False, remove_unused_variables = False)
|
|
334
413
|
writeStringToHere(pythonSource, pathFilename)
|
|
335
414
|
|
|
336
|
-
def unjoinBinOP(astAST: ast.AST, operator: type[ast.operator] = ast.operator) -> list[ast.expr]:
|
|
337
|
-
"""
|
|
338
|
-
Unjoin a binary operation AST node into a list of expressions.
|
|
339
|
-
|
|
340
|
-
(AI generated docstring)
|
|
341
|
-
|
|
342
|
-
This function takes an AST node representing a binary operation and recursively
|
|
343
|
-
unjoins it into a flat list of expressions. It handles both binary operations
|
|
344
|
-
and unary operations, ensuring that all nested expressions are extracted.
|
|
345
|
-
|
|
346
|
-
Parameters
|
|
347
|
-
----------
|
|
348
|
-
astAST : ast.AST
|
|
349
|
-
The AST node to unjoin.
|
|
350
|
-
operator : type[ast.operator] = ast.operator
|
|
351
|
-
The type of binary operator to look for in the AST. Defaults to `ast.operator`.
|
|
352
|
-
|
|
353
|
-
Returns
|
|
354
|
-
-------
|
|
355
|
-
list[ast.expr]
|
|
356
|
-
A list of expressions extracted from the binary operation AST node.
|
|
357
|
-
"""
|
|
358
|
-
list_ast_expr: list[ast.expr] = []
|
|
359
|
-
workbench: list[ast.expr] = []
|
|
360
|
-
|
|
361
|
-
findThis = Be.BinOp.opIs(lambda this_op: isinstance(this_op, operator))
|
|
362
|
-
doThat = Grab.andDoAllOf([Grab.leftAttribute(Then.appendTo(workbench)), Grab.rightAttribute(Then.appendTo(list_ast_expr))])
|
|
363
|
-
breakingBinOp = NodeTourist(findThis, doThat)
|
|
364
|
-
|
|
365
|
-
breakingBinOp.visit(astAST)
|
|
366
|
-
|
|
367
|
-
while workbench:
|
|
368
|
-
ast_expr = workbench.pop()
|
|
369
|
-
if isinstance(ast_expr, ast.BinOp):
|
|
370
|
-
breakingBinOp.visit(ast_expr)
|
|
371
|
-
else:
|
|
372
|
-
list_ast_expr.append(ast_expr)
|
|
373
|
-
|
|
374
|
-
return list_ast_expr
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: astToolkit
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: A powerfully composable, type-safe toolkit for Python abstract syntax tree (AST) manipulation, analysis, transformation, and code generation with a layered architecture designed for building sophisticated code processing assembly-lines.
|
|
5
5
|
Author-email: Hunter Hogan <HunterHogan@pm.me>
|
|
6
6
|
License: CC-BY-NC-4.0
|
|
@@ -28,7 +28,7 @@ Requires-Python: >=3.12
|
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
License-File: LICENSE
|
|
30
30
|
Requires-Dist: autoflake
|
|
31
|
-
Requires-Dist: hunterMakesPy
|
|
31
|
+
Requires-Dist: hunterMakesPy>=0.2.1
|
|
32
32
|
Requires-Dist: typing_extensions>=4.10.0
|
|
33
33
|
Provides-Extra: development
|
|
34
34
|
Requires-Dist: mypy; extra == "development"
|
|
@@ -354,4 +354,4 @@ Coding One Step at a Time:
|
|
|
354
354
|
2. Write good code.
|
|
355
355
|
3. When revising, write better code.
|
|
356
356
|
|
|
357
|
-
[](https://creativecommons.org/licenses/by-nc/4.0/)
|
|
@@ -26,9 +26,6 @@ astToolkit.egg-info/top_level.txt
|
|
|
26
26
|
tests/__init__.py
|
|
27
27
|
tests/conftest.py
|
|
28
28
|
tests/test_Be.py
|
|
29
|
-
tests/test_Find.py
|
|
30
|
-
tests/test_joinClassmethod.py
|
|
31
|
-
tests/test_nodeVisitors.py
|
|
32
29
|
tests/dataSamples/Make.py
|
|
33
30
|
tests/dataSamples/__init__.py
|
|
34
31
|
tests/dataSamples/createTestData.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "astToolkit"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.2"
|
|
4
4
|
description = "A powerfully composable, type-safe toolkit for Python abstract syntax tree (AST) manipulation, analysis, transformation, and code generation with a layered architecture designed for building sophisticated code processing assembly-lines."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -48,7 +48,7 @@ classifiers = [
|
|
|
48
48
|
urls = { Donate = "https://www.patreon.com/integrated", Homepage = "https://github.com/hunterhogan/astToolkit", Issues = "https://github.com/hunterhogan/astToolkit/issues", Repository = "https://github.com/hunterhogan/astToolkit.git" }
|
|
49
49
|
dependencies = [
|
|
50
50
|
"autoflake",
|
|
51
|
-
"hunterMakesPy",
|
|
51
|
+
"hunterMakesPy>=0.2.1",
|
|
52
52
|
"typing_extensions>=4.10.0",
|
|
53
53
|
]
|
|
54
54
|
optional-dependencies = { development = [
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
"""SSOT for all tests."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Iterator
|
|
4
|
-
from datetime import datetime, UTC
|
|
5
4
|
from functools import cache
|
|
6
5
|
from tests.dataSamples.Make import allSubclasses
|
|
7
6
|
from typing import Any
|
|
8
7
|
import ast # pyright: ignore[reportUnusedImport]
|
|
9
8
|
import pytest
|
|
10
9
|
|
|
11
|
-
antiTests: int = 3
|
|
12
|
-
|
|
13
|
-
shiftByHour: int = datetime.now(UTC).hour
|
|
14
|
-
shiftByDate: int = datetime.now(UTC).day
|
|
15
|
-
shiftTotal: int = shiftByHour * shiftByDate + 1
|
|
16
|
-
|
|
17
10
|
def generateBeTestData() -> Iterator[tuple[str, str, dict[str, Any]]]:
|
|
18
11
|
"""Yield test data for positive Be tests. (AI generated docstring).
|
|
19
12
|
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
"""Not every module needs a docstring."""
|
|
2
|
-
from astToolkit import Find
|
|
3
|
-
from tests.conftest import generateBeTestData
|
|
4
|
-
from typing import Any, TYPE_CHECKING
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
if TYPE_CHECKING:
|
|
8
|
-
import ast
|
|
9
|
-
|
|
10
|
-
# @pytest.mark.parametrize("identifierClass, subtestName, dictionaryTests", list(generateBeTestData())) # noqa: PT006
|
|
11
|
-
# def test_Find_identifies_class(identifierClass: str, subtestName: str, dictionaryTests: dict[str, Any]) -> None: # noqa: ARG001
|
|
12
|
-
# """test_Find_identifies_class."""
|
|
13
|
-
# node: ast.AST = dictionaryTests["expression"]
|
|
14
|
-
# ww = eval(f"Find.{identifierClass}")
|
|
15
|
-
# assert ww(node)
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
from astToolkit import ast_attributes, Make
|
|
2
|
-
from collections.abc import Callable
|
|
3
|
-
from typing import cast
|
|
4
|
-
import ast
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
constant_expression_5 = Make.Constant(5)
|
|
8
|
-
constant_expression_7 = Make.Constant(7)
|
|
9
|
-
constant_expression_11 = Make.Constant(11)
|
|
10
|
-
|
|
11
|
-
keyword_arguments_set_A: ast_attributes = {"lineno": 3, "col_offset": 8, "end_lineno": 3, "end_col_offset": 12}
|
|
12
|
-
keyword_arguments_set_B: ast_attributes = {"lineno": 17, "col_offset": 19, "end_lineno": 17, "end_col_offset": 25}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def construct_expected_ast_node(ast_operator_type: type[ast.operator], input_expressions_iterable: list[ast.expr], keyword_arguments_for_operation: ast_attributes) -> ast.expr:
|
|
16
|
-
processed_expressions_list = list(input_expressions_iterable)
|
|
17
|
-
if not processed_expressions_list:
|
|
18
|
-
return Make.Constant('', **keyword_arguments_for_operation)
|
|
19
|
-
|
|
20
|
-
current_joined_expression = processed_expressions_list[0]
|
|
21
|
-
for next_expression_to_join in processed_expressions_list[1:]:
|
|
22
|
-
current_joined_expression = ast.BinOp(
|
|
23
|
-
left=current_joined_expression,
|
|
24
|
-
op=ast_operator_type(),
|
|
25
|
-
right=next_expression_to_join,
|
|
26
|
-
**keyword_arguments_for_operation
|
|
27
|
-
)
|
|
28
|
-
return current_joined_expression
|
|
29
|
-
|
|
30
|
-
listBoolOpIdentifiers: list[str] = sorted([subclass.__name__ for subclass in ast.boolop.__subclasses__()])
|
|
31
|
-
listOperatorIdentifiers: list[str] = sorted([subclass.__name__ for subclass in ast.operator.__subclasses__()])
|
|
32
|
-
|
|
33
|
-
listBoolOpJoinImplementationAndAstPairs = [
|
|
34
|
-
(Make.__dict__[identifier], ast.__dict__[identifier])
|
|
35
|
-
for identifier in listBoolOpIdentifiers
|
|
36
|
-
if identifier in Make.__dict__
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
listOperatorJoinImplementationAndAstPairs = [
|
|
40
|
-
(Make.__dict__[identifier], ast.__dict__[identifier])
|
|
41
|
-
for identifier in listOperatorIdentifiers
|
|
42
|
-
if identifier in Make.__dict__
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
list_join_method_test_scenarios_params: list[object] = []
|
|
46
|
-
raw_scenarios: list[tuple[str, list[ast.expr], ast_attributes]] = [
|
|
47
|
-
("empty_list_no_kwargs", [], {}),
|
|
48
|
-
("empty_list_with_kwargs", [], keyword_arguments_set_A),
|
|
49
|
-
("single_expr_no_kwargs", [constant_expression_5], {}),
|
|
50
|
-
("single_expr_with_kwargs", [constant_expression_5], keyword_arguments_set_A),
|
|
51
|
-
("two_exprs_no_kwargs", [constant_expression_5, constant_expression_7], {}),
|
|
52
|
-
("two_exprs_with_kwargs", [constant_expression_5, constant_expression_7], keyword_arguments_set_A),
|
|
53
|
-
("three_exprs_no_kwargs", [constant_expression_5, constant_expression_7, constant_expression_11], {}),
|
|
54
|
-
("three_exprs_with_kwargs", [constant_expression_5, constant_expression_7, constant_expression_11], keyword_arguments_set_B),
|
|
55
|
-
(
|
|
56
|
-
"single_expr_with_internal_attrs_and_kwargs",
|
|
57
|
-
[Make.Constant(13, lineno=23, col_offset=29, end_lineno=23, end_col_offset=35)],
|
|
58
|
-
keyword_arguments_set_A,
|
|
59
|
-
),
|
|
60
|
-
(
|
|
61
|
-
"two_exprs_with_internal_attrs_and_kwargs",
|
|
62
|
-
[
|
|
63
|
-
Make.Constant(13, lineno=23, col_offset=29, end_lineno=23, end_col_offset=35),
|
|
64
|
-
Make.Constant(31, lineno=37, col_offset=41, end_lineno=37, end_col_offset=47)
|
|
65
|
-
],
|
|
66
|
-
keyword_arguments_set_A,
|
|
67
|
-
)
|
|
68
|
-
]
|
|
69
|
-
|
|
70
|
-
for scenario_id, expressions, kwargs in raw_scenarios:
|
|
71
|
-
# Create a builder that captures the specific expressions and kwargs for this scenario
|
|
72
|
-
def builder(op_type: type[ast.operator], bound_expressions: list[ast.expr] = expressions, bound_kwargs: ast_attributes = kwargs) -> ast.expr:
|
|
73
|
-
return construct_expected_ast_node(op_type, bound_expressions, bound_kwargs)
|
|
74
|
-
list_join_method_test_scenarios_params.append(
|
|
75
|
-
pytest.param(scenario_id, expressions, kwargs, builder, id=scenario_id)
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@pytest.mark.parametrize("JoinImplementerClass, ast_operator_type", listOperatorJoinImplementationAndAstPairs)
|
|
80
|
-
@pytest.mark.parametrize("scenario_identifier, expressions_for_join_method, keyword_arguments_for_join_method, expected_ast_constructor", list_join_method_test_scenarios_params)
|
|
81
|
-
def test_join_method(
|
|
82
|
-
JoinImplementerClass: type,
|
|
83
|
-
ast_operator_type: type[ast.operator],
|
|
84
|
-
scenario_identifier: str,
|
|
85
|
-
expressions_for_join_method: list[ast.expr],
|
|
86
|
-
keyword_arguments_for_join_method: ast_attributes,
|
|
87
|
-
expected_ast_constructor: Callable[[type[ast.operator]], ast.expr]
|
|
88
|
-
|
|
89
|
-
) -> None:
|
|
90
|
-
join_method = JoinImplementerClass.join
|
|
91
|
-
actual_ast_node = cast("ast.expr", join_method(expressions_for_join_method, **keyword_arguments_for_join_method))
|
|
92
|
-
expected_ast_node = expected_ast_constructor(ast_operator_type)
|
|
93
|
-
|
|
94
|
-
dump_of_actual_ast_node = ast.dump(actual_ast_node)
|
|
95
|
-
dump_of_expected_ast_node = ast.dump(expected_ast_node)
|
|
96
|
-
|
|
97
|
-
assert dump_of_actual_ast_node == dump_of_expected_ast_node, (
|
|
98
|
-
f"Test {scenario_identifier} for {JoinImplementerClass.__name__} failed.\n"
|
|
99
|
-
f"Input expressions: {[ast.dump(expression_node) for expression_node in expressions_for_join_method]}\n"
|
|
100
|
-
f"Input kwargs: {keyword_arguments_for_join_method}\n"
|
|
101
|
-
f"Expected AST dump: {dump_of_expected_ast_node}\n"
|
|
102
|
-
f"Actual AST dump: {dump_of_actual_ast_node}"
|
|
103
|
-
)
|
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
from astToolkit import Make, NodeChanger, NodeTourist
|
|
2
|
-
import ast
|
|
3
|
-
|
|
4
|
-
class TestNodeVisitors:
|
|
5
|
-
"""
|
|
6
|
-
Tests adapted from CPython's NodeVisitor and NodeTransformer tests
|
|
7
|
-
to validate astToolkit's NodeTourist and NodeChanger functionality.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
def test_nodeTouristBasicVisiting(self):
|
|
11
|
-
"""Test that NodeTourist can visit nodes and collect information."""
|
|
12
|
-
|
|
13
|
-
visitedNodes = []
|
|
14
|
-
|
|
15
|
-
def collectNameNodes(node: ast.AST):
|
|
16
|
-
if isinstance(node, ast.Name):
|
|
17
|
-
visitedNodes.append(('Name', node.id, node.ctx.__class__.__name__))
|
|
18
|
-
return True
|
|
19
|
-
return False
|
|
20
|
-
|
|
21
|
-
def collectNodeInfo(node: ast.AST):
|
|
22
|
-
# Action is already handled in the predicate for this test
|
|
23
|
-
return None
|
|
24
|
-
|
|
25
|
-
# Create test AST using Make factory
|
|
26
|
-
testModule = Make.Module([
|
|
27
|
-
Make.Assign(
|
|
28
|
-
[Make.Name("x", ast.Store())],
|
|
29
|
-
Make.Constant(42)
|
|
30
|
-
),
|
|
31
|
-
Make.Assign(
|
|
32
|
-
[Make.Name("y", ast.Store())],
|
|
33
|
-
Make.Name("x", ast.Load())
|
|
34
|
-
)
|
|
35
|
-
])
|
|
36
|
-
|
|
37
|
-
# Create tourist that captures Name nodes
|
|
38
|
-
nameTourist = NodeTourist(collectNameNodes, collectNodeInfo)
|
|
39
|
-
nameTourist.visit(testModule)
|
|
40
|
-
|
|
41
|
-
expected = [
|
|
42
|
-
('Name', 'x', 'Store'),
|
|
43
|
-
('Name', 'y', 'Store'),
|
|
44
|
-
('Name', 'x', 'Load') ]
|
|
45
|
-
assert visitedNodes == expected
|
|
46
|
-
|
|
47
|
-
def test_nodeChangerBasicTransformation(self):
|
|
48
|
-
"""Test that NodeChanger can transform nodes."""
|
|
49
|
-
|
|
50
|
-
def findNameNodes(node: ast.AST):
|
|
51
|
-
return isinstance(node, ast.Name) and node.id == "oldVariable"
|
|
52
|
-
|
|
53
|
-
def replaceWithNewName(node):
|
|
54
|
-
return Make.Name("newVariable", node.ctx)
|
|
55
|
-
|
|
56
|
-
# Create test AST
|
|
57
|
-
originalModule = Make.Module([
|
|
58
|
-
Make.Assign(
|
|
59
|
-
[Make.Name("oldVariable", ast.Store())],
|
|
60
|
-
Make.Constant(42)
|
|
61
|
-
),
|
|
62
|
-
Make.Expr(Make.Name("oldVariable", ast.Load()))
|
|
63
|
-
])
|
|
64
|
-
|
|
65
|
-
changer = NodeChanger(findNameNodes, replaceWithNewName)
|
|
66
|
-
transformedModule = changer.visit(originalModule)
|
|
67
|
-
|
|
68
|
-
# Check that names were replaced
|
|
69
|
-
assert isinstance(transformedModule, ast.Module)
|
|
70
|
-
assignment = transformedModule.body[0]
|
|
71
|
-
assert isinstance(assignment, ast.Assign)
|
|
72
|
-
assert isinstance(assignment.targets[0], ast.Name)
|
|
73
|
-
assert assignment.targets[0].id == "newVariable"
|
|
74
|
-
|
|
75
|
-
expression = transformedModule.body[1]
|
|
76
|
-
assert isinstance(expression, ast.Expr)
|
|
77
|
-
assert isinstance(expression.value, ast.Name)
|
|
78
|
-
assert expression.value.id == "newVariable"
|
|
79
|
-
|
|
80
|
-
def test_nodeChangerRemoveNodes(self):
|
|
81
|
-
"""Test NodeChanger can remove nodes by returning None."""
|
|
82
|
-
|
|
83
|
-
def findPrintExpressions(node):
|
|
84
|
-
return (isinstance(node, ast.Expr) and
|
|
85
|
-
isinstance(node.value, ast.Call) and
|
|
86
|
-
isinstance(node.value.func, ast.Name) and
|
|
87
|
-
node.value.func.id == "print")
|
|
88
|
-
|
|
89
|
-
def removeNode(node):
|
|
90
|
-
return None
|
|
91
|
-
# Remove print calls
|
|
92
|
-
|
|
93
|
-
# Create test AST with print calls
|
|
94
|
-
testModule = Make.Module([
|
|
95
|
-
Make.Expr(Make.Call(Make.Name("print", ast.Load()), [Make.Constant("hello")])),
|
|
96
|
-
Make.Assign([Make.Name("x", ast.Store())], Make.Constant(42)),
|
|
97
|
-
Make.Expr(Make.Call(Make.Name("print", ast.Load()), [Make.Name("x", ast.Load())]))
|
|
98
|
-
])
|
|
99
|
-
|
|
100
|
-
remover = NodeChanger(findPrintExpressions, removeNode)
|
|
101
|
-
transformedModule = remover.visit(testModule)
|
|
102
|
-
|
|
103
|
-
# Only the assignment should remain
|
|
104
|
-
assert isinstance(transformedModule, ast.Module)
|
|
105
|
-
assert len(transformedModule.body) == 1
|
|
106
|
-
assert isinstance(transformedModule.body[0], ast.Assign)
|
|
107
|
-
|
|
108
|
-
def test_nodeChangerMultipleReplacements(self):
|
|
109
|
-
"""Test NodeChanger can replace one node with multiple nodes."""
|
|
110
|
-
|
|
111
|
-
def findAssignments(node):
|
|
112
|
-
return (isinstance(node, ast.Assign) and
|
|
113
|
-
len(node.targets) == 1 and
|
|
114
|
-
isinstance(node.targets[0], ast.Name))
|
|
115
|
-
|
|
116
|
-
def expandAssignment(node):
|
|
117
|
-
# Expand x = 42 into x = 42; print(x)
|
|
118
|
-
assignmentNode = node
|
|
119
|
-
printCall = Make.Expr(Make.Call(
|
|
120
|
-
Make.Name("print", ast.Load()),
|
|
121
|
-
[Make.Name(node.targets[0].id, ast.Load())]
|
|
122
|
-
))
|
|
123
|
-
return [assignmentNode, printCall]
|
|
124
|
-
|
|
125
|
-
testModule = Make.Module([
|
|
126
|
-
Make.Assign([Make.Name("x", ast.Store())], Make.Constant(42))
|
|
127
|
-
])
|
|
128
|
-
|
|
129
|
-
expander = NodeChanger(findAssignments, expandAssignment)
|
|
130
|
-
transformedModule = expander.visit(testModule)
|
|
131
|
-
|
|
132
|
-
# Should have both assignment and print
|
|
133
|
-
assert isinstance(transformedModule, ast.Module)
|
|
134
|
-
assert len(transformedModule.body) == 2
|
|
135
|
-
assert isinstance(transformedModule.body[0], ast.Assign)
|
|
136
|
-
assert isinstance(transformedModule.body[1], ast.Expr)
|
|
137
|
-
assert isinstance(transformedModule.body[1].value, ast.Call)
|
|
138
|
-
|
|
139
|
-
def test_nodeTouristWithComplexAST(self):
|
|
140
|
-
"""Test NodeTourist with more complex AST structures."""
|
|
141
|
-
|
|
142
|
-
# Use a shared counter to track different node types
|
|
143
|
-
counts = {"functions": 0, "classes": 0, "loops": 0, "conditions": 0}
|
|
144
|
-
|
|
145
|
-
def findStructuralNodes(node):
|
|
146
|
-
return isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef,
|
|
147
|
-
ast.For, ast.While, ast.If))
|
|
148
|
-
|
|
149
|
-
def countNodeType(node):
|
|
150
|
-
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
151
|
-
counts["functions"] += 1
|
|
152
|
-
elif isinstance(node, ast.ClassDef):
|
|
153
|
-
counts["classes"] += 1
|
|
154
|
-
elif isinstance(node, (ast.For, ast.While)):
|
|
155
|
-
counts["loops"] += 1
|
|
156
|
-
elif isinstance(node, ast.If):
|
|
157
|
-
counts["conditions"] += 1
|
|
158
|
-
return node
|
|
159
|
-
|
|
160
|
-
# Create complex AST structure
|
|
161
|
-
complexModule = Make.Module([
|
|
162
|
-
Make.FunctionDef(
|
|
163
|
-
"function1",
|
|
164
|
-
Make.arguments(),
|
|
165
|
-
body=[
|
|
166
|
-
Make.If(
|
|
167
|
-
Make.Name("condition", ast.Load()),
|
|
168
|
-
body=[Make.Pass()]
|
|
169
|
-
),
|
|
170
|
-
Make.For(
|
|
171
|
-
Make.Name("item", ast.Store()),
|
|
172
|
-
Make.Name("items", ast.Load()),
|
|
173
|
-
body=[Make.Pass()]
|
|
174
|
-
)
|
|
175
|
-
]
|
|
176
|
-
),
|
|
177
|
-
Make.ClassDef(
|
|
178
|
-
"TestClass",
|
|
179
|
-
body=[
|
|
180
|
-
Make.FunctionDef(
|
|
181
|
-
"method",
|
|
182
|
-
Make.arguments(),
|
|
183
|
-
body=[
|
|
184
|
-
Make.While(
|
|
185
|
-
Make.Constant(True),
|
|
186
|
-
body=[Make.Break()]
|
|
187
|
-
)
|
|
188
|
-
]
|
|
189
|
-
)
|
|
190
|
-
]
|
|
191
|
-
)
|
|
192
|
-
])
|
|
193
|
-
|
|
194
|
-
analyzer = NodeTourist(findStructuralNodes, countNodeType)
|
|
195
|
-
analyzer.visit(complexModule)
|
|
196
|
-
|
|
197
|
-
assert counts["functions"] == 2
|
|
198
|
-
# function1 and method
|
|
199
|
-
assert counts["classes"] == 1
|
|
200
|
-
# TestClass
|
|
201
|
-
assert counts["loops"] == 2
|
|
202
|
-
# for and while
|
|
203
|
-
assert counts["conditions"] == 1
|
|
204
|
-
# if statement
|
|
205
|
-
def test_nodeChangerPreservesLocation(self):
|
|
206
|
-
"""Test that NodeChanger preserves location information."""
|
|
207
|
-
|
|
208
|
-
def findIntConstants(node):
|
|
209
|
-
return isinstance(node, ast.Constant) and isinstance(node.value, int)
|
|
210
|
-
|
|
211
|
-
def doubleConstant(node):
|
|
212
|
-
# Replace integer constants with doubled values
|
|
213
|
-
newNode = Make.Constant(node.value * 2)
|
|
214
|
-
return ast.copy_location(newNode, node)
|
|
215
|
-
|
|
216
|
-
# Create AST with location info
|
|
217
|
-
testModule = Make.Module([
|
|
218
|
-
Make.Assign(
|
|
219
|
-
[Make.Name("x", ast.Store())],
|
|
220
|
-
Make.Constant(21, lineno=1, col_offset=4)
|
|
221
|
-
)
|
|
222
|
-
])
|
|
223
|
-
|
|
224
|
-
changer = NodeChanger(findIntConstants, doubleConstant)
|
|
225
|
-
transformedModule = changer.visit(testModule)
|
|
226
|
-
|
|
227
|
-
# Check that value was doubled and location preserved
|
|
228
|
-
assert isinstance(transformedModule, ast.Module)
|
|
229
|
-
assignment = transformedModule.body[0]
|
|
230
|
-
assert isinstance(assignment, ast.Assign)
|
|
231
|
-
constantNode = assignment.value
|
|
232
|
-
assert isinstance(constantNode, ast.Constant)
|
|
233
|
-
assert constantNode.value == 42
|
|
234
|
-
assert constantNode.lineno == 1
|
|
235
|
-
assert constantNode.col_offset == 4
|
|
236
|
-
|
|
237
|
-
def test_nodeChangerWithMakeFactoryMethods(self):
|
|
238
|
-
"""Test NodeChanger using Make factory methods for transformations."""
|
|
239
|
-
|
|
240
|
-
def findAugAssign(node):
|
|
241
|
-
return isinstance(node, ast.AugAssign)
|
|
242
|
-
|
|
243
|
-
def expandToAssignment(node):
|
|
244
|
-
"""Expand x += 1 to x = x + 1"""
|
|
245
|
-
# Create regular assignment equivalent
|
|
246
|
-
binaryOp = Make.BinOp(
|
|
247
|
-
Make.Name(node.target.id, ast.Load()),
|
|
248
|
-
node.op,
|
|
249
|
-
node.value
|
|
250
|
-
)
|
|
251
|
-
assignment = Make.Assign(
|
|
252
|
-
[Make.Name(node.target.id, ast.Store())],
|
|
253
|
-
binaryOp
|
|
254
|
-
)
|
|
255
|
-
return ast.copy_location(assignment, node)
|
|
256
|
-
|
|
257
|
-
testModule = Make.Module([
|
|
258
|
-
Make.AugAssign(
|
|
259
|
-
Make.Name("counter", ast.Store()),
|
|
260
|
-
ast.Add(),
|
|
261
|
-
Make.Constant(1)
|
|
262
|
-
)
|
|
263
|
-
])
|
|
264
|
-
|
|
265
|
-
expander = NodeChanger(findAugAssign, expandToAssignment)
|
|
266
|
-
transformedModule = expander.visit(testModule)
|
|
267
|
-
|
|
268
|
-
# Should be converted to regular assignment
|
|
269
|
-
assert isinstance(transformedModule, ast.Module)
|
|
270
|
-
statement = transformedModule.body[0]
|
|
271
|
-
assert isinstance(statement, ast.Assign)
|
|
272
|
-
assert isinstance(statement.value, ast.BinOp)
|
|
273
|
-
assert isinstance(statement.value.op, ast.Add)
|
|
274
|
-
|
|
275
|
-
def test_nodeVisitorErrorHandling(self):
|
|
276
|
-
"""Test visitor error handling with malformed AST."""
|
|
277
|
-
|
|
278
|
-
errorCount = [0]
|
|
279
|
-
# Use list for mutable reference
|
|
280
|
-
|
|
281
|
-
def findNameNodes(node):
|
|
282
|
-
return isinstance(node, ast.Name)
|
|
283
|
-
|
|
284
|
-
def handleNameNode(node):
|
|
285
|
-
try:
|
|
286
|
-
# This might fail if node is malformed
|
|
287
|
-
if hasattr(node, 'id') and node.id:
|
|
288
|
-
pass
|
|
289
|
-
except Exception:
|
|
290
|
-
errorCount[0] += 1
|
|
291
|
-
return node
|
|
292
|
-
|
|
293
|
-
# Create valid AST
|
|
294
|
-
validModule = Make.Module([
|
|
295
|
-
Make.Expr(Make.Name("validName", ast.Load()))
|
|
296
|
-
])
|
|
297
|
-
|
|
298
|
-
tourist = NodeTourist(findNameNodes, handleNameNode)
|
|
299
|
-
tourist.visit(validModule)
|
|
300
|
-
|
|
301
|
-
# Should handle valid AST without errors
|
|
302
|
-
assert errorCount[0] == 0
|
|
303
|
-
|
|
304
|
-
def test_nestedVisitorTransformations(self):
|
|
305
|
-
"""Test applying multiple transformations in sequence."""
|
|
306
|
-
|
|
307
|
-
def findUppercaseNames(node):
|
|
308
|
-
return isinstance(node, ast.Name) and node.id.isupper()
|
|
309
|
-
|
|
310
|
-
def lowercaseName(node):
|
|
311
|
-
return Make.Name(node.id.lower(), node.ctx)
|
|
312
|
-
|
|
313
|
-
def findIntConstants(node):
|
|
314
|
-
return isinstance(node, ast.Constant) and isinstance(node.value, int)
|
|
315
|
-
|
|
316
|
-
def doubleConstant(node):
|
|
317
|
-
return Make.Constant(node.value * 2)
|
|
318
|
-
|
|
319
|
-
# Create test AST
|
|
320
|
-
originalModule = Make.Module([
|
|
321
|
-
Make.Assign(
|
|
322
|
-
[Make.Name("VARIABLE", ast.Store())],
|
|
323
|
-
Make.Constant(21)
|
|
324
|
-
)
|
|
325
|
-
])
|
|
326
|
-
|
|
327
|
-
# Apply transformations in sequence
|
|
328
|
-
step1 = NodeChanger(findUppercaseNames, lowercaseName)
|
|
329
|
-
step2 = NodeChanger(findIntConstants, doubleConstant)
|
|
330
|
-
|
|
331
|
-
intermediateModule = step1.visit(originalModule)
|
|
332
|
-
finalModule = step2.visit(intermediateModule)
|
|
333
|
-
|
|
334
|
-
# Check both transformations applied
|
|
335
|
-
assert isinstance(finalModule, ast.Module)
|
|
336
|
-
assignment = finalModule.body[0]
|
|
337
|
-
assert isinstance(assignment, ast.Assign)
|
|
338
|
-
assert isinstance(assignment.targets[0], ast.Name)
|
|
339
|
-
assert assignment.targets[0].id == "variable"
|
|
340
|
-
# lowercased
|
|
341
|
-
assert isinstance(assignment.value, ast.Constant)
|
|
342
|
-
assert assignment.value.value == 42
|
|
343
|
-
# doubled
|
|
344
|
-
def test_visitorWithMakeJoinMethods(self):
|
|
345
|
-
"""Test visitors with Make join methods for combining expressions."""
|
|
346
|
-
|
|
347
|
-
def findComplexBoolOp(node):
|
|
348
|
-
return (isinstance(node, ast.BoolOp) and
|
|
349
|
-
isinstance(node.op, ast.And) and
|
|
350
|
-
len(node.values) > 2)
|
|
351
|
-
|
|
352
|
-
def simplifyBoolOp(node):
|
|
353
|
-
# Use Make.And.join for cleaner combination
|
|
354
|
-
return Make.And.join(node.values)
|
|
355
|
-
|
|
356
|
-
# Create complex boolean expression
|
|
357
|
-
testModule = Make.Module([
|
|
358
|
-
Make.If(
|
|
359
|
-
Make.BoolOp(ast.And(), [
|
|
360
|
-
Make.Name("a", ast.Load()),
|
|
361
|
-
Make.Name("b", ast.Load()),
|
|
362
|
-
Make.Name("c", ast.Load()),
|
|
363
|
-
Make.Name("d", ast.Load())
|
|
364
|
-
]),
|
|
365
|
-
body=[Make.Pass()]
|
|
366
|
-
)
|
|
367
|
-
])
|
|
368
|
-
|
|
369
|
-
combiner = NodeChanger(findComplexBoolOp, simplifyBoolOp)
|
|
370
|
-
transformedModule = combiner.visit(testModule)
|
|
371
|
-
|
|
372
|
-
# Should still be a valid boolean expression
|
|
373
|
-
assert isinstance(transformedModule, ast.Module)
|
|
374
|
-
ifStatement = transformedModule.body[0]
|
|
375
|
-
assert isinstance(ifStatement, ast.If)
|
|
376
|
-
condition = ifStatement.test
|
|
377
|
-
assert isinstance(condition, (ast.BoolOp, ast.Name))
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|