mapFolding 0.8.6__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 (31) hide show
  1. mapFolding/__init__.py +60 -13
  2. mapFolding/basecamp.py +32 -17
  3. mapFolding/beDRY.py +3 -3
  4. mapFolding/oeis.py +83 -2
  5. mapFolding/someAssemblyRequired/__init__.py +48 -27
  6. mapFolding/someAssemblyRequired/_theTypes.py +11 -15
  7. mapFolding/someAssemblyRequired/_tool_Make.py +35 -8
  8. mapFolding/someAssemblyRequired/_tool_Then.py +59 -25
  9. mapFolding/someAssemblyRequired/_toolboxAntecedents.py +151 -276
  10. mapFolding/someAssemblyRequired/_toolboxContainers.py +133 -48
  11. mapFolding/someAssemblyRequired/_toolboxPython.py +165 -44
  12. mapFolding/someAssemblyRequired/synthesizeNumbaJob.py +101 -18
  13. mapFolding/someAssemblyRequired/toolboxNumba.py +83 -48
  14. mapFolding/someAssemblyRequired/transformationTools.py +220 -138
  15. mapFolding/theSSOT.py +147 -54
  16. mapFolding/toolboxFilesystem.py +1 -1
  17. mapfolding-0.9.0.dist-info/METADATA +177 -0
  18. mapfolding-0.9.0.dist-info/RECORD +46 -0
  19. tests/__init__.py +44 -0
  20. tests/conftest.py +75 -7
  21. tests/test_computations.py +90 -9
  22. tests/test_filesystem.py +32 -33
  23. tests/test_other.py +0 -1
  24. tests/test_tasks.py +1 -1
  25. mapFolding/someAssemblyRequired/newInliner.py +0 -22
  26. mapfolding-0.8.6.dist-info/METADATA +0 -190
  27. mapfolding-0.8.6.dist-info/RECORD +0 -47
  28. {mapfolding-0.8.6.dist-info → mapfolding-0.9.0.dist-info}/WHEEL +0 -0
  29. {mapfolding-0.8.6.dist-info → mapfolding-0.9.0.dist-info}/entry_points.txt +0 -0
  30. {mapfolding-0.8.6.dist-info → mapfolding-0.9.0.dist-info}/licenses/LICENSE +0 -0
  31. {mapfolding-0.8.6.dist-info → mapfolding-0.9.0.dist-info}/top_level.txt +0 -0
@@ -1,23 +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
- from typing import Literal
14
28
  from Z0Z_tools import updateExtendPolishDictionaryLists
15
29
  import ast
16
30
  import dataclasses
17
31
 
32
+ # Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
18
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
+ """
19
51
  # TODO When resolving the ledger of imports, remove self-referential imports
20
- # TODO TypeIgnore :/
52
+ # TODO add TypeIgnore tracking to the ledger of imports
21
53
 
22
54
  def __init__(self, startWith: ast.AST | None = None) -> None:
23
55
  self.dictionaryImportFrom: dict[str_nameDOTname, list[tuple[ast_Identifier, ast_Identifier | None]]] = defaultdict(list)
@@ -27,10 +59,10 @@ class LedgerOfImports:
27
59
 
28
60
  def addAst(self, astImport____: ast.Import | ast.ImportFrom) -> None:
29
61
  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____):
62
+ if isinstance(astImport____, ast.Import):
31
63
  for alias in astImport____.names:
32
64
  self.listImport.append(alias.name)
33
- elif be.ImportFrom(astImport____):
65
+ elif isinstance(astImport____, ast.ImportFrom): # type: ignore
34
66
  # TODO fix the mess created by `None` means '.'. I need a `str_nameDOTname` to replace '.'
35
67
  if astImport____.module is None:
36
68
  astImport____.module = '.'
@@ -49,12 +81,11 @@ class LedgerOfImports:
49
81
  self.dictionaryImportFrom[moduleWithLogicalPath].append((name, asname))
50
82
 
51
83
  def removeImportFromModule(self, moduleWithLogicalPath: str_nameDOTname) -> None:
52
- self.removeImportFrom(moduleWithLogicalPath, None, None)
53
84
  """Remove all imports from a specific module."""
85
+ self.removeImportFrom(moduleWithLogicalPath, None, None)
54
86
 
55
87
  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.")
88
+ assert moduleWithLogicalPath is not None, SyntaxError(f"I received `{moduleWithLogicalPath = }`, but it must be the name of a module.")
58
89
  if moduleWithLogicalPath in self.dictionaryImportFrom:
59
90
  """
