mapFolding 0.7.1__py3-none-any.whl → 0.8.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 (48) hide show
  1. mapFolding/__init__.py +33 -4
  2. mapFolding/basecamp.py +14 -0
  3. mapFolding/beDRY.py +93 -82
  4. mapFolding/filesystem.py +124 -90
  5. mapFolding/noHomeYet.py +14 -2
  6. mapFolding/oeis.py +18 -3
  7. mapFolding/reference/flattened.py +46 -45
  8. mapFolding/reference/hunterNumba.py +4 -4
  9. mapFolding/reference/irvineJavaPort.py +1 -1
  10. mapFolding/reference/lunnanNumpy.py +3 -4
  11. mapFolding/reference/lunnanWhile.py +5 -7
  12. mapFolding/reference/rotatedEntryPoint.py +2 -3
  13. mapFolding/someAssemblyRequired/__init__.py +33 -3
  14. mapFolding/someAssemblyRequired/getLLVMforNoReason.py +32 -15
  15. mapFolding/someAssemblyRequired/ingredientsNumba.py +108 -2
  16. mapFolding/someAssemblyRequired/synthesizeNumbaFlow.py +196 -0
  17. mapFolding/someAssemblyRequired/{synthesizeNumbaJob.py → synthesizeNumbaJobVESTIGIAL.py} +19 -23
  18. mapFolding/someAssemblyRequired/transformDataStructures.py +162 -0
  19. mapFolding/someAssemblyRequired/transformationTools.py +607 -252
  20. mapFolding/syntheticModules/numbaCount_doTheNeedful.py +197 -12
  21. mapFolding/theDao.py +37 -16
  22. mapFolding/theSSOT.py +47 -44
  23. {mapfolding-0.7.1.dist-info → mapfolding-0.8.1.dist-info}/METADATA +51 -46
  24. mapfolding-0.8.1.dist-info/RECORD +39 -0
  25. {mapfolding-0.7.1.dist-info → mapfolding-0.8.1.dist-info}/WHEEL +1 -1
  26. tests/conftest.py +2 -3
  27. tests/test_filesystem.py +0 -2
  28. tests/test_other.py +2 -3
  29. tests/test_tasks.py +0 -4
  30. mapFolding/reference/lunnan.py +0 -153
  31. mapFolding/someAssemblyRequired/Z0Z_workbench.py +0 -33
  32. mapFolding/someAssemblyRequired/synthesizeCountingFunctions.py +0 -7
  33. mapFolding/someAssemblyRequired/synthesizeDataConverters.py +0 -135
  34. mapFolding/someAssemblyRequired/synthesizeNumba.py +0 -91
  35. mapFolding/someAssemblyRequired/synthesizeNumbaModules.py +0 -91
  36. mapFolding/someAssemblyRequired/whatWillBe.py +0 -357
  37. mapFolding/syntheticModules/dataNamespaceFlattened.py +0 -30
  38. mapFolding/syntheticModules/multiprocessingCount_doTheNeedful.py +0 -216
  39. mapFolding/syntheticModules/numbaCount.py +0 -90
  40. mapFolding/syntheticModules/numbaCountExample.py +0 -158
  41. mapFolding/syntheticModules/numbaCountSequential.py +0 -111
  42. mapFolding/syntheticModules/numba_doTheNeedful.py +0 -12
  43. mapFolding/syntheticModules/numba_doTheNeedfulExample.py +0 -13
  44. mapfolding-0.7.1.dist-info/RECORD +0 -51
  45. /mapFolding/{syntheticModules → reference}/__init__.py +0 -0
  46. {mapfolding-0.7.1.dist-info → mapfolding-0.8.1.dist-info}/entry_points.txt +0 -0
  47. {mapfolding-0.7.1.dist-info → mapfolding-0.8.1.dist-info/licenses}/LICENSE +0 -0
  48. {mapfolding-0.7.1.dist-info → mapfolding-0.8.1.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,68 @@
1
+ """
2
+ Tools for transforming Python code through abstract syntax tree (AST) manipulation.
3
+
4
+ This module provides a comprehensive set of utilities for programmatically analyzing,
5
+ transforming, and generating Python code through AST manipulation. It implements
6
+ a highly flexible framework that enables:
7
+
8
+ 1. Precise identification of code patterns through composable predicates
9
+ 2. Targeted modification of code structures while preserving semantics
10
+ 3. Code generation with proper syntax and import management
11
+ 4. Analysis of code dependencies and relationships
12
+ 5. Clean transformation of one algorithmic implementation to another
13
+
14
+ The utilities are organized into several key components:
15
+ - Predicate factories (ifThis): Create composable functions for matching AST patterns
16
+ - Node transformers: Modify AST structures in targeted ways
17
+ - Code generation helpers (Make): Create well-formed AST nodes programmatically
18
+ - Import tracking: Maintain proper imports during code transformation
19
+ - Analysis tools: Extract and organize code information
20
+
21
+ While these tools were developed to transform the baseline algorithm into optimized formats,
22
+ they are designed as general-purpose utilities applicable to a wide range of code
23
+ transformation scenarios beyond the scope of this package.
24
+ """
25
+ from autoflake import fix_code as autoflake_fix_code
26
+ from collections import defaultdict
1
27
  from collections.abc import Callable, Container, Sequence
2
- from pathlib import Path
3
- from typing import Any, cast, NamedTuple, TypeAlias, TYPE_CHECKING, TypeGuard, TypeVar
28
+ from copy import deepcopy
29
+ from inspect import getsource as inspect_getsource
30
+ from mapFolding.filesystem import writeStringToHere
31
+ from mapFolding.theSSOT import (
32
+ getSourceAlgorithm,
33
+ raiseIfNoneGitHubIssueNumber3,
34
+ theDataclassIdentifier,
35
+ theDataclassInstance,
36
+ theDataclassInstanceTaskDistribution,
37
+ theDispatcherCallable,
38
+ theFileExtension,
39
+ theFormatStrModuleForCallableSynthetic,
40
+ theFormatStrModuleSynthetic,
41
+ theLogicalPathModuleDataclass,
42
+ theLogicalPathModuleDispatcherSynthetic,
43
+ theModuleDispatcherSynthetic,
44
+ theModuleOfSyntheticModules,
45
+ thePackageName,
46
+ thePathPackage,
47
+ theSourceInitializeCallable,
48
+ theSourceParallelCallable,
49
+ theSourceSequentialCallable,
50
+ )
51
+ from os import PathLike
52
+ from pathlib import Path, PurePath, PurePosixPath
53
+ from types import ModuleType
54
+ from typing import Any, cast, Generic, TypeAlias, TypeGuard, TypeVar
55
+ from Z0Z_tools import updateExtendPolishDictionaryLists
4
56
  import ast
5
- if TYPE_CHECKING:
6
- from mapFolding.someAssemblyRequired.whatWillBe import LedgerOfImports
57
+ import dataclasses
58
+
7
59
  """
8
60
  Semiotic notes:
9
61
  In the `ast` package, some things that look and feel like a "name" are not `ast.Name` type. The following semiotics are a balance between technical precision and practical usage.
10
62
 
11
63
  astName: always means `ast.Name`.
12
64
  Name: uppercase, _should_ be interchangeable with astName, even in camelCase.
65
+ Hunter: ^^ did you do that ^^ ? Are you sure? You just fixed some that should have been "_name" because it confused you.
13
66
  name: lowercase, never means `ast.Name`. In camelCase, I _should_ avoid using it in such a way that it could be confused with "Name", uppercase.
14
67
  _Identifier: very strongly correlates with the private `ast._Identifier`, which is a TypeAlias for `str`.
15
68
  identifier: lowercase, a general term that includes the above and other Python identifiers.
@@ -17,316 +70,332 @@ Identifier: uppercase, without the leading underscore should only appear in came
17
70
  namespace: lowercase, in dotted-names, such as `pathlib.Path` or `collections.abc`, "namespace" is the part before the dot.
18
71
  Namespace: uppercase, should only appear in camelCase and means "namespace", lowercase.
19
72
  """
20
- # TODO consider semiotic usefulness of "namespace" or variations such as "namespaceName", "namespacePath", and "namespace_Identifier"
21
73
 
22
- # TODO learn whether libcst can help
74
+ # Would `LibCST` be better than `ast` in some cases? https://github.com/hunterhogan/mapFolding/issues/7
23
75
 
24
- astParameter = TypeVar('astParameter', bound=Any)
76
+ ast_expr_Slice: TypeAlias = ast.expr
25
77
  ast_Identifier: TypeAlias = str
26
- strDotStrCuzPyStoopid: TypeAlias = str
27
- strORlist_ast_type_paramORintORNone: TypeAlias = Any
78
+ astClassHasAttributeDOTname: TypeAlias = ast.FunctionDef | ast.ClassDef | ast.AsyncFunctionDef
79
+ astMosDef = TypeVar('astMosDef', bound=astClassHasAttributeDOTname)
28
80
  list_ast_type_paramORintORNone: TypeAlias = Any
81
+ nodeType = TypeVar('nodeType', bound=ast.AST)
82
+ strDotStrCuzPyStoopid: TypeAlias = str
29
83
  strORintORNone: TypeAlias = Any
30
- Z0Z_thisCannotBeTheBestWay: TypeAlias = list[ast.Name] | list[ast.Attribute] | list[ast.Subscript] | list[ast.Name | ast.Attribute] | list[ast.Name | ast.Subscript] | list[ast.Attribute | ast.Subscript] | list[ast.Name | ast.Attribute | ast.Subscript]
31
-
32
- # NOTE: the new "Recipe" concept will allow me to remove this
33
- class YouOughtaKnow(NamedTuple):
34
- callableSynthesized: str
35
- pathFilenameForMe: Path
36
- astForCompetentProgrammers: ast.ImportFrom
37
-
38
- # listAsNode
84
+ strORlist_ast_type_paramORintORNone: TypeAlias = Any
39
85
 
40
- class NodeCollector(ast.NodeVisitor):
41
- # A node visitor that collects data via one or more actions when a predicate is met.
42
- def __init__(self, findPredicate: Callable[[ast.AST], bool], actions: list[Callable[[ast.AST], None]]) -> None:
43
- self.findPredicate = findPredicate
44
- self.actions = actions
86
+ class NodeCollector(Generic[nodeType], ast.NodeVisitor):
87
+ """A node visitor that collects data via one or more actions when a predicate is met."""
88
+ def __init__(self, findThis: Callable[[ast.AST], TypeGuard[nodeType] | bool], doThat: list[Callable[[nodeType], Any]]) -> None:
89
+ self.findThis = findThis
90
+ self.doThat = doThat
45
91
 
46
92
  def visit(self, node: ast.AST) -> None:
47
- if self.findPredicate(node):
48
- for action in self.actions:
49
- action(node)
93
+ if self.findThis(node):
94
+ for action in self.doThat:
95
+ action(cast(nodeType, node))
50
96
  self.generic_visit(node)
51
97
 
52
- class NodeReplacer(ast.NodeTransformer):
53
- """
54
- A node transformer that replaces or removes AST nodes based on a condition.
55
- This transformer traverses an AST and for each node checks a predicate. If the predicate
56
- returns True, the transformer uses the replacement builder to obtain a new node. Returning
57
- None from the replacement builder indicates that the node should be removed.
58
-
59
- Attributes:
60
- findMe: A function that finds all locations that match a one or more conditions.
61
- doThis: A function that does work at each location, such as make a new node, collect information or delete the node.
62
-
63
- Methods:
64
- visit(node: ast.AST) -> Optional[ast.AST]:
65
- Visits each node in the AST, replacing or removing it based on the predicate.
66
- """
67
- def __init__(self
68
- , findMe: Callable[[ast.AST], bool]
69
- , doThis: Callable[[ast.AST], ast.AST | Sequence[ast.AST] | None]
70
- ) -> None:
71
- self.findMe = findMe
72
- self.doThis = doThis
98
+ class NodeReplacer(Generic[nodeType], ast.NodeTransformer):
99
+ """A node transformer that replaces or removes AST nodes based on a condition."""
100
+ def __init__(self, findThis: Callable[[ast.AST], TypeGuard[nodeType] | bool], doThat: Callable[[nodeType], ast.AST | Sequence[ast.AST] | None]) -> None:
101
+ self.findThis = findThis
102
+ self.doThat = doThat
73
103
 
74
104
  def visit(self, node: ast.AST) -> ast.AST | Sequence[ast.AST] | None:
75
- if self.findMe(node):
76
- return self.doThis(node)
105
+ if self.findThis(node):
106
+ return self.doThat(cast(nodeType, node))
77
107
  return super().visit(node)
78
108
 
79
- def descendantContainsMatchingNode(node: ast.AST, predicateFunction: Callable[[ast.AST], bool]) -> bool:
80
- """ Return True if any descendant of the node (or the node itself) matches the predicateFunction. """
81
- matchFound = False
82
-
83
- class DescendantFinder(ast.NodeVisitor):
84
- def generic_visit(self, node: ast.AST) -> None:
85
- nonlocal matchFound
86
- if predicateFunction(node):
87
- matchFound = True
88
- else:
89
- super().generic_visit(node)
90
-
91
- DescendantFinder().visit(node)
92
- return matchFound
93
-
94
- def executeActionUnlessDescendantMatches(exclusionPredicate: Callable[[ast.AST], bool], actionFunction: Callable[[ast.AST], None]) -> Callable[[ast.AST], None]:
95
- """
96
- Return a new action that will execute actionFunction only if no descendant (or the node itself)
97
- matches exclusionPredicate.
98
- """
99
- def wrappedAction(node: ast.AST) -> None:
100
- if not descendantContainsMatchingNode(node, exclusionPredicate):
101
- actionFunction(node)
102
- return wrappedAction
103
-
104
109
  class ifThis:
105
110
  @staticmethod
106
- def anyOf(*somePredicates: Callable[[ast.AST], bool]) -> Callable[[ast.AST], bool]:
107
- return lambda nodeTarget: any(predicate(nodeTarget) for predicate in somePredicates)
108
-
109
- @staticmethod
110
- def ast_IdentifierIsIn(container: Container[ast_Identifier]) -> Callable[[ast_Identifier], bool]:
111
+ def ast_IdentifierIsIn(container: Container[ast_Identifier]) -> Callable[[ast_Identifier], TypeGuard[ast_Identifier] | bool]:
111
112
  return lambda node: node in container
112
-
113
- # TODO is this only useable if namespace is not `None`? Yes, but use "" for namespace if necessary.
114
113
  @staticmethod
115
- def CallDoesNotCallItself(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
116
- return lambda nodeFocus: ifThis.CallReallyIs(namespace, identifier)(nodeFocus) and 1 == sum(1 for descendant in ast.walk(nodeFocus) if ifThis.CallReallyIs(namespace, identifier)(descendant))
117
-
114
+ def CallDoesNotCallItself(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Call] | bool]:
115
+ """If `namespace` is not applicable to your case, then call with `namespace=""`."""
116
+ return lambda node: ifThis.matchesMeButNotAnyDescendant(ifThis.CallReallyIs(namespace, identifier))(node)
118
117
  @staticmethod
119
- def CallDoesNotCallItselfAndNameDOTidIsIn(container: Container[ast_Identifier]) -> Callable[[ast.AST], bool]:
120
- return lambda nodeSubject: (ifThis.isCall(nodeSubject) and ifThis.isName(nodeSubject.func) and ifThis.ast_IdentifierIsIn(container)(nodeSubject.func.id) and ifThis.CallDoesNotCallItself("", nodeSubject.func.id)(nodeSubject))
121
-
118
+ def CallDoesNotCallItselfAndNameDOTidIsIn(container: Container[ast_Identifier]) -> Callable[[ast.AST], TypeGuard[ast.Call] | bool]:
119
+ return lambda node: ifThis.isCall(node) and ifThis.isName(node.func) and ifThis.ast_IdentifierIsIn(container)(node.func.id) and ifThis.CallDoesNotCallItself("", node.func.id)(node)
122
120
  @staticmethod
123
- def CallReallyIs(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
124
- return ifThis.anyOf(ifThis.isCall_Identifier(identifier), ifThis.isCallNamespace_Identifier(namespace, identifier))
125
-
121
+ def CallReallyIs(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Call] | bool]:
122
+ return ifThis.isAnyOf(ifThis.isCall_Identifier(identifier), ifThis.isCallNamespace_Identifier(namespace, identifier))
126
123
  @staticmethod
127
124
  def is_keyword(node: ast.AST) -> TypeGuard[ast.keyword]:
128
125
  return isinstance(node, ast.keyword)
129
-
130
126
  @staticmethod
131
- def is_keywordAndValueIsConstant(nodeCheck: ast.AST) -> TypeGuard[ast.keyword]:
132
- return ifThis.is_keyword(nodeCheck) and ifThis.isConstant(nodeCheck.value)
133
-
127
+ def is_keywordAndValueIsConstant(node: ast.AST) -> TypeGuard[ast.keyword]:
128
+ return ifThis.is_keyword(node) and ifThis.isConstant(node.value)
134
129
  @staticmethod
135
- def is_keyword_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
136
- return lambda nodeInstant: ifThis.is_keyword(nodeInstant) and nodeInstant.arg == identifier
130
+ def is_keyword_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.keyword] | bool]:
131
+ def workhorse(node: ast.AST) -> TypeGuard[ast.keyword] | bool:
132
+ return ifThis.is_keyword(node) and node.arg == identifier
133
+ return workhorse
137
134
  @staticmethod
