mapFolding 0.8.6__py3-none-any.whl → 0.9.1__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 (34) hide show
  1. mapFolding/__init__.py +60 -13
  2. mapFolding/basecamp.py +32 -17
  3. mapFolding/beDRY.py +4 -5
  4. mapFolding/oeis.py +94 -7
  5. mapFolding/someAssemblyRequired/RecipeJob.py +103 -0
  6. mapFolding/someAssemblyRequired/__init__.py +71 -50
  7. mapFolding/someAssemblyRequired/_theTypes.py +11 -15
  8. mapFolding/someAssemblyRequired/_tool_Make.py +36 -9
  9. mapFolding/someAssemblyRequired/_tool_Then.py +59 -25
  10. mapFolding/someAssemblyRequired/_toolboxAntecedents.py +159 -272
  11. mapFolding/someAssemblyRequired/_toolboxContainers.py +155 -70
  12. mapFolding/someAssemblyRequired/_toolboxPython.py +168 -44
  13. mapFolding/someAssemblyRequired/synthesizeNumbaJob.py +154 -39
  14. mapFolding/someAssemblyRequired/toolboxNumba.py +72 -230
  15. mapFolding/someAssemblyRequired/transformationTools.py +370 -141
  16. mapFolding/syntheticModules/{numbaCount_doTheNeedful.py → numbaCount.py} +7 -4
  17. mapFolding/theDao.py +19 -16
  18. mapFolding/theSSOT.py +165 -62
  19. mapFolding/toolboxFilesystem.py +1 -1
  20. mapfolding-0.9.1.dist-info/METADATA +177 -0
  21. mapfolding-0.9.1.dist-info/RECORD +47 -0
  22. tests/__init__.py +44 -0
  23. tests/conftest.py +75 -7
  24. tests/test_computations.py +92 -10
  25. tests/test_filesystem.py +32 -33
  26. tests/test_other.py +0 -1
  27. tests/test_tasks.py +1 -1
  28. mapFolding/someAssemblyRequired/newInliner.py +0 -22
  29. mapfolding-0.8.6.dist-info/METADATA +0 -190
  30. mapfolding-0.8.6.dist-info/RECORD +0 -47
  31. {mapfolding-0.8.6.dist-info → mapfolding-0.9.1.dist-info}/WHEEL +0 -0
  32. {mapfolding-0.8.6.dist-info → mapfolding-0.9.1.dist-info}/entry_points.txt +0 -0
  33. {mapfolding-0.8.6.dist-info → mapfolding-0.9.1.dist-info}/licenses/LICENSE +0 -0
  34. {mapfolding-0.8.6.dist-info → mapfolding-0.9.1.dist-info}/top_level.txt +0 -0
@@ -1,23 +1,54 @@
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
- from typing import Literal
14
28
  from Z0Z_tools import updateExtendPolishDictionaryLists
15
29
  import ast
16
30
  import dataclasses
17
31
 
18
32
  class LedgerOfImports:
33
+ """
34
+ Track and manage import statements for programmatically generated code.
35
+
36
+ LedgerOfImports acts as a registry for import statements, maintaining a clean
37
+ separation between the logical structure of imports and their textual representation.
38
+ It enables:
39
+
40
+ 1. Tracking regular imports and import-from statements
41
+ 2. Adding imports programmatically during code transformation
42
+ 3. Merging imports from multiple sources
43
+ 4. Removing unnecessary or conflicting imports
44
+ 5. Generating optimized AST import nodes for the final code
45
+
46
+ This class forms the foundation of dependency management in generated code,
47
+ ensuring that all required libraries are available without duplication or
48
+ conflict.
49
+ """
19
50
  # TODO When resolving the ledger of imports, remove self-referential imports
20
- # TODO TypeIgnore :/
51
+ # TODO add TypeIgnore tracking to the ledger of imports
21
52
 
22
53
  def __init__(self, startWith: ast.AST | None = None) -> None:
23
54
  self.dictionaryImportFrom: dict[str_nameDOTname, list[tuple[ast_Identifier, ast_Identifier | None]]] = defaultdict(list)
@@ -26,43 +57,40 @@ class LedgerOfImports:
26
57
  self.walkThis(startWith)
27
58
 
28
59
  def addAst(self, astImport____: ast.Import | ast.ImportFrom) -> None:
