mapFolding 0.8.5__py3-none-any.whl → 0.9.0__py3-none-any.whl

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 (33) hide show
  1. mapFolding/__init__.py +66 -18
  2. mapFolding/basecamp.py +32 -17
  3. mapFolding/beDRY.py +3 -3
  4. mapFolding/oeis.py +121 -25
  5. mapFolding/someAssemblyRequired/__init__.py +48 -27
  6. mapFolding/someAssemblyRequired/_theTypes.py +11 -15
  7. mapFolding/someAssemblyRequired/_tool_Make.py +40 -12
  8. mapFolding/someAssemblyRequired/_tool_Then.py +59 -25
  9. mapFolding/someAssemblyRequired/_toolboxAntecedents.py +151 -276
  10. mapFolding/someAssemblyRequired/_toolboxContainers.py +185 -51
  11. mapFolding/someAssemblyRequired/_toolboxPython.py +165 -44
  12. mapFolding/someAssemblyRequired/synthesizeNumbaJob.py +141 -20
  13. mapFolding/someAssemblyRequired/toolboxNumba.py +93 -52
  14. mapFolding/someAssemblyRequired/transformationTools.py +228 -138
  15. mapFolding/syntheticModules/numbaCount_doTheNeedful.py +0 -1
  16. mapFolding/theSSOT.py +147 -55
  17. mapFolding/toolboxFilesystem.py +1 -1
  18. mapfolding-0.9.0.dist-info/METADATA +177 -0
  19. mapfolding-0.9.0.dist-info/RECORD +46 -0
  20. tests/__init__.py +44 -0
  21. tests/conftest.py +75 -7
  22. tests/test_computations.py +90 -9
  23. tests/test_filesystem.py +32 -33
  24. tests/test_other.py +0 -1
  25. tests/test_tasks.py +2 -2
  26. mapFolding/noHomeYet.py +0 -32
  27. mapFolding/someAssemblyRequired/newInliner.py +0 -22
  28. mapfolding-0.8.5.dist-info/METADATA +0 -190
  29. mapfolding-0.8.5.dist-info/RECORD +0 -48
  30. {mapfolding-0.8.5.dist-info → mapfolding-0.9.0.dist-info}/WHEEL +0 -0
  31. {mapfolding-0.8.5.dist-info → mapfolding-0.9.0.dist-info}/entry_points.txt +0 -0
  32. {mapfolding-0.8.5.dist-info → mapfolding-0.9.0.dist-info}/licenses/LICENSE +0 -0
  33. {mapfolding-0.8.5.dist-info → mapfolding-0.9.0.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,55 @@
1
1
  """
2
- Container classes for AST transformations and code synthesis.
2
+ AST Container Classes for Python Code Generation and Transformation
3
3
 
4
- This module provides container classes used in the AST transformation process
5
- and code synthesis workflows. It acts as a dependency boundary to prevent
6
- circular imports while providing reusable data structures.
4
+ This module provides specialized container classes that organize AST nodes, imports,
5
+ and program structure for code generation and transformation. These classes form
6
+ the organizational backbone of the code generation system, enabling:
7
+
8
+ 1. Tracking and managing imports with LedgerOfImports
9
+ 2. Packaging function definitions with their dependencies via IngredientsFunction
10
+ 3. Structuring complete modules with IngredientsModule
11
+ 4. Configuring code synthesis with RecipeSynthesizeFlow
12
+ 5. Organizing decomposed dataclass representations with ShatteredDataclass
13
+
14
+ Together, these container classes implement a component-based architecture for
15
+ programmatic generation of high-performance code. They maintain a clean separation
16
+ between structure and content, allowing transformations to be applied systematically
17
+ while preserving relationships between code elements.
18
+
19
+ The containers work in conjunction with transformation tools that manipulate the
20
+ contained AST nodes to implement specific optimizations and transformations.
7
21
  """
22
+
8
23
  from collections import defaultdict
9
24
  from collections.abc import Sequence
10
- from mapFolding.someAssemblyRequired import ImaAnnotationType, ast_Identifier, be, Make, parseLogicalPath2astModule, str_nameDOTname
11
- from mapFolding.theSSOT import callableDispatcherHARDCODED, The
25
+ from mapFolding.someAssemblyRequired import ast_Identifier, Make, parseLogicalPath2astModule, str_nameDOTname
26
+ from mapFolding.theSSOT import The
12
27
  from pathlib import Path, PurePosixPath
13
28
  from Z0Z_tools import updateExtendPolishDictionaryLists
14
29
  import ast
15
30
  import dataclasses
16
31
 
32
+ # Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
17
33
  class LedgerOfImports:
34
+ """
35
+ Track and manage import statements for programmatically generated code.
36
+
37
+ LedgerOfImports acts as a registry for import statements, maintaining a clean
38
+ separation between the logical structure of imports and their textual representation.
39
+ It enables:
40
+
41
+ 1. Tracking regular imports and import-from statements
42
+ 2. Adding imports programmatically during code transformation
43
+ 3. Merging imports from multiple sources
44
+ 4. Removing unnecessary or conflicting imports
45
+ 5. Generating optimized AST import nodes for the final code
46
+
47
+ This class forms the foundation of dependency management in generated code,
48
+ ensuring that all required libraries are available without duplication or
49
+ conflict.
50
+ """
18
51
  # TODO When resolving the ledger of imports, remove self-referential imports
19
- # TODO TypeIgnore :/
52
+ # TODO add TypeIgnore tracking to the ledger of imports
20
53
 
21
54
  def __init__(self, startWith: ast.AST | None = None) -> None:
22
55
  self.dictionaryImportFrom: dict[str_nameDOTname, list[tuple[ast_Identifier, ast_Identifier | None]]] = defaultdict(list)
@@ -26,21 +59,54 @@ class LedgerOfImports:
26
59
 
27
60
  def addAst(self, astImport____: ast.Import | ast.ImportFrom) -> None:
28
61
  assert isinstance(astImport____, (ast.Import, ast.ImportFrom)), f"I received {type(astImport____) = }, but I can only accept {ast.Import} and {ast.ImportFrom}."
29
- if be.Import(astImport____):
62
+ if isinstance(astImport____, ast.Import):
30
63
  for alias in astImport____.names:
31
64
  self.listImport.append(alias.name)
32
- elif be.ImportFrom(astImport____):
65
+ elif isinstance(astImport____, ast.ImportFrom): # type: ignore
33
66
  # TODO fix the mess created by `None` means '.'. I need a `str_nameDOTname` to replace '.'
34
67
  if astImport____.module is None:
35
68
  astImport____.module = '.'
36
69
  for alias in astImport____.names:
37
70
  self.dictionaryImportFrom[astImport____.module].append((alias.name, alias.asname))
38
71
 
39
- def addImport_asStr(self, moduleIdentifier: str_nameDOTname) -> None:
40
- self.listImport.append(moduleIdentifier)
41
-
42
- def addImportFrom_asStr(self, moduleIdentifier: ast_Identifier, name: ast_Identifier, asname: ast_Identifier | None = None) -> None:
43
- self.dictionaryImportFrom[moduleIdentifier].append((name, asname))
72
+ def addImport_asStr(self, moduleWithLogicalPath: str_nameDOTname) -> None:
73
+ self.listImport.append(moduleWithLogicalPath)
74
+
75
+ # def addImportFrom_asStr(self, moduleWithLogicalPath: str_nameDOTname, name: ast_Identifier, asname: ast_Identifier | None = None) -> None:
76
+ # self.dictionaryImportFrom[moduleWithLogicalPath].append((name, asname))
77
+
78
+ def addImportFrom_asStr(self, moduleWithLogicalPath: str_nameDOTname, name: ast_Identifier, asname: ast_Identifier | None = None) -> None:
79
+ if moduleWithLogicalPath not in self.dictionaryImportFrom:
80
+ self.dictionaryImportFrom[moduleWithLogicalPath] = []
81
+ self.dictionaryImportFrom[moduleWithLogicalPath].append((name, asname))
82
+
83
+ def removeImportFromModule(self, moduleWithLogicalPath: str_nameDOTname) -> None:
84
+ """Remove all imports from a specific module."""
85
+ self.removeImportFrom(moduleWithLogicalPath, None, None)
86
+
87
+ def removeImportFrom(self, moduleWithLogicalPath: str_nameDOTname, name: ast_Identifier | None, asname: ast_Identifier | None = None) -> None:
88
+ assert moduleWithLogicalPath is not None, SyntaxError(f"I received `{moduleWithLogicalPath = }`, but it must be the name of a module.")
89
+ if moduleWithLogicalPath in self.dictionaryImportFrom:
90
+ """
91
+ name, asname Meaning
92
+ ast_Identifier, ast_Identifier : remove exact matches
93
+ ast_Identifier, None : remove exact matches
94
+ None, ast_Identifier : remove all matches for asname and if entry_asname is None remove name == ast_Identifier
95
+ None, None : remove all matches for the module
96
+ """
97
+ if name is None and asname is None:
98
+ # Remove all entries for the module
99
+ self.dictionaryImportFrom.pop(moduleWithLogicalPath)
100
+ else:
101
+ if name is None:
102
+ self.dictionaryImportFrom[moduleWithLogicalPath] = [(entry_name, entry_asname) for entry_name, entry_asname in self.dictionaryImportFrom[moduleWithLogicalPath]
103
+ if not (entry_asname == asname) and not (entry_asname is None and entry_name == asname)]
104
+ else:
105
+ # Remove exact matches for the module
106
+ self.dictionaryImportFrom[moduleWithLogicalPath] = [(entry_name, entry_asname) for entry_name, entry_asname in self.dictionaryImportFrom[moduleWithLogicalPath]
107
+ if not (entry_name == name and entry_asname == asname)]
108
+ if not self.dictionaryImportFrom[moduleWithLogicalPath]:
109
+ self.dictionaryImportFrom.pop(moduleWithLogicalPath)
44
110
 
45
111
  def exportListModuleIdentifiers(self) -> list[ast_Identifier]:
46
112
  listModuleIdentifiers: list[ast_Identifier] = list(self.dictionaryImportFrom.keys())
@@ -49,13 +115,14 @@ class LedgerOfImports:
49
115
 
50
116
  def makeList_ast(self) -> list[ast.ImportFrom | ast.Import]:
51
117
  listImportFrom: list[ast.ImportFrom] = []
52
- for moduleIdentifier, listOfNameTuples in sorted(self.dictionaryImportFrom.items()):
118
+ for moduleWithLogicalPath, listOfNameTuples in sorted(self.dictionaryImportFrom.items()):
53
119
  listOfNameTuples = sorted(list(set(listOfNameTuples)), key=lambda nameTuple: nameTuple[0])
54
120
  list_alias: list[ast.alias] = []
55
121
  for name, asname in listOfNameTuples:
56
122
  list_alias.append(Make.alias(name, asname))
57
- listImportFrom.append(Make.ImportFrom(moduleIdentifier, list_alias))
58
- list_astImport: list[ast.Import] = [Make.Import(moduleIdentifier) for moduleIdentifier in sorted(set(self.listImport))]
123
+ if list_alias:
124
+ listImportFrom.append(Make.ImportFrom(moduleWithLogicalPath, list_alias))
125
+ list_astImport: list[ast.Import] = [Make.Import(moduleWithLogicalPath) for moduleWithLogicalPath in sorted(set(self.listImport))]
59
126
  return listImportFrom + list_astImport
60
127
 
61
128
  def update(self, *fromLedger: 'LedgerOfImports') -> None:
@@ -72,20 +139,55 @@ class LedgerOfImports:
72
139
  if isinstance(nodeBuffalo, (ast.Import, ast.ImportFrom)):
73
140
  self.addAst(nodeBuffalo)
74
141
 
142
+ # Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
75
143
  @dataclasses.dataclass
76
144
  class IngredientsFunction:
77
- """Everything necessary to integrate a function into a module should be here.
145
+ """
146
+ Package a function definition with its import dependencies for code generation.
147
+
148
+ IngredientsFunction encapsulates an AST function definition along with all the
149
+ imports required for that function to operate correctly. This creates a modular,
150
+ portable unit that can be:
151
+
152
+ 1. Transformed independently (e.g., by applying Numba decorators)
153
+ 2. Transplanted between modules while maintaining dependencies
154
+ 3. Combined with other functions to form complete modules
155
+ 4. Analyzed for optimization opportunities
156
+
157
+ This class forms the primary unit of function manipulation in the code generation
158
+ system, enabling targeted transformations while preserving function dependencies.
159
+
78
160
  Parameters:
79
- astFunctionDef: hint `Make.astFunctionDef()`
161
+ astFunctionDef: The AST representation of the function definition
162
+ imports: Import statements needed by the function
163
+ type_ignores: Type ignore comments associated with the function
80
164
  """
81
165
  astFunctionDef: ast.FunctionDef
82
166
  imports: LedgerOfImports = dataclasses.field(default_factory=LedgerOfImports)
83
167
  type_ignores: list[ast.TypeIgnore] = dataclasses.field(default_factory=list)
84
168
 
169
+ # Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
85
170
  @dataclasses.dataclass
86
171
  class IngredientsModule:
87
- """Everything necessary to create one _logical_ `ast.Module` should be here.
88
- Extrinsic qualities should _probably_ be handled externally.
172
+ """
173
+ Assemble a complete Python module from its constituent AST components.
174
+
175
+ IngredientsModule provides a structured container for all elements needed to
176
+ generate a complete Python module, including:
177
+
178
+ 1. Import statements aggregated from all module components
179
+ 2. Prologue code that runs before function definitions
180
+ 3. Function definitions with their dependencies
181
+ 4. Epilogue code that runs after function definitions
182
+ 5. Entry point code executed when the module runs as a script
183
+ 6. Type ignores and other annotations
184
+
185
+ This class enables programmatic assembly of Python modules with a clear
186
+ separation between different structural elements, while maintaining the
187
+ proper ordering and relationships between components.
188
+
189
+ The modular design allows transformations to be applied to specific parts
190
+ of a module while preserving the overall structure.
89
191
 
90
192
  Parameters:
91
193
  ingredientsFunction (None): One or more `IngredientsFunction` that will appended to `listIngredientsFunctions`.
@@ -124,7 +226,7 @@ class IngredientsModule:
124
226
  """Append one or more statements to `prologue`."""
125
227
  list_body: list[ast.stmt] = []
126
228
  listTypeIgnore: list[ast.TypeIgnore] = []
127
- if astModule is not None and be.Module(astModule):
229
+ if astModule is not None and isinstance(astModule, ast.Module): # type: ignore
128
230
  list_body.extend(astModule.body)
129
231
  listTypeIgnore.extend(astModule.type_ignores)
130
232
  if type_ignores is not None:
@@ -153,10 +255,21 @@ class IngredientsModule:
153
255
  def appendIngredientsFunction(self, *ingredientsFunction: IngredientsFunction) -> None:
154
256
  """Append one or more `IngredientsFunction`."""
155
257
  for allegedIngredientsFunction in ingredientsFunction:
156
- if isinstance(allegedIngredientsFunction, IngredientsFunction):
157
- self.listIngredientsFunctions.append(allegedIngredientsFunction)
158
- else:
159
- raise ValueError(f"I received `{type(allegedIngredientsFunction) = }`, but I can only accept `{IngredientsFunction}`.")
258
+ assert isinstance(allegedIngredientsFunction, IngredientsFunction), ValueError(f"I received `{type(allegedIngredientsFunction) = }`, but I can only accept `{IngredientsFunction}`.")
259
+ self.listIngredientsFunctions.append(allegedIngredientsFunction)
260
+
261
+ def removeImportFromModule(self, moduleWithLogicalPath: str_nameDOTname) -> None:
262
+ self.removeImportFrom(moduleWithLogicalPath, None, None)
263
+ """Remove all imports from a specific module."""
264
+
265
+ def removeImportFrom(self, moduleWithLogicalPath: str_nameDOTname, name: ast_Identifier | None, asname: ast_Identifier | None = None) -> None:
266
+ """
267
+ This method modifies all `LedgerOfImports` in this `IngredientsModule` and all `IngredientsFunction` in `listIngredientsFunctions`.
268
+ It is not a "blacklist", so the `import from` could be added after this modification.
269
+ """
270
+ self.imports.removeImportFrom(moduleWithLogicalPath, name, asname)
271
+ for ingredientsFunction in self.listIngredientsFunctions:
272
+ ingredientsFunction.imports.removeImportFrom(moduleWithLogicalPath, name, asname)
160
273
 
161
274
  @property
162
275
  def list_astImportImportFrom(self) -> list[ast.Import | ast.ImportFrom]:
@@ -191,13 +304,33 @@ class IngredientsModule:
191
304
  listTypeIgnore.extend(self.launcher.type_ignores)
192
305
  return listTypeIgnore
193
306
 
307
+ # Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
194
308
  @dataclasses.dataclass
195
309
  class RecipeSynthesizeFlow:
196
- """Settings for synthesizing flow."""
310
+ """
311
+ Configure the generation of optimized Numba-accelerated code modules.
312
+
313
+ RecipeSynthesizeFlow defines the complete blueprint for transforming an original
314
+ Python algorithm into an optimized, accelerated implementation. It specifies:
315
+
316
+ 1. Source code locations and identifiers
317
+ 2. Target code locations and identifiers
318
+ 3. Naming conventions for generated modules and functions
319
+ 4. File system paths for output files
320
+ 5. Import relationships between components
321
+
322
+ This configuration class serves as a single source of truth for the code generation
323
+ process, ensuring consistency across all generated artifacts while enabling
324
+ customization of the transformation pipeline.
325
+
326
+ The transformation process uses this configuration to extract functions from the
327
+ source module, transform them according to optimization rules, and output
328
+ properly structured optimized modules with all necessary imports.
329
+ """
197
330
  # ========================================
198
331
  # Source
199
- # ========================================
200
- source_astModule = parseLogicalPath2astModule(The.logicalPathModuleSourceAlgorithm)
332
+ source_astModule: ast.Module = parseLogicalPath2astModule(The.logicalPathModuleSourceAlgorithm)
333
+ """AST of the source algorithm module containing the original implementation."""
201
334
 
202
335
  # Figure out dynamic flow control to synthesized modules https://github.com/hunterhogan/mapFolding/issues/4
203
336
  sourceCallableDispatcher: ast_Identifier = The.sourceCallableDispatcher
@@ -245,18 +378,15 @@ class RecipeSynthesizeFlow:
245
378
 
246
379
  # ========================================
247
380
  # Computed
248
- # ========================================
249
- """
250
- theFormatStrModuleSynthetic = "{packageFlow}Count"
251
- theFormatStrModuleForCallableSynthetic = theFormatStrModuleSynthetic + "_{callableTarget}"
252
- theModuleDispatcherSynthetic: ast_Identifier = theFormatStrModuleForCallableSynthetic.format(packageFlow=packageFlowSynthetic, callableTarget=The.sourceCallableDispatcher)
253
- theLogicalPathModuleDispatcherSynthetic: str = '.'.join([The.packageName, The.moduleOfSyntheticModules, theModuleDispatcherSynthetic])
254
-
255
- """
381
+ # Figure out dynamic flow control to synthesized modules https://github.com/hunterhogan/mapFolding/issues/4
382
+ # theFormatStrModuleSynthetic = "{packageFlow}Count"
383
+ # theFormatStrModuleForCallableSynthetic = theFormatStrModuleSynthetic + "_{callableTarget}"
384
+ # theModuleDispatcherSynthetic: ast_Identifier = theFormatStrModuleForCallableSynthetic.format(packageFlow=packageFlowSynthetic, callableTarget=The.sourceCallableDispatcher)
385
+ # theLogicalPathModuleDispatcherSynthetic: str = '.'.join([The.packageName, The.moduleOfSyntheticModules, theModuleDispatcherSynthetic])
256
386
  # logicalPathModuleDispatcher: str = '.'.join([Z0Z_flowLogicalPathRoot, moduleDispatcher])
387
+
257
388
  # ========================================
258
389
  # Filesystem (names of physical objects)
259
- # ========================================
260
390
  pathPackage: PurePosixPath | None = PurePosixPath(The.pathPackage)
261
391
  fileExtension: str = The.fileExtension
262
392
 
@@ -289,46 +419,50 @@ theLogicalPathModuleDispatcherSynthetic: str = '.'.join([The.packageName, The.mo
289
419
  def pathFilenameSequential(self) -> PurePosixPath:
290
420
  return self._makePathFilename(filenameStem=self.moduleSequential, logicalPathINFIX=self.logicalPathFlowRoot)
291
421
 
292
- def __post_init__(self) -> None:
293
- if ((self.concurrencyManagerIdentifier is not None and self.concurrencyManagerIdentifier != self.sourceConcurrencyManagerIdentifier) # `submit` # type: ignore
294
- or ((self.concurrencyManagerIdentifier is None) != (self.concurrencyManagerNamespace is None))): # type: ignore
295
- import warnings
296
- warnings.warn(f"If your synthesized module is weird, check `{self.concurrencyManagerIdentifier=}` and `{self.concurrencyManagerNamespace=}`. (ChildProcessError? 'Yeah! Children shouldn't be processing stuff, man.')", category=ChildProcessError, stacklevel=2) # pyright: ignore[reportCallIssue, reportArgumentType] Y'all Pynatics need to be less shrill and focus on making code that doesn't need 8000 error categories.
297
-
298
- # self.logicalPathModuleDispatcher!=logicalPathModuleDispatcherHARDCODED or
299
- if self.callableDispatcher!=callableDispatcherHARDCODED:
300
- print(f"fyi: `{self.callableDispatcher=}` but\n\t`{callableDispatcherHARDCODED=}`.")
301
-
302
422
  dummyAssign = Make.Assign([Make.Name("dummyTarget")], Make.Constant(None))
303
423
  dummySubscript = Make.Subscript(Make.Name("dummy"), Make.Name("slice"))
304
424
  dummyTuple = Make.Tuple([Make.Name("dummyElement")])
305
425
 
426
+ # Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
306
427
  @dataclasses.dataclass
307
428
  class ShatteredDataclass:
308
- countingVariableAnnotation: ImaAnnotationType
429
+ countingVariableAnnotation: ast.expr
309
430
  """Type annotation for the counting variable extracted from the dataclass."""
431
+
310
432
  countingVariableName: ast.Name
311
433
  """AST name node representing the counting variable identifier."""
434
+
312
435
  field2AnnAssign: dict[ast_Identifier, ast.AnnAssign] = dataclasses.field(default_factory=dict)
313
436
  """Maps field names to their corresponding AST call expressions."""
437
+
314
438
  Z0Z_field2AnnAssign: dict[ast_Identifier, tuple[ast.AnnAssign, str]] = dataclasses.field(default_factory=dict)
439
+
315
440
  fragments4AssignmentOrParameters: ast.Tuple = dummyTuple
316
441
  """AST tuple used as target for assignment to capture returned fragments."""
442
+
317
443
  ledger: LedgerOfImports = dataclasses.field(default_factory=LedgerOfImports)
318
444
  """Import records for the dataclass and its constituent parts."""
445
+
319
446
  list_argAnnotated4ArgumentsSpecification: list[ast.arg] = dataclasses.field(default_factory=list)
320
447
  """Function argument nodes with annotations for parameter specification."""
448
+
321
449
  list_keyword_field__field4init: list[ast.keyword] = dataclasses.field(default_factory=list)
322
450
  """Keyword arguments for dataclass initialization with field=field format."""
323
- listAnnotations: list[ImaAnnotationType] = dataclasses.field(default_factory=list)
451
+
452
+ listAnnotations: list[ast.expr] = dataclasses.field(default_factory=list)
324
453
  """Type annotations for each dataclass field."""
454
+
325
455
  listName4Parameters: list[ast.Name] = dataclasses.field(default_factory=list)
326
456
  """Name nodes for each dataclass field used as function parameters."""
457
+
327
458
  listUnpack: list[ast.AnnAssign] = dataclasses.field(default_factory=list)
328
459
  """Annotated assignment statements to extract fields from dataclass."""
329
- map_stateDOTfield2Name: dict[ast.expr, ast.Name] = dataclasses.field(default_factory=dict)
460
+
461
+ map_stateDOTfield2Name: dict[ast.AST, ast.Name] = dataclasses.field(default_factory=dict)
330
462
  """Maps AST expressions to Name nodes for find-replace operations."""
463
+
331
464
  repack: ast.Assign = dummyAssign
332
465
  """AST assignment statement that reconstructs the original dataclass instance."""
466
+
333
467
  signatureReturnAnnotation: ast.Subscript = dummySubscript
334
468
  """tuple-based return type annotation for function definitions."""
@@ -1,62 +1,183 @@
1
- from collections.abc import Callable, Sequence
1
+ """
2
+ Core AST Traversal and Transformation Utilities for Python Code Manipulation
3
+
4
+ This module provides the foundation for traversing and modifying Python Abstract
5
+ Syntax Trees (ASTs). It contains two primary classes:
6
+
7
+ 1. NodeTourist: Implements the visitor pattern to traverse an AST and extract information
8
+ from nodes that match specific predicates without modifying the AST.
9
+
10
+ 2. NodeChanger: Extends ast.NodeTransformer to selectively transform AST nodes that
11
+ match specific predicates, enabling targeted code modifications.
12
+
13
+ The module also provides utilities for importing modules, loading callables from files,
14
+ and parsing Python code into AST structures, creating a complete workflow for code
15
+ analysis and transformation.
16
+ """
17
+
18
+ from collections.abc import Callable
2
19
  from inspect import getsource as inspect_getsource
3
- from mapFolding.someAssemblyRequired import ast_Identifier, str_nameDOTname, 个
20
+ from mapFolding.someAssemblyRequired import ast_Identifier, str_nameDOTname
4
21
  from os import PathLike
5
22
  from pathlib import Path, PurePath
6
23
  from types import ModuleType
7
- from typing import Any, cast, Generic, TypeGuard
24
+ from typing import Any, Literal
8
25
  import ast
9
26
  import importlib
10
27
  import importlib.util
11
28
 
12
29
  # TODO Identify the logic that narrows the type and can help the user during static type checking.
13
30
 
14
- class NodeTourist(ast.NodeVisitor, Generic[个]):
15
- def __init__(self, findThis: Callable[[个], TypeGuard[个] | bool], doThat: Callable[[个], 个 | None]) -> None:
16
- self.findThis = findThis
17
- self.doThat = doThat
18
- self.nodeCaptured: | None = None
19
-
20
- def visit(self, node: 个) -> None: # pyright: ignore [reportGeneralTypeIssues]
21
- if self.findThis(node):
22
- nodeActionReturn = self.doThat(node)
23
- if nodeActionReturn is not None:
24
- self.nodeCaptured = nodeActionReturn
25
- self.generic_visit(cast(ast.AST, node))
26
-
27
- def captureLastMatch(self, node: 个) -> 个 | None: # pyright: ignore [reportGeneralTypeIssues]
28
- self.nodeCaptured = None
29
- self.visit(node)
30
- return self.nodeCaptured
31
-
32
- class NodeChanger(ast.NodeTransformer, Generic[个]):
33
- def __init__(self, findThis: Callable[[个], bool], doThat: Callable[[个], Sequence[个] | 个 | None]) -> None:
34
- self.findThis = findThis
35
- self.doThat = doThat
36
-
37
- def visit(self, node: 个) -> Sequence[个] | 个 | None: # pyright: ignore [reportGeneralTypeIssues]
38
- if self.findThis(node):
39
- return self.doThat(node)
40
- return super().visit(cast(ast.AST, node))
31
+ class NodeTourist(ast.NodeVisitor):
32
+ """
33
+ Visit and extract information from AST nodes that match a predicate.
34
+
35
+ NodeTourist implements the visitor pattern to traverse an AST, applying
36
+ a predicate function to each node and capturing nodes or their attributes
37
+ when they match. Unlike NodeChanger, it doesn't modify the AST but collects
38
+ information during traversal.
39
+
40
+ This class is particularly useful for analyzing AST structures, extracting
41
+ specific nodes or node properties, and gathering information about code patterns.
42
+ """
43
+ def __init__(self, findThis: Callable[..., Any], doThat: Callable[..., Any]) -> None:
44
+ self.findThis = findThis
45
+ self.doThat = doThat
46
+ self.nodeCaptured: Any | None = None
47
+
48
+ def visit(self, node: ast.AST) -> None:
49
+ if self.findThis(node):
50
+ nodeActionReturn = self.doThat(node)
51
+ if nodeActionReturn is not None:
52
+ self.nodeCaptured = nodeActionReturn
53
+ self.generic_visit(node)
54
+
55
+ def captureLastMatch(self, node: ast.AST) -> Any | None:
56
+ self.nodeCaptured = None
57
+ self.visit(node)
58
+ return self.nodeCaptured
59
+
60
+ class NodeChanger(ast.NodeTransformer):
61
+ """
62
+ Transform AST nodes that match a predicate by applying a transformation function.
63
+
64
+ NodeChanger is an AST node transformer that applies a targeted transformation
65
+ to nodes matching a specific predicate. It traverses the AST and only modifies
66
+ nodes that satisfy the predicate condition, leaving other nodes unchanged.
67
+
68
+ This class extends ast.NodeTransformer and implements the visitor pattern
69
+ to systematically process and transform an AST tree.
70
+ """
71
+ def __init__(self, findThis: Callable[..., Any], doThat: Callable[..., Any]) -> None:
72
+ self.findThis = findThis
73
+ self.doThat = doThat
74
+
75
+ def visit(self, node: ast.AST) -> ast.AST:
76
+ if self.findThis(node):
77
+ return self.doThat(node)
78
+ return super().visit(node)
41
79
 
42
80
  def importLogicalPath2Callable(logicalPathModule: str_nameDOTname, identifier: ast_Identifier, packageIdentifierIfRelative: ast_Identifier | None = None) -> Callable[..., Any]:
43
- moduleImported: ModuleType = importlib.import_module(logicalPathModule, packageIdentifierIfRelative)
44
- return getattr(moduleImported, identifier)
81
+ """
82
+ Import a callable object (function or class) from a module based on its logical path.
83
+
84
+ This function imports a module using `importlib.import_module()` and then retrieves
85
+ a specific attribute (function, class, or other object) from that module.
86
+
87
+ Parameters
88
+ ----------
89
+ logicalPathModule : str
90
+ The logical path to the module, using dot notation (e.g., 'package.subpackage.module').
91
+ identifier : str
92
+ The name of the callable object to retrieve from the module.
93
+ packageIdentifierIfRelative : str, optional
94
+ The package name to use as the anchor point if `logicalPathModule` is a relative import.
95
+ If None, absolute import is assumed.
96
+
97
+ Returns
98
+ -------
99
+ Callable[..., Any]
100
+ The callable object (function, class, etc.) retrieved from the module.
101
+ """
102
+ moduleImported: ModuleType = importlib.import_module(logicalPathModule, packageIdentifierIfRelative)
103
+ return getattr(moduleImported, identifier)
45
104
 
46
105
  def importPathFilename2Callable(pathFilename: PathLike[Any] | PurePath, identifier: ast_Identifier, moduleIdentifier: ast_Identifier | None = None) -> Callable[..., Any]:
47
- pathFilename = Path(pathFilename)
106
+ """
107
+ Load a callable (function, class, etc.) from a Python file.
108
+ This function imports a specified Python file as a module, extracts a callable object
109
+ from it by name, and returns that callable.
110
+ Parameters
111
+ ----------
112
+ pathFilename : Union[PathLike[Any], PurePath]
113
+ Path to the Python file to import.
114
+ identifier : str
115
+ Name of the callable to extract from the imported module.
116
+ moduleIdentifier : Optional[str]
117
+ Name to use for the imported module. If None, the filename stem is used.
118
+ Returns
119
+ -------
120
+ Callable[..., Any]
121
+ The callable object extracted from the imported module.
122
+ Raises
123
+ ------
124
+ ImportError
125
+ If the file cannot be imported or the importlib specification is invalid.
126
+ AttributeError
127
+ If the identifier does not exist in the imported module.
128
+ """
129
+ pathFilename = Path(pathFilename)
130
+
131
+ importlibSpecification = importlib.util.spec_from_file_location(moduleIdentifier or pathFilename.stem, pathFilename)
132
+ if importlibSpecification is None or importlibSpecification.loader is None: raise ImportError(f"I received\n\t`{pathFilename = }`,\n\t`{identifier = }`, and\n\t`{moduleIdentifier = }`.\n\tAfter loading, \n\t`importlibSpecification` {'is `None`' if importlibSpecification is None else 'has a value'} and\n\t`importlibSpecification.loader` is unknown.")
133
+
134
+ moduleImported_jk_hahaha: ModuleType = importlib.util.module_from_spec(importlibSpecification)
135
+ importlibSpecification.loader.exec_module(moduleImported_jk_hahaha)
136
+ return getattr(moduleImported_jk_hahaha, identifier)
137
+
138
+ def parseLogicalPath2astModule(logicalPathModule: str_nameDOTname, packageIdentifierIfRelative: ast_Identifier|None=None, mode: Literal['exec'] = 'exec') -> ast.Module:
139
+ """
140
+ Parse a logical Python module path into an AST Module.
141
+
142
+ This function imports a module using its logical path (e.g., 'package.subpackage.module')
143
+ and converts its source code into an Abstract Syntax Tree (AST) Module object.
144
+
145
+ Parameters
146
+ ----------
147
+ logicalPathModule : str
148
+ The logical path to the module using dot notation (e.g., 'package.module').
149
+ packageIdentifierIfRelative : ast.Identifier or None, optional
150
+ The package identifier to use if the module path is relative, defaults to None.
151
+ mode : Literal['exec'], optional
152
+ The parsing mode to use, defaults to 'exec'.
153
+
154
+ Returns
155
+ -------
156
+ ast.Module
157
+ An AST Module object representing the parsed source code of the imported module.
158
+ """
159
+ moduleImported: ModuleType = importlib.import_module(logicalPathModule, packageIdentifierIfRelative)
160
+ sourcePython: str = inspect_getsource(moduleImported)
161
+ return ast.parse(sourcePython, mode=mode)
48
162
 
49
- importlibSpecification = importlib.util.spec_from_file_location(moduleIdentifier or pathFilename.stem, pathFilename)
50
- if importlibSpecification is None or importlibSpecification.loader is None: raise ImportError(f"I received\n\t`{pathFilename = }`,\n\t`{identifier = }`, and\n\t`{moduleIdentifier = }`.\n\tAfter loading, \n\t`importlibSpecification` {'is `None`' if importlibSpecification is None else 'has a value'} and\n\t`importlibSpecification.loader` is unknown.")
163
+ def parsePathFilename2astModule(pathFilename: PathLike[Any] | PurePath, mode: Literal['exec'] = 'exec') -> ast.Module:
164
+ """
165
+ Parse a file from a given path into an ast.Module.
51
166
 
52
- moduleImported_jk_hahaha: ModuleType = importlib.util.module_from_spec(importlibSpecification)
53
- importlibSpecification.loader.exec_module(moduleImported_jk_hahaha)
54
- return getattr(moduleImported_jk_hahaha, identifier)
167
+ This function reads the content of a file specified by `pathFilename` and parses it into an
168
+ Abstract Syntax Tree (AST) Module using Python's ast module.
55
169
 
56
- def parseLogicalPath2astModule(logicalPathModule: str_nameDOTname, packageIdentifierIfRelative: ast_Identifier|None=None, mode:str='exec') -> ast.AST:
57
- moduleImported: ModuleType = importlib.import_module(logicalPathModule, packageIdentifierIfRelative)
58
- sourcePython: str = inspect_getsource(moduleImported)
59
- return ast.parse(sourcePython, mode=mode)
170
+ Parameters
171
+ ----------
172
+ pathFilename : PathLike[Any] | PurePath
173
+ The path to the file to be parsed. Can be a string path, PathLike object, or PurePath object.
174
+ mode : Literal['exec'], optional
175
+ The mode parameter for ast.parse. Default is 'exec'.
176
+ Options are 'exec', 'eval', or 'single'. See ast.parse documentation for details.
60
177
 
61
- def parsePathFilename2astModule(pathFilename: PathLike[Any] | PurePath, mode:str='exec') -> ast.AST:
62
- return ast.parse(Path(pathFilename).read_text(), mode=mode)
178
+ Returns
179
+ -------
180
+ ast.Module
181
+ The parsed abstract syntax tree module.
182
+ """
183
+ return ast.parse(Path(pathFilename).read_text(), mode=mode)