138
- def is_keyword_IdentifierEqualsConstantValue(identifier: ast_Identifier, ConstantValue: Any) -> Callable[[ast.AST], bool]:
139
- return lambda astNode: (ifThis.is_keyword_Identifier(identifier)(astNode) and ifThis.is_keywordAndValueIsConstant(astNode) and ifThis.isConstantEquals(ConstantValue)(astNode.value))
140
-
135
+ def is_keyword_IdentifierEqualsConstantValue(identifier: ast_Identifier, ConstantValue: Any) -> Callable[[ast.AST], TypeGuard[ast.keyword] | bool]:
136
+ return lambda node: ifThis.is_keyword_Identifier(identifier)(node) and ifThis.is_keywordAndValueIsConstant(node) and ifThis.isConstantEquals(ConstantValue)(node.value)
137
+ @staticmethod
138
+ def isAllOf(*thesePredicates: Callable[[ast.AST], bool]) -> Callable[[ast.AST], bool]:
139
+ return lambda node: all(predicate(node) for predicate in thesePredicates)
141
140
  @staticmethod
142
141
  def isAnnAssign(node: ast.AST) -> TypeGuard[ast.AnnAssign]:
143
142
  return isinstance(node, ast.AnnAssign)
144
-
145
143
  @staticmethod