29
- assert isinstance(astImport____, (ast.Import, ast.ImportFrom)), f"I received {type(astImport____) = }, but I can only accept {ast.Import} and {ast.ImportFrom}."
30
- if be.Import(astImport____):
31
- for alias in astImport____.names:
32
- self.listImport.append(alias.name)
33
- elif be.ImportFrom(astImport____):
34
- # TODO fix the mess created by `None` means '.'. I need a `str_nameDOTname` to replace '.'
35
- if astImport____.module is None:
36
- astImport____.module = '.'
37
- for alias in astImport____.names:
38
- self.dictionaryImportFrom[astImport____.module].append((alias.name, alias.asname))
60
+ match astImport____:
61
+ case ast.Import():
62
+ for alias in astImport____.names:
63
+ self.listImport.append(alias.name)
64
+ case ast.ImportFrom():
65
+ # TODO fix the mess created by `None` means '.'. I need a `str_nameDOTname` to replace '.'
66
+ if astImport____.module is None:
67
+ astImport____.module = '.'
68
+ for alias in astImport____.names:
69
+ self.dictionaryImportFrom[astImport____.module].append((alias.name, alias.asname))
70
+ case _:
71
+ raise ValueError(f"I received {type(astImport____) = }, but I can only accept {ast.Import} and {ast.ImportFrom}.")
39
72
 
40
73
  def addImport_asStr(self, moduleWithLogicalPath: str_nameDOTname) -> None:
41
74
  self.listImport.append(moduleWithLogicalPath)
42
75
 
43
- # def addImportFrom_asStr(self, moduleWithLogicalPath: str_nameDOTname, name: ast_Identifier, asname: ast_Identifier | None = None) -> None:
44
- # self.dictionaryImportFrom[moduleWithLogicalPath].append((name, asname))
45
-
46
76
  def addImportFrom_asStr(self, moduleWithLogicalPath: str_nameDOTname, name: ast_Identifier, asname: ast_Identifier | None = None) -> None:
47
77
  if moduleWithLogicalPath not in self.dictionaryImportFrom:
48
78
  self.dictionaryImportFrom[moduleWithLogicalPath] = []
49
79
  self.dictionaryImportFrom[moduleWithLogicalPath].append((name, asname))
50
80
 
51
81
  def removeImportFromModule(self, moduleWithLogicalPath: str_nameDOTname) -> None:
52
- self.removeImportFrom(moduleWithLogicalPath, None, None)
53
82
  """Remove all imports from a specific module."""
83
+ self.removeImportFrom(moduleWithLogicalPath, None, None)
54
84
 
55
85
  def removeImportFrom(self, moduleWithLogicalPath: str_nameDOTname, name: ast_Identifier | None, asname: ast_Identifier | None = None) -> None:
56
- if moduleWithLogicalPath is None:
57
- raise SyntaxError(f"I received `{moduleWithLogicalPath = }`, but it must be the name of a module.")
86
+ """
87
+ name, asname Action
88
+ None, None : remove all matches for the module
89
+ ast_Identifier, ast_Identifier : remove exact matches
90
+ ast_Identifier, None : remove exact matches
91
+ None, ast_Identifier : remove all matches for asname and if entry_asname is None remove name == ast_Identifier
92
+ """
58
93
  if moduleWithLogicalPath in self.dictionaryImportFrom:
59
- """
60
- name, asname Meaning
61
- ast_Identifier, ast_Identifier : remove exact matches
62
- ast_Identifier, None : remove exact matches
63
- None, ast_Identifier : remove all matches for asname and if entry_asname is None remove name == ast_Identifier
64
- None, None : remove all matches for the module
65
- """
66
94
  if name is None and asname is None:
67
95
  # Remove all entries for the module
68
96
  self.dictionaryImportFrom.pop(moduleWithLogicalPath)
@@ -71,7 +99,6 @@ class LedgerOfImports:
71
99
  self.dictionaryImportFrom[moduleWithLogicalPath] = [(entry_name, entry_asname) for entry_name, entry_asname in self.dictionaryImportFrom[moduleWithLogicalPath]
72
100
  if not (entry_asname == asname) and not (entry_asname is None and entry_name == asname)]
73
101
  else:
74
- # Remove exact matches for the module
75
102
  self.dictionaryImportFrom[moduleWithLogicalPath] = [(entry_name, entry_asname) for entry_name, entry_asname in self.dictionaryImportFrom[moduleWithLogicalPath]
76
103
  if not (entry_name == name and entry_asname == asname)]
