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.
Files changed (36) hide show
  1. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/PKG-INFO +3 -3
  2. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/README.md +1 -1
  3. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolkitAST.py +6 -10
  4. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/transformationTools.py +97 -57
  5. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit.egg-info/PKG-INFO +3 -3
  6. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit.egg-info/SOURCES.txt +0 -3
  7. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit.egg-info/requires.txt +1 -1
  8. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/pyproject.toml +2 -2
  9. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/tests/conftest.py +0 -7
  10. asttoolkit-0.7.0/tests/test_Find.py +0 -15
  11. asttoolkit-0.7.0/tests/test_joinClassmethod.py +0 -103
  12. asttoolkit-0.7.0/tests/test_nodeVisitors.py +0 -377
  13. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/LICENSE +0 -0
  14. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/__init__.py +0 -0
  15. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_astTypes.py +0 -0
  16. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_dumpFunctionDef.py +0 -0
  17. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_dumpHandmade.py +0 -0
  18. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_prototypeFind.py +0 -0
  19. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_theSSOT.py +0 -0
  20. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolBe.py +0 -0
  21. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolDOT.py +0 -0
  22. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolGrab.py +0 -0
  23. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolIfThis.py +0 -0
  24. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolMake.py +0 -0
  25. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolThen.py +0 -0
  26. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolkitContainers.py +0 -0
  27. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/_toolkitNodeVisitor.py +0 -0
  28. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit/py.typed +0 -0
  29. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit.egg-info/dependency_links.txt +0 -0
  30. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/astToolkit.egg-info/top_level.txt +0 -0
  31. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/setup.cfg +0 -0
  32. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/tests/__init__.py +0 -0
  33. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/tests/dataSamples/Make.py +0 -0
  34. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/tests/dataSamples/__init__.py +0 -0
  35. {asttoolkit-0.7.0 → asttoolkit-0.7.2}/tests/dataSamples/createTestData.py +0 -0
  36. {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.0
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
- [![CC-BY-NC-4.0](https://github.com/hunterhogan/astToolkit/blob/main/CC-BY-NC-4.0.svg)](https://creativecommons.org/licenses/by-nc/4.0/)
357
+ [![CC-BY-NC-4.0](https://github.com/hunterhogan/astToolkit/blob/main/CC-BY-NC-4.0.png)](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
- [![CC-BY-NC-4.0](https://github.com/hunterhogan/astToolkit/blob/main/CC-BY-NC-4.0.svg)](https://creativecommons.org/licenses/by-nc/4.0/)
315
+ [![CC-BY-NC-4.0](https://github.com/hunterhogan/astToolkit/blob/main/CC-BY-NC-4.0.png)](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 a function definition with a specific name from an AST module.
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
- The AST object to search within.
79
+ (abstract syntax tree) An `ast.AST` subclass from which to extract the function.
84
80
  identifier : str
85
- The name of the function to find.
81
+ The identifier in `ast.FunctionDef.name`.
86
82
 
87
83
  Returns
88
84
  -------
89
- astFunctionDef | None
90
- The matching function definition AST node, or `None` if not found.
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. Supports all relevant `ast.parse` parameters.
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, module: ast.Module) -> ast.FunctionDef: # noqa: C901, PLR0912
131
+ def inlineFunctionDef(identifierToInline: str, astModule: ast.Module) -> ast.FunctionDef:
131
132
  """
132
- Inline function calls within a function definition to create a self-contained function.
133
+ Synthesize a new function from `identifierToInline` with the logic "inlined" from functions called by `identifierToInline`.
133
134
 
134
- (AI generated docstring)
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
- This function takes a function identifier and a module, finds the function definition,
137
- and then recursively inlines all function calls within that function with their
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 name of the function to inline.
145
- module : ast.Module
146
- The AST module containing the function and its dependencies.
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 inlined function definition as an `ast.FunctionDef` object.
160
+ The synthesized function with inlined logic from other functions defined in `astModule`.
152
161
 
153
162
  Raises
154
163
  ------
155
- ValueError: If the function to inline is not found in the module.
164
+ ValueError: If `identifierToInline` does not match any `ast.FunctionDef.name` in `astModule`.
156
165
  """
157
- dictionaryFunctionDef: dict[str, ast.FunctionDef] = makeDictionaryFunctionDef(module)
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"FunctionDefToInline not found in dictionaryIdentifier2FunctionDef: {identifierToInline = }"
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
- if NodeTourist(IfThis.matchesMeButNotAnyDescendant(IfThis.isCallIdentifier(identifier)), Then.extractIt).captureLastMatch(module) is not None:
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(module) is not None:
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 = raiseIfNone(ast.unparse(astModule))
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.0
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
- [![CC-BY-NC-4.0](https://github.com/hunterhogan/astToolkit/blob/main/CC-BY-NC-4.0.svg)](https://creativecommons.org/licenses/by-nc/4.0/)
357
+ [![CC-BY-NC-4.0](https://github.com/hunterhogan/astToolkit/blob/main/CC-BY-NC-4.0.png)](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,5 +1,5 @@
1
1
  autoflake
2
- hunterMakesPy
2
+ hunterMakesPy>=0.2.1
3
3
  typing_extensions>=4.10.0
4
4
 
5
5
  [development]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "astToolkit"
3
- version = "0.7.0"
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