146
144
  def isAnnAssignAndAnnotationIsName(node: ast.AST) -> TypeGuard[ast.AnnAssign]:
147
145
  return ifThis.isAnnAssign(node) and ifThis.isName(node.annotation)
148
-
149
146
  @staticmethod
150
- def isAnnAssignAndTargetIsName(whatNode: ast.AST) -> TypeGuard[ast.AnnAssign]:
151
- return ifThis.isAnnAssign(whatNode) and ifThis.isName(whatNode.target)
152
-
147
+ def isAnnAssignAndTargetIsName(node: ast.AST) -> TypeGuard[ast.AnnAssign]:
148
+ return ifThis.isAnnAssign(node) and ifThis.isName(node.target)
153
149
  @staticmethod
154
- def isAnnAssignTo(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
155
- return lambda nodeStop: ifThis.isAnnAssign(nodeStop) and ifThis.NameReallyIs(identifier)(nodeStop.target)
156
-
150
+ def isAnnAssignTo(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.AnnAssign] | bool]:
151
+ return lambda node: ifThis.isAnnAssign(node) and ifThis.NameReallyIs_Identifier(identifier)(node.target)
157
152
  @staticmethod
158
153
  def isAnyAssignmentTo(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
159
- return ifThis.anyOf(ifThis.isAssignOnlyTo(identifier), ifThis.isAnnAssignTo(identifier), ifThis.isAugAssignTo(identifier))
160
-
154
+ return ifThis.isAnyOf(ifThis.isAssignOnlyTo(identifier), ifThis.isAnnAssignTo(identifier), ifThis.isAugAssignTo(identifier))
155
+ @staticmethod
156
+ def isAnyCompare(node: ast.AST) -> TypeGuard[ast.Compare] | TypeGuard[ast.BoolOp]:
157
+ return ifThis.isCompare(node) or ifThis.isBoolOp(node)
158
+ @staticmethod
159
+ def isAnyOf(*thesePredicates: Callable[[ast.AST], bool]) -> Callable[[ast.AST], bool]:
160
+ return lambda node: any(predicate(node) for predicate in thesePredicates)
161
161
  @staticmethod
162
162
  def isAssign(node: ast.AST) -> TypeGuard[ast.Assign]:
163
163
  return isinstance(node, ast.Assign)
164
-
165
164
  @staticmethod
166
- def isAssignOnlyTo(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
167
- return lambda aNode: ifThis.isAssign(aNode) and ifThis.NameReallyIs(identifier)(aNode.targets[0])
168
-
165
+ def isAssignAndValueIsCall_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Assign] | bool]:
166
+ return lambda node: ifThis.isAssign(node) and ifThis.isCall_Identifier(identifier)(node.value)
167
+ @staticmethod
168
+ def isAssignAndValueIsCallNamespace_Identifier(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Assign] | bool]:
169
+ return ifThis.isAssignAndValueIs(ifThis.isCallNamespace_Identifier(namespace, identifier))
170
+ @staticmethod
171
+ def isAssignOnlyTo(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Assign] | bool]:
172
+ return lambda node: ifThis.isAssign(node) and ifThis.NameReallyIs_Identifier(identifier)(node.targets[0])
173
+ @staticmethod
174
+ def isAssignAndTargets0Is(targets0Predicate: Callable[[ast.AST], bool]) -> Callable[[ast.AST], TypeGuard[ast.Assign] | bool]:
175
+ """node is Assign and node.targets[0] matches `targets0Predicate`."""
176
+ return lambda node: ifThis.isAssign(node) and targets0Predicate(node.targets[0])
177
+ @staticmethod
178
+ def isAssignAndValueIs(valuePredicate: Callable[[ast.AST], bool]) -> Callable[[ast.AST], TypeGuard[ast.Assign] | bool]:
179
+ """node is ast.Assign and node.value matches `valuePredicate`.
180
+ Parameters:
181
+ valuePredicate: Function that evaluates the value of the assignment
182
+ Returns:
183
+ predicate: matches assignments with values meeting the criteria
184
+ """
185
+ return lambda node: ifThis.isAssign(node) and valuePredicate(node.value)
169
186
  @staticmethod
170
187
  def isAttribute(node: ast.AST) -> TypeGuard[ast.Attribute]:
171
188
  return isinstance(node, ast.Attribute)
172
-
173
189
  @staticmethod
174
190
  def isAugAssign(node: ast.AST) -> TypeGuard[ast.AugAssign]:
175
191
  return isinstance(node, ast.AugAssign)
176
-
177
192
  @staticmethod
178
- def isAugAssignTo(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
179
- return lambda nodeQuestion: ifThis.isAugAssign(nodeQuestion) and ifThis.NameReallyIs(identifier)(nodeQuestion.target)
180
-
193
+ def isAugAssignTo(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.AugAssign] | bool]:
194
+ return lambda node: ifThis.isAugAssign(node) and ifThis.NameReallyIs_Identifier(identifier)(node.target)
195
+ @staticmethod
196
+ def isBoolOp(node: ast.AST) -> TypeGuard[ast.BoolOp]:
197
+ return isinstance(node, ast.BoolOp)
181
198
  @staticmethod
182
199
  def isCall(node: ast.AST) -> TypeGuard[ast.Call]:
183
200
  return isinstance(node, ast.Call)
184
-
185
201
  @staticmethod
186
- def isCall_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
187
- return lambda ImaNode: ifThis.isCall(ImaNode) and ifThis.isName_Identifier(identifier)(ImaNode.func)
188
-
189
- # TODO what happens if `None` is passed as the namespace?
202
+ def isCall_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Call] | bool]:
203
+ return lambda node: ifThis.isCall(node) and ifThis.isName_Identifier(identifier)(node.func)
190
204
  @staticmethod
191
- def isCallNamespace_Identifier(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
192
- return lambda node: ifThis.isCall(node) and ifThis.isNameDOTnameNamespace_Identifier(namespace, identifier)(node.func)
193
-
205
+ def isCallNamespace_Identifier(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Call] | bool]:
206
+ return lambda node: ifThis.isCall(node) and ifThis.is_nameDOTnameNamespace_Identifier(namespace, identifier)(node.func)
194
207
  @staticmethod
195
208
  def isCallToName(node: ast.AST) -> TypeGuard[ast.Call]:
196
209
  return ifThis.isCall(node) and ifThis.isName(node.func)
197
-
198
210
  @staticmethod
199
211
  def isClassDef(node: ast.AST) -> TypeGuard[ast.ClassDef]:
200
212
  return isinstance(node, ast.ClassDef)
201
-
202
213
  @staticmethod
203
- def isClassDef_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
214
+ def isClassDef_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.ClassDef] | bool]:
204
215
  return lambda node: ifThis.isClassDef(node) and node.name == identifier
205
-
216
+ @staticmethod
217
+ def isCompare(node: ast.AST) -> TypeGuard[ast.Compare]:
218
+ return isinstance(node, ast.Compare)
206
219
  @staticmethod
207
220
  def isConstant(node: ast.AST) -> TypeGuard[ast.Constant]:
208
221
  return isinstance(node, ast.Constant)
209
-
210
222
  @staticmethod
211
- def isConstantEquals(value: Any) -> Callable[[ast.AST], bool]:
223
+ def isConstantEquals(value: Any) -> Callable[[ast.AST], TypeGuard[ast.Constant] | bool]:
212
224
  return lambda node: ifThis.isConstant(node) and node.value == value