77
104
  if not self.dictionaryImportFrom[moduleWithLogicalPath]:
@@ -108,20 +135,55 @@ class LedgerOfImports:
108
135
  if isinstance(nodeBuffalo, (ast.Import, ast.ImportFrom)):
109
136
  self.addAst(nodeBuffalo)
110
137
 
138
+ # Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
111
139
  @dataclasses.dataclass
112
140
  class IngredientsFunction:
113
- """Everything necessary to integrate a function into a module should be here.
141
+ """
142
+ Package a function definition with its import dependencies for code generation.
143
+
144
+ IngredientsFunction encapsulates an AST function definition along with all the
145
+ imports required for that function to operate correctly. This creates a modular,
146
+ portable unit that can be:
147
+
148
+ 1. Transformed independently (e.g., by applying Numba decorators)
149
+ 2. Transplanted between modules while maintaining dependencies
150
+ 3. Combined with other functions to form complete modules
151
+ 4. Analyzed for optimization opportunities
152
+
153
+ This class forms the primary unit of function manipulation in the code generation
154
+ system, enabling targeted transformations while preserving function dependencies.
155
+
114
156
  Parameters:
115
- astFunctionDef: hint `Make.astFunctionDef()`
157
+ astFunctionDef: The AST representation of the function definition
158
+ imports: Import statements needed by the function
159
+ type_ignores: Type ignore comments associated with the function
116
160
  """
117
161
  astFunctionDef: ast.FunctionDef
118
162
  imports: LedgerOfImports = dataclasses.field(default_factory=LedgerOfImports)
119
163
  type_ignores: list[ast.TypeIgnore] = dataclasses.field(default_factory=list)
120
164
 
165
+ # Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
121
166
  @dataclasses.dataclass
122
167
  class IngredientsModule:
123
- """Everything necessary to create one _logical_ `ast.Module` should be here.
124
- Extrinsic qualities should _probably_ be handled externally.
168
+ """
169
+ Assemble a complete Python module from its constituent AST components.
170
+
171
+ IngredientsModule provides a structured container for all elements needed to
172
+ generate a complete Python module, including:
173
+
174
+ 1. Import statements aggregated from all module components
175
+ 2. Prologue code that runs before function definitions
176
+ 3. Function definitions with their dependencies
177
+ 4. Epilogue code that runs after function definitions
178
+ 5. Entry point code executed when the module runs as a script
179
+ 6. Type ignores and other annotations
180
+
181
+ This class enables programmatic assembly of Python modules with a clear
182
+ separation between different structural elements, while maintaining the
183
+ proper ordering and relationships between components.
184
+
185
+ The modular design allows transformations to be applied to specific parts
186
+ of a module while preserving the overall structure.
125
187
 
126
188
  Parameters:
127
189
  ingredientsFunction (None): One or more `IngredientsFunction` that will appended to `listIngredientsFunctions`.
@@ -160,7 +222,7 @@ class IngredientsModule:
160
222
  """Append one or more statements to `prologue`."""
161
223
  list_body: list[ast.stmt] = []
162
224
  listTypeIgnore: list[ast.TypeIgnore] = []
163
- if astModule is not None and be.Module(astModule):
225
+ if astModule is not None and isinstance(astModule, ast.Module): # type: ignore
164
226
  list_body.extend(astModule.body)
165
227
  listTypeIgnore.extend(astModule.type_ignores)
166
228
  if type_ignores is not None:
@@ -189,10 +251,8 @@ class IngredientsModule:
189
251
  def appendIngredientsFunction(self, *ingredientsFunction: IngredientsFunction) -> None:
190
252
  """Append one or more `IngredientsFunction`."""
191
253
  for allegedIngredientsFunction in ingredientsFunction:
192
- if isinstance(allegedIngredientsFunction, IngredientsFunction):
193
- self.listIngredientsFunctions.append(allegedIngredientsFunction)
194
- else:
195
- raise ValueError(f"I received `{type(allegedIngredientsFunction) = }`, but I can only accept `{IngredientsFunction}`.")
254
+ assert isinstance(allegedIngredientsFunction, IngredientsFunction), ValueError(f"I received `{type(allegedIngredientsFunction) = }`, but I can only accept `{IngredientsFunction}`.")
255
+ self.listIngredientsFunctions.append(allegedIngredientsFunction)
196
256
 
197
257
  def removeImportFromModule(self, moduleWithLogicalPath: str_nameDOTname) -> None:
198
258
  self.removeImportFrom(moduleWithLogicalPath, None, None)
@@ -201,7 +261,7 @@ class IngredientsModule:
201
261
  def removeImportFrom(self, moduleWithLogicalPath: str_nameDOTname, name: ast_Identifier | None, asname: ast_Identifier | None = None) -> None:
202
262
  """
