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.
- mapFolding/__init__.py +60 -13
- mapFolding/basecamp.py +32 -17
- mapFolding/beDRY.py +4 -5
- mapFolding/oeis.py +94 -7
- mapFolding/someAssemblyRequired/RecipeJob.py +103 -0
- mapFolding/someAssemblyRequired/__init__.py +71 -50
- mapFolding/someAssemblyRequired/_theTypes.py +11 -15
- mapFolding/someAssemblyRequired/_tool_Make.py +36 -9
- mapFolding/someAssemblyRequired/_tool_Then.py +59 -25
- mapFolding/someAssemblyRequired/_toolboxAntecedents.py +159 -272
- mapFolding/someAssemblyRequired/_toolboxContainers.py +155 -70
- mapFolding/someAssemblyRequired/_toolboxPython.py +168 -44
- mapFolding/someAssemblyRequired/synthesizeNumbaJob.py +154 -39
- mapFolding/someAssemblyRequired/toolboxNumba.py +72 -230
- mapFolding/someAssemblyRequired/transformationTools.py +370 -141
- mapFolding/syntheticModules/{numbaCount_doTheNeedful.py → numbaCount.py} +7 -4
- mapFolding/theDao.py +19 -16
- mapFolding/theSSOT.py +165 -62
- mapFolding/toolboxFilesystem.py +1 -1
- mapfolding-0.9.1.dist-info/METADATA +177 -0
- mapfolding-0.9.1.dist-info/RECORD +47 -0
- tests/__init__.py +44 -0
- tests/conftest.py +75 -7
- tests/test_computations.py +92 -10
- tests/test_filesystem.py +32 -33
- tests/test_other.py +0 -1
- tests/test_tasks.py +1 -1
- mapFolding/someAssemblyRequired/newInliner.py +0 -22
- mapfolding-0.8.6.dist-info/METADATA +0 -190
- mapfolding-0.8.6.dist-info/RECORD +0 -47
- {mapfolding-0.8.6.dist-info → mapfolding-0.9.1.dist-info}/WHEEL +0 -0
- {mapfolding-0.8.6.dist-info → mapfolding-0.9.1.dist-info}/entry_points.txt +0 -0
- {mapfolding-0.8.6.dist-info → mapfolding-0.9.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
2
|
+
AST Container Classes for Python Code Generation and Transformation
|
|
3
3
|
|
|
4
|
-
This module provides container classes
|
|
5
|
-
and
|
|
6
|
-
|
|
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
|
|
11
|
-
from mapFolding.theSSOT import
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
"""
|
|
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:
|
|
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
|
-
"""
|
|
124
|
-
|
|
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
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
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 = '
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
ast.Module
|
|
184
|
+
The parsed abstract syntax tree module.
|
|
185
|
+
"""
|
|
186
|
+
return ast.parse(Path(pathFilename).read_text(), mode=mode)
|