60
91
  name, asname Meaning
@@ -108,20 +139,55 @@ class LedgerOfImports:
108
139
  if isinstance(nodeBuffalo, (ast.Import, ast.ImportFrom)):
109
140
  self.addAst(nodeBuffalo)
110
141
 
142
+ # Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
111
143
  @dataclasses.dataclass
112
144
  class IngredientsFunction:
113
- """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
+
114
160
  Parameters:
115
- 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
116
164
  """
117
165
  astFunctionDef: ast.FunctionDef
118
166
  imports: LedgerOfImports = dataclasses.field(default_factory=LedgerOfImports)
119
167
  type_ignores: list[ast.TypeIgnore] = dataclasses.field(default_factory=list)
120
168
 
169
+ # Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
121
170
  @dataclasses.dataclass
122
171
  class IngredientsModule:
123
- """Everything necessary to create one _logical_ `ast.Module` should be here.
124
- 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.
125
191
 
126
192
  Parameters:
127
193
  ingredientsFunction (None): One or more `IngredientsFunction` that will appended to `listIngredientsFunctions`.
@@ -160,7 +226,7 @@ class IngredientsModule:
160
226
  """Append one or more statements to `prologue`."""
161
227
  list_body: list[ast.stmt] = []
162
228
  listTypeIgnore: list[ast.TypeIgnore] = []
163
- if astModule is not None and be.Module(astModule):
229
+ if astModule is not None and isinstance(astModule, ast.Module): # type: ignore
164
230
  list_body.extend(astModule.body)
165
231
  listTypeIgnore.extend(astModule.type_ignores)
166
232
  if type_ignores is not None:
@@ -189,10 +255,8 @@ class IngredientsModule:
189
255
  def appendIngredientsFunction(self, *ingredientsFunction: IngredientsFunction) -> None:
190
256
  """Append one or more `IngredientsFunction`."""
191
257
  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}`.")
258
+ assert isinstance(allegedIngredientsFunction, IngredientsFunction), ValueError(f"I received `{type(allegedIngredientsFunction) = }`, but I can only accept `{IngredientsFunction}`.")
259
+ self.listIngredientsFunctions.append(allegedIngredientsFunction)
196
260
 
197
261
  def removeImportFromModule(self, moduleWithLogicalPath: str_nameDOTname) -> None:
198
262
  self.removeImportFrom(moduleWithLogicalPath, None, None)
@@ -201,7 +265,7 @@ class IngredientsModule:
201
265
  def removeImportFrom(self, moduleWithLogicalPath: str_nameDOTname, name: ast_Identifier | None, asname: ast_Identifier | None = None) -> None:
202
266
  """
203
267
  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.
268
+ It is not a "blacklist", so the `import from` could be added after this modification.
205
269
  """
206
270
  self.imports.removeImportFrom(moduleWithLogicalPath, name, asname)
207
271
  for ingredientsFunction in self.listIngredientsFunctions:
@@ -240,13 +304,33 @@ class IngredientsModule:
240
304
  listTypeIgnore.extend(self.launcher.type_ignores)
241
305
  return listTypeIgnore
242
306
 
307
+ # Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
243
308
  @dataclasses.dataclass
244
309
  class RecipeSynthesizeFlow:
245
- """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
+ """
246
330
  # ========================================
247
331
  # Source
248
- # ========================================
249
- source_astModule = parseLogicalPath2astModule(The.logicalPathModuleSourceAlgorithm)
332
+ source_astModule: ast.Module = parseLogicalPath2astModule(The.logicalPathModuleSourceAlgorithm)
333
+ """AST of the source algorithm module containing the original implementation."""
250
334
 
251
335
  # Figure out dynamic flow control to synthesized modules https://github.com/hunterhogan/mapFolding/issues/4
252
336
  sourceCallableDispatcher: ast_Identifier = The.sourceCallableDispatcher
@@ -294,18 +378,15 @@ class RecipeSynthesizeFlow:
294
378
 
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."""
434
+
361
435
  field2AnnAssign: dict[ast_Identifier, ast.AnnAssign] = dataclasses.field(default_factory=dict)
362
436
  """Maps field names to their corresponding AST call expressions."""
437
+
363
438
  Z0Z_field2AnnAssign: dict[ast_Identifier, tuple[ast.AnnAssign, 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,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)