203
263
  This method modifies all `LedgerOfImports` in this `IngredientsModule` and all `IngredientsFunction` in `listIngredientsFunctions`.
204
- It is not a "blacklist", so the import from could be added after this modification.
264
+ It is not a "blacklist", so the `import from` could be added after this modification.
205
265
  """
206
266
  self.imports.removeImportFrom(moduleWithLogicalPath, name, asname)
207
267
  for ingredientsFunction in self.listIngredientsFunctions:
@@ -240,13 +300,33 @@ class IngredientsModule:
240
300
  listTypeIgnore.extend(self.launcher.type_ignores)
241
301
  return listTypeIgnore
242
302
 
303
+ # Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
243
304
  @dataclasses.dataclass
244
305
  class RecipeSynthesizeFlow:
245
- """Settings for synthesizing flow."""
306
+ """
307
+ Configure the generation of new modules, including Numba-accelerated code modules.
308
+
309
+ RecipeSynthesizeFlow defines the complete blueprint for transforming an original
310
+ Python algorithm into an optimized, accelerated implementation. It specifies:
311
+
312
+ 1. Source code locations and identifiers
313
+ 2. Target code locations and identifiers
314
+ 3. Naming conventions for generated modules and functions
315
+ 4. File system paths for output files
316
+ 5. Import relationships between components
317
+
318
+ This configuration class serves as a single source of truth for the code generation
319
+ process, ensuring consistency across all generated artifacts while enabling
320
+ customization of the transformation pipeline.
321
+
322
+ The transformation process uses this configuration to extract functions from the
323
+ source module, transform them according to optimization rules, and output
324
+ properly structured optimized modules with all necessary imports.
325
+ """
246
326
  # ========================================
247
327
  # Source
248
- # ========================================
249
- source_astModule = parseLogicalPath2astModule(The.logicalPathModuleSourceAlgorithm)
328
+ source_astModule: ast.Module = parseLogicalPath2astModule(The.logicalPathModuleSourceAlgorithm)
329
+ """AST of the source algorithm module containing the original implementation."""
250
330
 
251
331
  # Figure out dynamic flow control to synthesized modules https://github.com/hunterhogan/mapFolding/issues/4
252
332
  sourceCallableDispatcher: ast_Identifier = The.sourceCallableDispatcher
@@ -274,7 +354,7 @@ class RecipeSynthesizeFlow:
274
354
  """ `logicalPathFlowRoot` likely corresponds to a physical filesystem directory."""
275
355
 
276
356
  # Module ================================
277
- moduleDispatcher: ast_Identifier = 'numbaCount_doTheNeedful'
357
+ moduleDispatcher: ast_Identifier = 'numbaCount'
278
358
  moduleInitialize: ast_Identifier = moduleDispatcher
279
359
  moduleParallel: ast_Identifier = moduleDispatcher
280
360
  moduleSequential: ast_Identifier = moduleDispatcher
@@ -292,20 +372,21 @@ class RecipeSynthesizeFlow:
292
372
  dataclassInstance: ast_Identifier = sourceDataclassInstance
293
373
  dataclassInstanceTaskDistribution: ast_Identifier = sourceDataclassInstanceTaskDistribution
294
374
 
375
+ removeDataclassDispatcher: bool = False
376
+ removeDataclassInitialize: bool = False
377
+ removeDataclassParallel: bool = True
378
+ removeDataclassSequential: bool = True
295
379
  # ========================================
296
380
  # Computed
297
- # ========================================
298
- """
299
- theFormatStrModuleSynthetic = "{packageFlow}Count"
300
- theFormatStrModuleForCallableSynthetic = theFormatStrModuleSynthetic + "_{callableTarget}"
301
- theModuleDispatcherSynthetic: ast_Identifier = theFormatStrModuleForCallableSynthetic.format(packageFlow=packageFlowSynthetic, callableTarget=The.sourceCallableDispatcher)
302
- theLogicalPathModuleDispatcherSynthetic: str = '.'.join([The.packageName, The.moduleOfSyntheticModules, theModuleDispatcherSynthetic])
303
-
304
- """
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])
305
386
  # logicalPathModuleDispatcher: str = '.'.join([Z0Z_flowLogicalPathRoot, moduleDispatcher])
387
+
306
388
  # ========================================
307
389
  # Filesystem (names of physical objects)
308
- # ========================================
309
390
  pathPackage: PurePosixPath | None = PurePosixPath(The.pathPackage)
310
391
  fileExtension: str = The.fileExtension
311
392
 
@@ -338,46 +419,50 @@ theLogicalPathModuleDispatcherSynthetic: str = '.'.join([The.packageName, The.mo
338
419
  def pathFilenameSequential(self) -> PurePosixPath:
339
420
  return self._makePathFilename(filenameStem=self.moduleSequential, logicalPathINFIX=self.logicalPathFlowRoot)
340
421
 
341
- def __post_init__(self) -> None:
342
- if ((self.concurrencyManagerIdentifier is not None and self.concurrencyManagerIdentifier != self.sourceConcurrencyManagerIdentifier) # `submit` # type: ignore
343
- or ((self.concurrencyManagerIdentifier is None) != (self.concurrencyManagerNamespace is None))): # type: ignore
344
- import warnings
345
- 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.
346
-
347
- # self.logicalPathModuleDispatcher!=logicalPathModuleDispatcherHARDCODED or
348
- if self.callableDispatcher!=callableDispatcherHARDCODED:
349
- print(f"fyi: `{self.callableDispatcher=}` but\n\t`{callableDispatcherHARDCODED=}`.")
350
-
351
422
  dummyAssign = Make.Assign([Make.Name("dummyTarget")], Make.Constant(None))
352
423
  dummySubscript = Make.Subscript(Make.Name("dummy"), Make.Name("slice"))
353
424
  dummyTuple = Make.Tuple([Make.Name("dummyElement")])
354
425
 
426
+ # Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
355
427
  @dataclasses.dataclass
356
428
  class ShatteredDataclass:
357
- countingVariableAnnotation: ImaAnnotationType
429
+ countingVariableAnnotation: ast.expr
358
430
  """Type annotation for the counting variable extracted from the dataclass."""
431
+
359
432
  countingVariableName: ast.Name
360
433
  """AST name node representing the counting variable identifier."""
361
- field2AnnAssign: dict[ast_Identifier, ast.AnnAssign] = dataclasses.field(default_factory=dict)
434
+
435
+ field2AnnAssign: dict[ast_Identifier, ast.AnnAssign | ast.Assign] = dataclasses.field(default_factory=dict)
362
436
  """Maps field names to their corresponding AST call expressions."""
363
- Z0Z_field2AnnAssign: dict[ast_Identifier, tuple[ast.AnnAssign, str]] = dataclasses.field(default_factory=dict)
437
+
438
+ Z0Z_field2AnnAssign: dict[ast_Identifier, tuple[ast.AnnAssign | ast.Assign, str]] = dataclasses.field(default_factory=dict)
439
+
364
440
  fragments4AssignmentOrParameters: ast.Tuple = dummyTuple
365
441
  """AST tuple used as target for assignment to capture returned fragments."""
442
+
366
443
  ledger: LedgerOfImports = dataclasses.field(default_factory=LedgerOfImports)
367
444
  """Import records for the dataclass and its constituent parts."""
445
+
368
446
  list_argAnnotated4ArgumentsSpecification: list[ast.arg] = dataclasses.field(default_factory=list)
369
447
  """Function argument nodes with annotations for parameter specification."""
448
+
370
449
  list_keyword_field__field4init: list[ast.keyword] = dataclasses.field(default_factory=list)
371
450
  """Keyword arguments for dataclass initialization with field=field format."""
372
- listAnnotations: list[ImaAnnotationType] = dataclasses.field(default_factory=list)
451
+
452
+ listAnnotations: list[ast.expr] = dataclasses.field(default_factory=list)
373
453
  """Type annotations for each dataclass field."""
454
+
374
455
  listName4Parameters: list[ast.Name] = dataclasses.field(default_factory=list)
375
456
  """Name nodes for each dataclass field used as function parameters."""
457
+
376
458
  listUnpack: list[ast.AnnAssign] = dataclasses.field(default_factory=list)
377
459
  """Annotated assignment statements to extract fields from dataclass."""
378
- 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)
379
462
  """Maps AST expressions to Name nodes for find-replace operations."""
463
+
380
464
  repack: ast.Assign = dummyAssign
381
465
  """AST assignment statement that reconstructs the original dataclass instance."""
466
+
382
467
  signatureReturnAnnotation: ast.Subscript = dummySubscript
383
468
  """tuple-based return type annotation for function definitions."""
@@ -1,62 +1,186 @@
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
+
111
+ Parameters
112
+ ----------
113
+ pathFilename : Union[PathLike[Any], PurePath]
114
+ Path to the Python file to import.
115
+ identifier : str
116
+ Name of the callable to extract from the imported module.
117
+ moduleIdentifier : Optional[str]
118
+ Name to use for the imported module. If None, the filename stem is used.
119
+
120
+ Returns
121
+ -------
122
+ Callable[..., Any]
123
+ The callable object extracted from the imported module.
124
+
125
+ Raises
126
+ ------
127
+ ImportError
128
+ If the file cannot be imported or the importlib specification is invalid.
129
+ AttributeError
130
+ If the identifier does not exist in the imported module.
131
+ """
132
+ pathFilename = Path(pathFilename)
133
+
134
+ importlibSpecification = importlib.util.spec_from_file_location(moduleIdentifier or pathFilename.stem, pathFilename)
135
+ 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.")
136
+
137
+ moduleImported_jk_hahaha: ModuleType = importlib.util.module_from_spec(importlibSpecification)
138
+ importlibSpecification.loader.exec_module(moduleImported_jk_hahaha)
139
+ return getattr(moduleImported_jk_hahaha, identifier)
140
+
141
+ def parseLogicalPath2astModule(logicalPathModule: str_nameDOTname, packageIdentifierIfRelative: ast_Identifier|None=None, mode: Literal['exec'] = 'exec') -> ast.Module:
142
+ """
143
+ Parse a logical Python module path into an AST Module.
144
+
145
+ This function imports a module using its logical path (e.g., 'package.subpackage.module')
146
+ and converts its source code into an Abstract Syntax Tree (AST) Module object.
147
+
148
+ Parameters
149
+ ----------
150
+ logicalPathModule : str
151
+ The logical path to the module using dot notation (e.g., 'package.module').
152
+ packageIdentifierIfRelative : ast.Identifier or None, optional
153
+ The package identifier to use if the module path is relative, defaults to None.
154
+ mode : Literal['exec'], optional
155
+ The parsing mode to use, defaults to 'exec'.
156
+
157
+ Returns
158
+ -------
159
+ ast.Module
160
+ An AST Module object representing the parsed source code of the imported module.
161
+ """
162
+ moduleImported: ModuleType = importlib.import_module(logicalPathModule, packageIdentifierIfRelative)
163
+ sourcePython: str = inspect_getsource(moduleImported)
164
+ return ast.parse(sourcePython, mode=mode)
48
165
 
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.")
166
+ def parsePathFilename2astModule(pathFilename: PathLike[Any] | PurePath, mode: Literal['exec'] = 'exec') -> ast.Module:
167
+ """
168
+ Parse a file from a given path into an ast.Module.
51
169
 
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)
170
+ This function reads the content of a file specified by `pathFilename` and parses it into an
171
+ Abstract Syntax Tree (AST) Module using Python's ast module.
55
172
 
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)
173
+ Parameters
174
+ ----------
175
+ pathFilename : PathLike[Any] | PurePath
176
+ The path to the file to be parsed. Can be a string path, PathLike object, or PurePath object.
177
+ mode : Literal['exec'], optional
178
+ The mode parameter for ast.parse. Default is 'exec'.
179
+ Options are 'exec', 'eval', or 'single'. See ast.parse documentation for details.
60
180
 
61
- def parsePathFilename2astModule(pathFilename: PathLike[Any] | PurePath, mode:str='exec') -> ast.AST:
62
- return ast.parse(Path(pathFilename).read_text(), mode=mode)
181
+ Returns
182
+ -------
183
+ ast.Module
184
+ The parsed abstract syntax tree module.
185
+ """
186
+ return ast.parse(Path(pathFilename).read_text(), mode=mode)