213
-
225
+ @staticmethod
226
+ def isExpr(node: ast.AST) -> TypeGuard[ast.Expr]:
227
+ return isinstance(node, ast.Expr)
214
228
  @staticmethod
215
229
  def isFunctionDef(node: ast.AST) -> TypeGuard[ast.FunctionDef]:
216
230
  return isinstance(node, ast.FunctionDef)
217
-
218
231
  @staticmethod
219
- def isFunctionDef_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
232
+ def isFunctionDef_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.FunctionDef] | bool]:
220
233
  return lambda node: ifThis.isFunctionDef(node) and node.name == identifier
221
-
222
234
  @staticmethod
223
235
  def isImport(node: ast.AST) -> TypeGuard[ast.Import]:
224
236
  return isinstance(node, ast.Import)
225
-
226
237
  @staticmethod
227
238
  def isName(node: ast.AST) -> TypeGuard[ast.Name]:
239
+ """TODO
240
+ ast.Name()
241
+ ast.Attribute()
242
+ ast.Subscript()
243
+ ast.Starred()
244
+ """
228
245
  return isinstance(node, ast.Name)
229
-
230
246
  @staticmethod
231
- def isName_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
247
+ def isName_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Name] | bool]:
232
248
  return lambda node: ifThis.isName(node) and node.id == identifier
233
-
234
249
  @staticmethod
235
- def isNameDOTname(node: ast.AST) -> TypeGuard[ast.Attribute]:
250
+ def is_nameDOTname(node: ast.AST) -> TypeGuard[ast.Attribute]:
236
251
  return ifThis.isAttribute(node) and ifThis.isName(node.value)
237
-
238
252
  @staticmethod
239
- def isNameDOTnameNamespace_Identifier(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
240
- return lambda node: ifThis.isNameDOTname(node) and ifThis.isName_Identifier(namespace)(node.value) and node.attr == identifier
241
-
253
+ def is_nameDOTnameNamespace(namespace: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Attribute] | bool]:
254
+ return lambda node: ifThis.is_nameDOTname(node) and ifThis.isName_Identifier(namespace)(node.value)
255
+ @staticmethod
256
+ def is_nameDOTnameNamespace_Identifier(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Attribute] | bool]:
257
+ return lambda node: ifThis.is_nameDOTname(node) and ifThis.isName_Identifier(namespace)(node.value) and node.attr == identifier
258
+ @staticmethod
259
+ def NameReallyIs_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
260
+ # The following logic is incomplete.
261
+ return ifThis.isAnyOf(ifThis.isName_Identifier(identifier), ifThis.isSubscriptIsName_Identifier(identifier))
262
+ @staticmethod
263
+ def isReturn(node: ast.AST) -> TypeGuard[ast.Return]:
264
+ return isinstance(node, ast.Return)
265
+ @staticmethod
266
+ def isReturnAnyCompare(node: ast.AST) -> TypeGuard[ast.Return]:
267
+ return ifThis.isReturn(node) and node.value is not None and ifThis.isAnyCompare(node.value)
268
+ @staticmethod
269
+ def isReturnUnaryOp(node: ast.AST) -> TypeGuard[ast.Return]:
270
+ return ifThis.isReturn(node) and node.value is not None and ifThis.isUnaryOp(node.value)
242
271
  @staticmethod
243
272
  def isSubscript(node: ast.AST) -> TypeGuard[ast.Subscript]:
244
273
  return isinstance(node, ast.Subscript)
245
-
246
274
  @staticmethod
247
- def isSubscript_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
275
+ def isSubscript_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Subscript]]:
276
+ """node is `ast.Subscript` and the top-level `ast.Name` is `identifier`
277
+ Parameters:
278
+ identifier: The identifier to look for in the value chain
279
+ Returns:
280
+ predicate: function that checks if a node matches the criteria
281
+ """
282
+ def predicate(node: ast.AST) -> TypeGuard[ast.Subscript]:
283
+ if not ifThis.isSubscript(node):
284
+ return False
285
+ def checkNodeDOTvalue(nodeDOTvalue: ast.AST) -> bool:
286
+ if ifThis.isName(nodeDOTvalue):
287
+ if nodeDOTvalue.id == identifier:
288
+ return True
289
+ elif hasattr(nodeDOTvalue, "value"):
290
+ return checkNodeDOTvalue(nodeDOTvalue.value) # type: ignore
291
+ return False
292
+ return checkNodeDOTvalue(node.value)
293
+ return predicate
294
+ @staticmethod
295
+ def isSubscriptIsName_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Subscript] | bool]:
248
296
  return lambda node: ifThis.isSubscript(node) and ifThis.isName_Identifier(identifier)(node.value)
249
-
250
297
  @staticmethod
251
- def isSubscript_Identifier_Identifier(identifier: ast_Identifier, sliceIdentifier: ast_Identifier) -> Callable[[ast.AST], bool]:
252
- return lambda node: ifThis.isSubscript(node) and ifThis.isName_Identifier(identifier)(node.value) and ifThis.isName_Identifier(sliceIdentifier)(node.slice) # auto-generated
253
-
298
+ def isSubscript_Identifier_Identifier(identifier: ast_Identifier, sliceIdentifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Subscript] | bool]:
299
+ return lambda node: ifThis.isSubscript(node) and ifThis.isName_Identifier(identifier)(node.value) and ifThis.isName_Identifier(sliceIdentifier)(node.slice)
300
+ @staticmethod
301
+ def isUnaryOp(node: ast.AST) -> TypeGuard[ast.UnaryOp]:
302
+ return isinstance(node, ast.UnaryOp)
303
+ # TODO Does this work?
304
+ @staticmethod
305
+ def matchesAtLeast1Descendant(predicate: Callable[[ast.AST], bool]) -> Callable[[ast.AST], bool]:
306
+ """Create a predicate that returns True if any descendant of the node matches the given predicate."""
307
+ return lambda node: not ifThis.matchesNoDescendant(predicate)(node)
308
+ # TODO Does this work?
309
+ @staticmethod
310
+ def matchesMeAndMyDescendantsExactlyNTimes(predicate: Callable[[ast.AST], bool], nTimes: int) -> Callable[[ast.AST], bool]:
311
+ """Create a predicate that returns True if exactly 'count' nodes in the tree match the predicate."""
312
+ def countMatchingNodes(node: ast.AST) -> bool:
313
+ matches = sum(1 for descendant in ast.walk(node) if predicate(descendant))
314
+ return matches == nTimes
315
+ return countMatchingNodes
316
+ @staticmethod
317
+ def matchesMeButNotAnyDescendant(predicate: Callable[[ast.AST], bool]) -> Callable[[ast.AST], bool]:
318
+ """Create a predicate that returns True if the node matches but none of its descendants match the predicate."""
319
+ return lambda node: predicate(node) and ifThis.matchesNoDescendant(predicate)(node)
320
+ @staticmethod
321
+ def matchesNoDescendant(predicate: Callable[[ast.AST], bool]) -> Callable[[ast.AST], bool]:
322
+ """Create a predicate that returns True if no descendant of the node matches the given predicate."""
323
+ def checkNoMatchingDescendant(node: ast.AST) -> bool:
324
+ for descendant in ast.walk(node):
325
+ if descendant is not node and predicate(descendant):
326
+ return False
327
+ return True
328
+ return checkNoMatchingDescendant
329
+ @staticmethod
330
+ def onlyReturnAnyCompare(astFunctionDef: ast.AST) -> TypeGuard[ast.FunctionDef]:
331
+ return ifThis.isFunctionDef(astFunctionDef) and len(astFunctionDef.body) == 1 and ifThis.isReturnAnyCompare(astFunctionDef.body[0])
254
332
  @staticmethod
255
- def NameReallyIs(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
256
- return ifThis.anyOf(ifThis.isName_Identifier(identifier), ifThis.isSubscript_Identifier(identifier))
333
+ def onlyReturnUnaryOp(astFunctionDef: ast.AST) -> TypeGuard[ast.FunctionDef]:
334
+ return ifThis.isFunctionDef(astFunctionDef) and len(astFunctionDef.body) == 1 and ifThis.isReturnUnaryOp(astFunctionDef.body[0])
257
335
 
258
336
  class Make:
259
337
  @staticmethod
260
- def copy_astCallKeywords(astCall: ast.Call) -> dict[str, Any]:
261
- """Extract keyword parameters from a decorator AST node."""
262
- dictionaryKeywords: dict[str, Any] = {}
263
- for keywordItem in astCall.keywords:
264
- if isinstance(keywordItem.value, ast.Constant) and keywordItem.arg is not None:
265
- dictionaryKeywords[keywordItem.arg] = keywordItem.value.value
266
- return dictionaryKeywords
267
-
338
+ def ast_arg(identifier: ast_Identifier, annotation: ast.expr | None = None, **keywordArguments: strORintORNone) -> ast.arg:
339
+ """keywordArguments: type_comment:str|None, lineno:int, col_offset:int, end_lineno:int|None, end_col_offset:int|None"""
340
+ return ast.arg(identifier, annotation, **keywordArguments)
341
+ @staticmethod
342
+ def ast_keyword(keywordArgument: ast_Identifier, value: ast.expr, **keywordArguments: int) -> ast.keyword:
343
+ return ast.keyword(arg=keywordArgument, value=value, **keywordArguments)
268
344
  @staticmethod
269
345
  def astAlias(name: ast_Identifier, asname: ast_Identifier | None = None) -> ast.alias:
270
- return ast.alias(name=name, asname=asname)
271
-
346
+ return ast.alias(name, asname)
272
347
  @staticmethod
273
348
  def astAnnAssign(target: ast.Name | ast.Attribute | ast.Subscript, annotation: ast.expr, value: ast.expr | None = None, **keywordArguments: int) -> ast.AnnAssign:
274
- """ `simple: int`: uses a clever int-from-boolean to assign the correct value to the `simple` attribute. So, don't add it as a parameter."""
349
+ """`simple: int`: uses a clever int-from-boolean to assign the correct value to the `simple` attribute. So, don't add it as a parameter."""
275
350
  return ast.AnnAssign(target, annotation, value, simple=int(isinstance(target, ast.Name)), **keywordArguments)
276
-
277
351
  @staticmethod
278
352
  def astAssign(listTargets: Any, value: ast.expr, **keywordArguments: strORintORNone) -> ast.Assign:
279
353
  """keywordArguments: type_comment:str|None, lineno:int, col_offset:int, end_lineno:int|None, end_col_offset:int|None"""
280
354
  return ast.Assign(targets=listTargets, value=value, **keywordArguments)
281
-
282
- @staticmethod
283
- def astArg(identifier: ast_Identifier, annotation: ast.expr | None = None, **keywordArguments: strORintORNone) -> ast.arg:
284
- """keywordArguments: type_comment:str|None, lineno:int, col_offset:int, end_lineno:int|None, end_col_offset:int|None"""
285
- return ast.arg(identifier, annotation, **keywordArguments)
286
-
287
355
  @staticmethod
288
356
  def astArgumentsSpecification(posonlyargs: list[ast.arg]=[], args: list[ast.arg]=[], vararg: ast.arg|None=None, kwonlyargs: list[ast.arg]=[], kw_defaults: list[ast.expr|None]=[None], kwarg: ast.arg|None=None, defaults: list[ast.expr]=[]) -> ast.arguments:
289
- return ast.arguments(posonlyargs=posonlyargs, args=args, vararg=vararg, kwonlyargs=kwonlyargs, kw_defaults=kw_defaults, kwarg=kwarg, defaults=defaults)
290
-
357
+ return ast.arguments(posonlyargs, args, vararg, kwonlyargs, kw_defaults, kwarg, defaults)
291
358
  @staticmethod
292
- def astCall(caller: ast.Name | ast.Attribute, args: Sequence[ast.expr] | None = None, list_astKeywords: Sequence[ast.keyword] | None = None) -> ast.Call:
293
- return ast.Call(func=caller, args=list(args) if args else [], keywords=list(list_astKeywords) if list_astKeywords else [])
294
-
359
+ def astAttribute(value: ast.expr, attribute: ast_Identifier, context: ast.expr_context = ast.Load(), **keywordArguments: int) -> ast.Attribute:
360
+ """
361
+ Parameters:
362
+ value: the part before the dot (hint `ast.Name` for nameDOTname)
363
+ attribute: the `str` after the dot
364
+ context (ast.Load()): Load/Store/Del"""
365
+ return ast.Attribute(value, attribute, context, **keywordArguments)
366
+ @staticmethod
367
+ def astCall(caller: ast.Name | ast.Attribute, listArguments: Sequence[ast.expr] | None = None, list_astKeywords: Sequence[ast.keyword] | None = None) -> ast.Call:
368
+ return ast.Call(func=caller, args=list(listArguments) if listArguments else [], keywords=list(list_astKeywords) if list_astKeywords else [])
295
369
  @staticmethod
296
370
  def astClassDef(name: ast_Identifier, listBases: list[ast.expr]=[], list_keyword: list[ast.keyword]=[], body: list[ast.stmt]=[], decorator_list: list[ast.expr]=[], **keywordArguments: list_ast_type_paramORintORNone) -> ast.ClassDef:
297
371
  """keywordArguments: type_params:list[ast.type_param], lineno:int, col_offset:int, end_lineno:int|None, end_col_offset:int|None"""
298
372
  return ast.ClassDef(name=name, bases=listBases, keywords=list_keyword, body=body, decorator_list=decorator_list, **keywordArguments)
299
-
373
+ @staticmethod
374
+ def astConstant(value: Any, **keywordArguments: strORintORNone) -> ast.Constant:
375
+ """value: str|int|float|bool|None|bytes|bytearray|memoryview|complex|list|tuple|dict|set, or any other type that can be represented as a constant in Python.
376
+ keywordArguments: kind:str, lineno:int, col_offset:int, end_lineno:int|None, end_col_offset:int|None"""
377
+ return ast.Constant(value, **keywordArguments)
300
378
  @staticmethod
301
379
  def astFunctionDef(name: ast_Identifier, argumentsSpecification: ast.arguments=ast.arguments(), body: list[ast.stmt]=[], decorator_list: list[ast.expr]=[], returns: ast.expr|None=None, **keywordArguments: strORlist_ast_type_paramORintORNone) -> ast.FunctionDef:
302
380
  """keywordArguments: type_comment:str|None, type_params:list[ast.type_param], lineno:int, col_offset:int, end_lineno:int|None, end_col_offset:int|None"""
303
381
  return ast.FunctionDef(name=name, args=argumentsSpecification, body=body, decorator_list=decorator_list, returns=returns, **keywordArguments)
304
-
305
382
  @staticmethod
306
383
  def astImport(moduleName: ast_Identifier, asname: ast_Identifier | None = None, **keywordArguments: int) -> ast.Import:
307
384
  return ast.Import(names=[Make.astAlias(moduleName, asname)], **keywordArguments)
308
-
309
385
  @staticmethod
310
386
  def astImportFrom(moduleName: ast_Identifier, list_astAlias: list[ast.alias], **keywordArguments: int) -> ast.ImportFrom:
311
387
  return ast.ImportFrom(module=moduleName, names=list_astAlias, level=0, **keywordArguments)
312
-
313
- @staticmethod
314
- def astKeyword(keywordArgument: ast_Identifier, value: ast.expr, **keywordArguments: int) -> ast.keyword:
315
- return ast.keyword(arg=keywordArgument, value=value, **keywordArguments)
316
-
317
388
  @staticmethod
318
389
  def astModule(body: list[ast.stmt], type_ignores: list[ast.TypeIgnore] = []) -> ast.Module:
319
390
  return ast.Module(body, type_ignores)
320
-
321
391
  @staticmethod
322
- def astName(identifier: ast_Identifier) -> ast.Name:
323
- return ast.Name(id=identifier, ctx=ast.Load())
324
-
392
+ def astName(identifier: ast_Identifier, context: ast.expr_context = ast.Load(), **keywordArguments: int) -> ast.Name:
393
+ return ast.Name(identifier, context, **keywordArguments)
325
394
  @staticmethod
326
395
  def itDOTname(nameChain: ast.Name | ast.Attribute, dotName: str) -> ast.Attribute:
327
396
  return ast.Attribute(value=nameChain, attr=dotName, ctx=ast.Load())
328
-
329
397
  @staticmethod
398
+ # TODO rewrite with all parameters
330
399
  def nameDOTname(identifier: ast_Identifier, *dotName: str) -> ast.Name | ast.Attribute:
331
400
  nameDOTname: ast.Name | ast.Attribute = Make.astName(identifier)
332
401
  if not dotName:
@@ -334,81 +403,234 @@ class Make:
334
403
  for suffix in dotName:
335
404
  nameDOTname = Make.itDOTname(nameDOTname, suffix)
336
405
  return nameDOTname
337
-
338
406
  @staticmethod
339
407
  def astReturn(value: ast.expr | None = None, **keywordArguments: int) -> ast.Return:
340
- return ast.Return(value=value, **keywordArguments)
341
-
408
+ return ast.Return(value, **keywordArguments)
409
+ @staticmethod
410
+ def astSubscript(value: ast.expr, slice: ast_expr_Slice, context: ast.expr_context = ast.Load(), **keywordArguments: int) -> ast.Subscript:
411
+ return ast.Subscript(value, slice, ctx=context, **keywordArguments)
342
412
  @staticmethod
343
- def astTuple(elements: Sequence[ast.expr], context: ast.expr_context | None = None, **keywordArguments: int) -> ast.Tuple:
413
+ def astTuple(elements: Sequence[ast.expr], context: ast.expr_context = ast.Load(), **keywordArguments: int) -> ast.Tuple:
344
414
  """context: Load/Store/Del"""
345
- context = context or ast.Load()
346
415
  return ast.Tuple(elts=list(elements), ctx=context, **keywordArguments)
347
416
 
417
+ class LedgerOfImports:
418
+ # TODO When resolving the ledger of imports, remove self-referential imports
419
+
420
+ def __init__(self, startWith: ast.AST | None = None) -> None:
421
+ self.dictionaryImportFrom: dict[str, list[tuple[str, str | None]]] = defaultdict(list)
422
+ self.listImport: list[str] = []
423
+
424
+ if startWith:
425
+ self.walkThis(startWith)
426
+
427
+ def addAst(self, astImport_: ast.Import | ast.ImportFrom) -> None:
428
+ assert isinstance(astImport_, (ast.Import, ast.ImportFrom)), f"Expected ast.Import or ast.ImportFrom, got {type(astImport_)}"
429
+ if isinstance(astImport_, ast.Import):
430
+ for alias in astImport_.names:
431
+ self.listImport.append(alias.name)
432
+ else:
433
+ if astImport_.module is not None:
434
+ for alias in astImport_.names:
435
+ self.dictionaryImportFrom[astImport_.module].append((alias.name, alias.asname))
436
+
437
+ def addImportStr(self, module: str) -> None:
438
+ self.listImport.append(module)
439
+
440
+ def addImportFromStr(self, module: str, name: str, asname: str | None = None) -> None:
441
+ self.dictionaryImportFrom[module].append((name, asname))
442
+
443
+ def exportListModuleNames(self) -> list[str]:
444
+ listModuleNames: list[str] = list(self.dictionaryImportFrom.keys())
445
+ listModuleNames.extend(self.listImport)
446
+ return sorted(set(listModuleNames))
447
+
448
+ def makeListAst(self) -> list[ast.ImportFrom | ast.Import]:
449
+ listAstImportFrom: list[ast.ImportFrom] = []
450
+
451
+ for module, listOfNameTuples in sorted(self.dictionaryImportFrom.items()):
452
+ listOfNameTuples = sorted(list(set(listOfNameTuples)), key=lambda nameTuple: nameTuple[0])
453
+ listAlias: list[ast.alias] = []
454
+ for name, asname in listOfNameTuples:
455
+ listAlias.append(Make.astAlias(name, asname))
456
+ listAstImportFrom.append(Make.astImportFrom(module, listAlias))
457
+
458
+ listAstImport: list[ast.Import] = [Make.astImport(name) for name in sorted(set(self.listImport))]
459
+ return listAstImportFrom + listAstImport
460
+
461
+ def update(self, *fromLedger: 'LedgerOfImports') -> None:
462
+ """Update this ledger with imports from one or more other ledgers.
463
+ Parameters:
464
+ *fromLedger: One or more other `LedgerOfImports` objects from which to merge.
465
+ """
466
+ self.dictionaryImportFrom = updateExtendPolishDictionaryLists(self.dictionaryImportFrom, *(ledger.dictionaryImportFrom for ledger in fromLedger), destroyDuplicates=True, reorderLists=True)
467
+
468
+ for ledger in fromLedger:
469
+ self.listImport.extend(ledger.listImport)
470
+
471
+ def walkThis(self, walkThis: ast.AST) -> None:
472
+ for smurf in ast.walk(walkThis):
473
+ if isinstance(smurf, (ast.Import, ast.ImportFrom)):
474
+ self.addAst(smurf)
475
+
348
476
  class Then:
349
477
  @staticmethod
350
- def insertThisAbove(astStatement: ast.stmt) -> Callable[[ast.stmt], Sequence[ast.stmt]]:
351
- return lambda aboveMe: [astStatement, aboveMe]
478
+ def append_targetTo(listName: list[ast.AST]) -> Callable[[ast.AnnAssign], None]:
479
+ return lambda node: listName.append(node.target)
352
480
  @staticmethod
353
- def insertThisBelow(astStatement: ast.stmt) -> Callable[[ast.stmt], Sequence[ast.stmt]]:
354
- return lambda belowMe: [belowMe, astStatement]
481
+ def appendTo(listOfAny: list[Any]) -> Callable[[ast.AST], None]:
482
+ return lambda node: listOfAny.append(node)
355
483
  @staticmethod
356
- def replaceWith(astStatement: ast.stmt) -> Callable[[ast.stmt], ast.stmt]:
357
- return lambda replaceMe: astStatement
484
+ def insertThisAbove(list_astAST: Sequence[ast.AST]) -> Callable[[ast.AST], Sequence[ast.AST]]:
485
+ return lambda aboveMe: [*list_astAST, aboveMe]
358
486
  @staticmethod
359
- def removeThis(node: ast.AST) -> None:
487
+ def insertThisBelow(list_astAST: Sequence[ast.AST]) -> Callable[[ast.AST], Sequence[ast.AST]]:
488
+ return lambda belowMe: [belowMe, *list_astAST]
489
+ @staticmethod
490
+ def removeThis(_node: ast.AST) -> None:
360
491
  return None
361
- from mapFolding.someAssemblyRequired.whatWillBe import LedgerOfImports
362
- @staticmethod
363
- def Z0Z_ledger(logicalPath: strDotStrCuzPyStoopid, ledger: LedgerOfImports) -> Callable[[ast.AST], None]:
364
- return lambda node: ledger.addImportFromStr(logicalPath, cast(ast.Name, cast(ast.AnnAssign, node).annotation).id)
365
- @staticmethod
366
- def Z0Z_appendKeywordMirroredTo(list_keyword: list[ast.keyword]) -> Callable[[ast.AST], None]:
367
- return lambda node: list_keyword.append(Make.astKeyword(cast(ast.Name, cast(ast.AnnAssign, node).target).id, cast(ast.Name, cast(ast.AnnAssign, node).target)))
368
- @staticmethod
369
- def append_targetTo(listName: list[ast.Name]) -> Callable[[ast.AST], None]:
370
- return lambda node: listName.append(cast(ast.Name, cast(ast.AnnAssign, node).target))
371
- @staticmethod
372
- def appendTo(listAST: Sequence[ast.AST]) -> Callable[[ast.AST], None]:
373
- return lambda node: list(listAST).append(node)
374
- @staticmethod
375
- def Z0Z_appendAnnAssignOfNameDOTnameTo(identifier: ast_Identifier, listNameDOTname: list[ast.AnnAssign]) -> Callable[[ast.AST], None]:
376
- return lambda node: listNameDOTname.append(Make.astAnnAssign(cast(ast.AnnAssign, node).target, cast(ast.AnnAssign, node).annotation, Make.nameDOTname(identifier, cast(ast.Name, cast(ast.AnnAssign, node).target).id)))
377
-
378
- class FunctionInliner(ast.NodeTransformer):
379
- def __init__(self, dictionaryFunctions: dict[str, ast.FunctionDef]) -> None:
380
- self.dictionaryFunctions: dict[str, ast.FunctionDef] = dictionaryFunctions
381
-
382
- def inlineFunctionBody(self, callableTargetName: str) -> ast.FunctionDef:
383
- inlineDefinition: ast.FunctionDef = self.dictionaryFunctions[callableTargetName]
384
- # Process nested calls within the inlined function
385
- for astNode in ast.walk(inlineDefinition):
386
- self.visit(astNode)
387
- return inlineDefinition
388
-
389
- def visit_Call(self, node: ast.Call):
390
- astCall = self.generic_visit(node)
391
- if ifThis.CallDoesNotCallItselfAndNameDOTidIsIn(self.dictionaryFunctions)(astCall):
392
- inlineDefinition: ast.FunctionDef = self.inlineFunctionBody(cast(ast.Name, cast(ast.Call, astCall).func).id)
393
-
394
- if (inlineDefinition and inlineDefinition.body):
395
- statementTerminating: ast.stmt = inlineDefinition.body[-1]
396
-
397
- if (isinstance(statementTerminating, ast.Return)
398
- and statementTerminating.value is not None):
399
- return self.visit(statementTerminating.value)
400
- elif isinstance(statementTerminating, ast.Expr):
401
- return self.visit(statementTerminating.value)
402
- else:
403
- return ast.Constant(value=None)
404
- return astCall
405
-
406
- def visit_Expr(self, node: ast.Expr):
407
- if ifThis.CallDoesNotCallItselfAndNameDOTidIsIn(self.dictionaryFunctions)(node.value):
408
- inlineDefinition: ast.FunctionDef = self.inlineFunctionBody(cast(ast.Name, cast(ast.Call, node.value).func).id)
409
- return [self.visit(stmt) for stmt in inlineDefinition.body]
410
- return self.generic_visit(node)
411
- # TODO When resolving the ledger of imports, remove self-referential imports
492
+ @staticmethod
493
+ def replaceWith(astAST: ast.AST) -> Callable[[ast.AST], ast.AST]:
494
+ return lambda _replaceMe: astAST
495
+ @staticmethod
496
+ def updateThis(dictionaryOf_astMosDef: dict[ast_Identifier, astMosDef]) -> Callable[[astMosDef], astMosDef]:
497
+ return lambda node: dictionaryOf_astMosDef.setdefault(node.name, node)
498
+ @staticmethod
499
+ def Z0Z_ledger(logicalPath: strDotStrCuzPyStoopid, ledger: LedgerOfImports) -> Callable[[ast.AnnAssign], None]:
500
+ return lambda node: ledger.addImportFromStr(logicalPath, node.annotation.id) # type: ignore
501
+ @staticmethod
502
+ def Z0Z_appendKeywordMirroredTo(list_keyword: list[ast.keyword]) -> Callable[[ast.AnnAssign], None]:
503
+ return lambda node: list_keyword.append(Make.ast_keyword(node.target.id, node.target)) # type: ignore
504
+ @staticmethod
505
+ def Z0Z_appendAnnAssignOf_nameDOTnameTo(identifier: ast_Identifier, list_nameDOTname: list[ast.AnnAssign]) -> Callable[[ast.AnnAssign], None]:
506
+ return lambda node: list_nameDOTname.append(Make.astAnnAssign(node.target, node.annotation, Make.nameDOTname(identifier, node.target.id))) # type: ignore
507
+
508
+ @dataclasses.dataclass
509
+ class IngredientsFunction:
510
+ """Everything necessary to integrate a function into a module should be here."""
511
+ astFunctionDef: ast.FunctionDef # hint `Make.astFunctionDef`
512
+ imports: LedgerOfImports = dataclasses.field(default_factory=LedgerOfImports)
513
+
514
+ @dataclasses.dataclass
515
+ class IngredientsModule:
516
+ """Everything necessary to create one _logical_ `ast.Module` should be here.
517
+ Extrinsic qualities should _probably_ be handled externally."""
518
+ ingredientsFunction: dataclasses.InitVar[Sequence[IngredientsFunction] | IngredientsFunction | None] = None
519
+
520
+ # init var with an existing module? method to deconstruct an existing module?
521
+
522
+ # `body` attribute of `ast.Module`
523
+ imports: LedgerOfImports = dataclasses.field(default_factory=LedgerOfImports)
524
+ prologue: list[ast.stmt] = dataclasses.field(default_factory=list)
525
+ functions: list[ast.FunctionDef | ast.stmt] = dataclasses.field(default_factory=list)
526
+ epilogue: list[ast.stmt] = dataclasses.field(default_factory=list)
527
+ launcher: list[ast.stmt] = dataclasses.field(default_factory=list)
528
+
529
+ # parameter for `ast.Module` constructor
530
+ type_ignores: list[ast.TypeIgnore] = dataclasses.field(default_factory=list)
531
+
532
+ def __post_init__(self, ingredientsFunction: Sequence[IngredientsFunction] | IngredientsFunction | None = None) -> None:
533
+ if ingredientsFunction is not None:
534
+ if isinstance(ingredientsFunction, IngredientsFunction):
535
+ self.addIngredientsFunction(ingredientsFunction)
536
+ else:
537
+ self.addIngredientsFunction(*ingredientsFunction)
538
+
539
+ def addIngredientsFunction(self, *ingredientsFunction: IngredientsFunction) -> None:
540
+ """Add one or more `IngredientsFunction`."""
541
+ listLedgers: list[LedgerOfImports] = []
542
+ for definition in ingredientsFunction:
543
+ self.functions.append(definition.astFunctionDef)
544
+ listLedgers.append(definition.imports)
545
+ self.imports.update(*listLedgers)
546
+
547
+ def _makeModuleBody(self) -> list[ast.stmt]:
548
+ body: list[ast.stmt] = []
549
+ body.extend(self.imports.makeListAst())
550
+ body.extend(self.prologue)
551
+ body.extend(self.functions)
552
+ body.extend(self.epilogue)
553
+ body.extend(self.launcher)
554
+ # TODO `launcher`, if it exists, must start with `if __name__ == '__main__':` and be indented
555
+ return body
556
+
557
+ def export(self) -> ast.Module:
558
+ """Create a new `ast.Module` from the ingredients."""
559
+ return Make.astModule(self._makeModuleBody(), self.type_ignores)
560
+
561
+ @dataclasses.dataclass
562
+ class RecipeSynthesizeFlow:
563
+ """Settings for synthesizing flow."""
564
+ # ========================================
565
+ # Source
566
+ sourceAlgorithm: ModuleType = getSourceAlgorithm()
567
+ sourcePython: str = inspect_getsource(sourceAlgorithm)
568
+ source_astModule: ast.Module = ast.parse(sourcePython)
569
+
570
+ # Figure out dynamic flow control to synthesized modules https://github.com/hunterhogan/mapFolding/issues/4
571
+ sourceDispatcherCallable: str = theDispatcherCallable
572
+ sourceInitializeCallable: str = theSourceInitializeCallable
573
+ sourceParallelCallable: str = theSourceParallelCallable
574
+ sourceSequentialCallable: str = theSourceSequentialCallable
575
+
576
+ sourceDataclassIdentifier: str = theDataclassIdentifier
577
+ sourceDataclassInstance: str = theDataclassInstance
578
+ sourceDataclassInstanceTaskDistribution: str = theDataclassInstanceTaskDistribution
579
+ sourcePathModuleDataclass: str = theLogicalPathModuleDataclass
580
+
581
+ # ========================================
582
+ # Filesystem
583
+ pathPackage: PurePosixPath | None = PurePosixPath(thePathPackage)
584
+ fileExtension: str = theFileExtension
585
+
586
+ # ========================================
587
+ # Logical identifiers
588
+ # meta
589
+ formatStrModuleSynthetic: str = theFormatStrModuleSynthetic
590
+ formatStrModuleForCallableSynthetic: str = theFormatStrModuleForCallableSynthetic
591
+
592
+ # Package
593
+ packageName: ast_Identifier | None = thePackageName
594
+
595
+ # Module
596
+ # Figure out dynamic flow control to synthesized modules https://github.com/hunterhogan/mapFolding/issues/4
597
+ Z0Z_flowLogicalPathRoot: str = theModuleOfSyntheticModules
598
+ moduleDispatcher: str = theModuleDispatcherSynthetic
599
+ logicalPathModuleDataclass: str = sourcePathModuleDataclass
600
+ # Figure out dynamic flow control to synthesized modules https://github.com/hunterhogan/mapFolding/issues/4
601
+ # `theLogicalPathModuleDispatcherSynthetic` is a problem. It is defined in theSSOT, but it can also be calculated.
602
+ logicalPathModuleDispatcher: str = theLogicalPathModuleDispatcherSynthetic
603
+
604
+ # Function
605
+ dispatcherCallable: str = sourceDispatcherCallable
606
+ initializeCallable: str = sourceInitializeCallable
607
+ parallelCallable: str = sourceParallelCallable
608
+ sequentialCallable: str = sourceSequentialCallable
609
+
610
+ dataclassIdentifier: str = sourceDataclassIdentifier
611
+
612
+ # Variable
613
+ dataclassInstance: str = sourceDataclassInstance
614
+
615
+ def _makePathFilename(self, filenameStem: str,
616
+ pathRoot: PurePosixPath | None = None,
617
+ logicalPathINFIX: strDotStrCuzPyStoopid | None = None,
618
+ fileExtension: str | None = None,
619
+ ) -> PurePosixPath:
620
+ """filenameStem: (hint: the name of the logical module)"""
621
+ if pathRoot is None:
622
+ pathRoot = self.pathPackage or PurePosixPath(Path.cwd())
623
+ if logicalPathINFIX:
624
+ whyIsThisStillAThing: list[str] = logicalPathINFIX.split('.')
625
+ pathRoot = pathRoot.joinpath(*whyIsThisStillAThing)
626
+ if fileExtension is None:
627
+ fileExtension = self.fileExtension
628
+ filename: str = filenameStem + fileExtension
629
+ return pathRoot.joinpath(filename)
630
+
631
+ @property
632
+ def pathFilenameDispatcher(self) -> PurePosixPath:
633
+ return self._makePathFilename(filenameStem=self.moduleDispatcher, logicalPathINFIX=self.Z0Z_flowLogicalPathRoot)
412
634
 
413
635
  def extractClassDef(identifier: ast_Identifier, module: ast.Module) -> ast.ClassDef | None:
414
636
  sherpa: list[ast.ClassDef] = []
@@ -423,3 +645,136 @@ def extractFunctionDef(identifier: ast_Identifier, module: ast.Module) -> ast.Fu
423
645
  extractor.visit(module)
424
646
  astClassDef = sherpa[0] if sherpa else None
425
647
  return astClassDef
648
+
649
+ def makeDictionaryFunctionDef(module: ast.Module) -> dict[ast_Identifier, ast.FunctionDef]:
650
+ dictionaryFunctionDef: dict[ast_Identifier, ast.FunctionDef] = {}
651
+ NodeCollector(ifThis.isFunctionDef, [Then.updateThis(dictionaryFunctionDef)]).visit(module)
652
+ return dictionaryFunctionDef
653
+
654
+ def makeDictionaryReplacementStatements(module: ast.Module) -> dict[ast_Identifier, ast.stmt | list[ast.stmt]]:
655
+ """Return a dictionary of function names and their replacement statements."""
656
+ dictionaryFunctionDef: dict[ast_Identifier, ast.FunctionDef] = makeDictionaryFunctionDef(module)
657
+ dictionaryReplacementStatements: dict[ast_Identifier, ast.stmt | list[ast.stmt]] = {}
658
+ for name, astFunctionDef in dictionaryFunctionDef.items():
659
+ if ifThis.onlyReturnAnyCompare(astFunctionDef):
660
+ dictionaryReplacementStatements[name] = astFunctionDef.body[0].value # type: ignore
661
+ elif ifThis.onlyReturnUnaryOp(astFunctionDef):
662
+ dictionaryReplacementStatements[name] = astFunctionDef.body[0].value # type: ignore
663
+ else:
664
+ dictionaryReplacementStatements[name] = astFunctionDef.body[0:-1]
665
+ return dictionaryReplacementStatements
666
+
667
+ def Z0Z_descendantContainsMatchingNode(node: ast.AST, predicateFunction: Callable[[ast.AST], bool]) -> bool:
668
+ """Return True if any descendant of the node (or the node itself) matches the predicateFunction."""
669
+ matchFound = False
670
+
671
+ class DescendantFinder(ast.NodeVisitor):
672
+ def generic_visit(self, node: ast.AST) -> None:
673
+ nonlocal matchFound
674
+ if predicateFunction(node):
675
+ matchFound = True
676
+ else:
677
+ super().generic_visit(node)
678
+
679
+ DescendantFinder().visit(node)
680
+ return matchFound
681
+
682
+ def Z0Z_executeActionUnlessDescendantMatches(exclusionPredicate: Callable[[ast.AST], bool], actionFunction: Callable[[ast.AST], None]) -> Callable[[ast.AST], None]:
683
+ """Return a new action that will execute actionFunction only if no descendant (or the node itself) matches exclusionPredicate."""
684
+ def wrappedAction(node: ast.AST) -> None:
685
+ if not Z0Z_descendantContainsMatchingNode(node, exclusionPredicate):
686
+ actionFunction(node)
687
+ return wrappedAction
688
+
689
+ def inlineThisFunctionWithTheseValues(astFunctionDef: ast.FunctionDef, dictionaryReplacementStatements: dict[str, ast.stmt | list[ast.stmt]]) -> ast.FunctionDef:
690
+ class FunctionInliner(ast.NodeTransformer):
691
+ def __init__(self, dictionaryReplacementStatements: dict[str, ast.stmt | list[ast.stmt]]) -> None:
692
+ self.dictionaryReplacementStatements = dictionaryReplacementStatements
693
+
694
+ def generic_visit(self, node: ast.AST) -> ast.AST:
695
+ """Visit all nodes and replace them if necessary."""
696
+ return super().generic_visit(node)
697
+
698
+ def visit_Expr(self, node: ast.Expr) -> ast.AST | list[ast.stmt]:
699
+ if ifThis.CallDoesNotCallItselfAndNameDOTidIsIn(self.dictionaryReplacementStatements)(node.value):
700
+ return self.dictionaryReplacementStatements[node.value.func.id] # type: ignore[attr-defined]
701
+ return node
702
+
703
+ def visit_Assign(self, node: ast.Assign) -> ast.AST | list[ast.stmt]:
704
+ if ifThis.CallDoesNotCallItselfAndNameDOTidIsIn(self.dictionaryReplacementStatements)(node.value):
705
+ return self.dictionaryReplacementStatements[node.value.func.id] # type: ignore[attr-defined]
706
+ return node
707
+
708
+ def visit_Call(self, node: ast.Call) -> ast.AST | list[ast.stmt]:
709
+ if ifThis.CallDoesNotCallItselfAndNameDOTidIsIn(self.dictionaryReplacementStatements)(node):
710
+ replacement = self.dictionaryReplacementStatements[node.func.id] # type: ignore[attr-defined]
711
+ if not isinstance(replacement, list):
712
+ return replacement
713
+ return node
714
+
715
+ keepGoing = True
716
+ ImaInlineFunction = deepcopy(astFunctionDef)
717
+ while keepGoing:
718
+ ImaInlineFunction = deepcopy(astFunctionDef)
719
+ FunctionInliner(deepcopy(dictionaryReplacementStatements)).visit(ImaInlineFunction)
720
+ if ast.unparse(ImaInlineFunction) == ast.unparse(astFunctionDef):
721
+ keepGoing = False
722
+ else:
723
+ astFunctionDef = deepcopy(ImaInlineFunction)
724
+ ast.fix_missing_locations(astFunctionDef)
725
+ return ImaInlineFunction
726
+
727
+ def Z0Z_replaceMatchingASTnodes(astTree: ast.AST, mappingFindReplaceNodes: dict[ast.AST, ast.AST]) -> ast.AST:
728
+ class TargetedNodeReplacer(ast.NodeTransformer):
729
+ def __init__(self, mappingFindReplaceNodes: dict[ast.AST, ast.AST]) -> None:
730
+ self.mappingFindReplaceNodes = mappingFindReplaceNodes
731
+
732
+ def visit(self, node: ast.AST) -> ast.AST:
733
+ for nodeFind, nodeReplace in self.mappingFindReplaceNodes.items():
734
+ if self.nodesMatchStructurally(node, nodeFind):
735
+ return nodeReplace
736
+ return self.generic_visit(node)
737
+
738
+ def nodesMatchStructurally(self, nodeSubject: ast.AST | list[Any] | Any, nodePattern: ast.AST | list[Any] | Any) -> bool:
739
+ if nodeSubject is None or nodePattern is None:
740
+ return nodeSubject is None and nodePattern is None
741
+
742
+ if type(nodeSubject) != type(nodePattern):
743
+ return False
744
+
745
+ if isinstance(nodeSubject, ast.AST):
746
+ for field, fieldValueSubject in ast.iter_fields(nodeSubject):
747
+ if field in ('lineno', 'col_offset', 'end_lineno', 'end_col_offset', 'ctx'):
748
+ continue
749
+ attrPattern = getattr(nodePattern, field, None)
750
+ if not self.nodesMatchStructurally(fieldValueSubject, attrPattern):
751
+ return False
752
+ return True
753
+
754
+ if isinstance(nodeSubject, list) and isinstance(nodePattern, list):
755
+ nodeSubjectList: list[Any] = nodeSubject
756
+ nodePatternList: list[Any] = nodePattern
757
+ return len(nodeSubjectList) == len(nodePatternList) and all(
758
+ self.nodesMatchStructurally(elementSubject, elementPattern)
759
+ for elementSubject, elementPattern in zip(nodeSubjectList, nodePatternList)
760
+ )
761
+
762
+ return nodeSubject == nodePattern
763
+
764
+ astTreeCurrent, astTreePrevious = None, astTree
765
+ while astTreeCurrent is None or ast.unparse(astTreeCurrent) != ast.unparse(astTreePrevious):
766
+ astTreePrevious = astTreeCurrent if astTreeCurrent else astTree
767
+ astTreeCurrent = TargetedNodeReplacer(mappingFindReplaceNodes).visit(astTreePrevious)
768
+
769
+ return astTreeCurrent
770
+
771
+ def write_astModule(ingredients: IngredientsModule, pathFilename: str | PathLike[Any] | PurePath, packageName: ast_Identifier | None = None) -> None:
772
+ astModule = ingredients.export()
773
+ ast.fix_missing_locations(astModule)
774
+ pythonSource: str = ast.unparse(astModule)
775
+ if not pythonSource: raise raiseIfNoneGitHubIssueNumber3
776
+ autoflake_additional_imports: list[str] = ingredients.imports.exportListModuleNames()
777
+ if packageName:
778
+ autoflake_additional_imports.append(packageName)
779
+ pythonSource = autoflake_fix_code(pythonSource, autoflake_additional_imports, expand_star_imports=False, remove_all_unused_imports=False, remove_duplicate_keys = False, remove_unused_variables = False)
780
+ writeStringToHere(pythonSource, pathFilename)