fluent-codegen 0.2__tar.gz → 0.3.0__tar.gz
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.
- {fluent_codegen-0.2/src/fluent_codegen.egg-info → fluent_codegen-0.3.0}/PKG-INFO +2 -2
- {fluent_codegen-0.2 → fluent_codegen-0.3.0}/pyproject.toml +3 -3
- {fluent_codegen-0.2 → fluent_codegen-0.3.0}/src/fluent_codegen/codegen.py +50 -71
- fluent_codegen-0.3.0/src/fluent_codegen/remove_unused_assignments.py +111 -0
- {fluent_codegen-0.2 → fluent_codegen-0.3.0/src/fluent_codegen.egg-info}/PKG-INFO +2 -2
- {fluent_codegen-0.2 → fluent_codegen-0.3.0}/src/fluent_codegen.egg-info/SOURCES.txt +3 -1
- {fluent_codegen-0.2 → fluent_codegen-0.3.0}/tests/test_codegen.py +498 -5
- fluent_codegen-0.3.0/tests/test_remove_unused.py +166 -0
- {fluent_codegen-0.2 → fluent_codegen-0.3.0}/LICENSE +0 -0
- {fluent_codegen-0.2 → fluent_codegen-0.3.0}/README.rst +0 -0
- {fluent_codegen-0.2 → fluent_codegen-0.3.0}/setup.cfg +0 -0
- {fluent_codegen-0.2 → fluent_codegen-0.3.0}/src/fluent_codegen/__init__.py +0 -0
- {fluent_codegen-0.2 → fluent_codegen-0.3.0}/src/fluent_codegen/ast_compat.py +0 -0
- {fluent_codegen-0.2 → fluent_codegen-0.3.0}/src/fluent_codegen/py.typed +0 -0
- {fluent_codegen-0.2 → fluent_codegen-0.3.0}/src/fluent_codegen/utils.py +0 -0
- {fluent_codegen-0.2 → fluent_codegen-0.3.0}/src/fluent_codegen.egg-info/dependency_links.txt +0 -0
- {fluent_codegen-0.2 → fluent_codegen-0.3.0}/src/fluent_codegen.egg-info/top_level.txt +0 -0
- {fluent_codegen-0.2 → fluent_codegen-0.3.0}/tests/test_fizzbuzz_example.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fluent-codegen
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A Python library for generating Python code via AST construction.
|
|
5
5
|
Author-email: Luke Plant <luke@lukeplant.me.uk>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Repository, https://github.com/spookylukey/fluent-codegen
|
|
8
8
|
Keywords: codegen,code-generation,ast,python,metaprogramming
|
|
9
|
-
Classifier: Development Status ::
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
@@ -14,7 +14,7 @@ keywords = [
|
|
|
14
14
|
"metaprogramming",
|
|
15
15
|
]
|
|
16
16
|
classifiers = [
|
|
17
|
-
"Development Status ::
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
18
|
"Intended Audience :: Developers",
|
|
19
19
|
"Operating System :: OS Independent",
|
|
20
20
|
"Programming Language :: Python :: 3 :: Only",
|
|
@@ -24,7 +24,7 @@ classifiers = [
|
|
|
24
24
|
"Topic :: Software Development :: Code Generators",
|
|
25
25
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
26
|
]
|
|
27
|
-
version = "0.
|
|
27
|
+
version = "0.3.0"
|
|
28
28
|
dependencies = []
|
|
29
29
|
|
|
30
30
|
[project.urls]
|
|
@@ -74,7 +74,7 @@ dev = [
|
|
|
74
74
|
"pyright==1.1.406",
|
|
75
75
|
"pytest>=7.4.4",
|
|
76
76
|
"pytest-cov>=7.0.0",
|
|
77
|
-
"ruff>=0.
|
|
77
|
+
"ruff>=0.15.1",
|
|
78
78
|
"tox>=4.36.0",
|
|
79
79
|
"tox-uv>=1.29.0",
|
|
80
80
|
]
|
|
@@ -10,7 +10,7 @@ import keyword
|
|
|
10
10
|
import re
|
|
11
11
|
import textwrap
|
|
12
12
|
from abc import ABC, abstractmethod
|
|
13
|
-
from collections.abc import Callable, Sequence
|
|
13
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
14
14
|
from dataclasses import dataclass
|
|
15
15
|
from typing import ClassVar, Protocol, assert_never, overload, runtime_checkable
|
|
16
16
|
|
|
@@ -102,8 +102,6 @@ class CodeGenAst(ABC):
|
|
|
102
102
|
@abstractmethod
|
|
103
103
|
def as_ast(self, *, include_comments: bool = False) -> py_ast.AST: ...
|
|
104
104
|
|
|
105
|
-
child_elements: ClassVar[list[str]]
|
|
106
|
-
|
|
107
105
|
def as_python_source(self) -> str:
|
|
108
106
|
"""Return the Python source code for this AST node."""
|
|
109
107
|
node = self.as_ast(include_comments=True)
|
|
@@ -120,8 +118,6 @@ class CodeGenAstList(ABC):
|
|
|
120
118
|
@abstractmethod
|
|
121
119
|
def as_ast_list(self, allow_empty: bool = True, *, include_comments: bool = False) -> list[py_ast.stmt]: ...
|
|
122
120
|
|
|
123
|
-
child_elements: ClassVar[list[str]]
|
|
124
|
-
|
|
125
121
|
def as_python_source(self) -> str:
|
|
126
122
|
"""Return the Python source code for this AST list."""
|
|
127
123
|
mod = py_ast.Module(body=self.as_ast_list(include_comments=True), type_ignores=[], **DEFAULT_AST_ARGS_MODULE)
|
|
@@ -258,11 +254,9 @@ class SupportsNameAssignment(Protocol):
|
|
|
258
254
|
def has_assignment_for_name(self, name: str) -> bool: ...
|
|
259
255
|
|
|
260
256
|
|
|
261
|
-
class
|
|
257
|
+
class Annotation(Statement):
|
|
262
258
|
"""A bare type annotation without a value, e.g. ``x: int``."""
|
|
263
259
|
|
|
264
|
-
child_elements = []
|
|
265
|
-
|
|
266
260
|
def __init__(self, name: str, annotation: Expression):
|
|
267
261
|
self.name = name
|
|
268
262
|
self.annotation = annotation
|
|
@@ -282,9 +276,7 @@ class _Annotation(Statement):
|
|
|
282
276
|
return self.name == name
|
|
283
277
|
|
|
284
278
|
|
|
285
|
-
class
|
|
286
|
-
child_elements = ["value"]
|
|
287
|
-
|
|
279
|
+
class Assignment(Statement):
|
|
288
280
|
def __init__(self, name: str, value: Expression, /, *, type_hint: Expression | None = None):
|
|
289
281
|
self.name = name
|
|
290
282
|
self.value = value
|
|
@@ -313,8 +305,6 @@ class _Assignment(Statement):
|
|
|
313
305
|
|
|
314
306
|
|
|
315
307
|
class Block(CodeGenAstList):
|
|
316
|
-
child_elements = ["statements"]
|
|
317
|
-
|
|
318
308
|
def __init__(self, scope: Scope, parent_block: Block | None = None):
|
|
319
309
|
self.scope = scope
|
|
320
310
|
# We allow `Expression` here for things like MethodCall which
|
|
@@ -459,7 +449,7 @@ class Block(CodeGenAstList):
|
|
|
459
449
|
else:
|
|
460
450
|
self.scope.register_assignment(name)
|
|
461
451
|
|
|
462
|
-
self.add_statement(
|
|
452
|
+
self.add_statement(Assignment(name, value, type_hint=type_hint))
|
|
463
453
|
|
|
464
454
|
def create_annotation(self, name: str, annotation: Expression) -> Name:
|
|
465
455
|
"""
|
|
@@ -471,7 +461,7 @@ class Block(CodeGenAstList):
|
|
|
471
461
|
"""
|
|
472
462
|
name_obj = self.scope.create_name(name)
|
|
473
463
|
self.scope.register_assignment(name_obj.name)
|
|
474
|
-
self.add_statement(
|
|
464
|
+
self.add_statement(Annotation(name_obj.name, annotation))
|
|
475
465
|
return name_obj
|
|
476
466
|
|
|
477
467
|
def create_field(self, name: str, annotation: Expression, *, default: Expression | None = None) -> Name:
|
|
@@ -489,7 +479,7 @@ class Block(CodeGenAstList):
|
|
|
489
479
|
if default is not None:
|
|
490
480
|
name_obj = self.scope.create_name(name)
|
|
491
481
|
self.scope.register_assignment(name_obj.name)
|
|
492
|
-
self.add_statement(
|
|
482
|
+
self.add_statement(Assignment(name_obj.name, default, type_hint=annotation))
|
|
493
483
|
return name_obj
|
|
494
484
|
else:
|
|
495
485
|
return self.create_annotation(name, annotation)
|
|
@@ -669,8 +659,6 @@ def _validate_arg_order(args: list[FunctionArg]) -> None:
|
|
|
669
659
|
|
|
670
660
|
|
|
671
661
|
class Function(Scope, Statement):
|
|
672
|
-
child_elements = ["body"]
|
|
673
|
-
|
|
674
662
|
def __init__(
|
|
675
663
|
self,
|
|
676
664
|
name: str,
|
|
@@ -688,6 +676,11 @@ class Function(Scope, Statement):
|
|
|
688
676
|
if args is not None:
|
|
689
677
|
self.add_args(args)
|
|
690
678
|
|
|
679
|
+
@property
|
|
680
|
+
def args(self) -> Sequence[FunctionArg]:
|
|
681
|
+
"""Return the function's arguments as a read-only sequence."""
|
|
682
|
+
return tuple(self._args)
|
|
683
|
+
|
|
691
684
|
def add_args(self, args: Sequence[str | FunctionArg]) -> None:
|
|
692
685
|
"""Add arguments to the function, with the same validation as in __init__."""
|
|
693
686
|
normalized = _normalize_args(args)
|
|
@@ -750,8 +743,6 @@ class Function(Scope, Statement):
|
|
|
750
743
|
|
|
751
744
|
|
|
752
745
|
class Class(Scope, Statement):
|
|
753
|
-
child_elements = ["body"]
|
|
754
|
-
|
|
755
746
|
def __init__(
|
|
756
747
|
self,
|
|
757
748
|
name: str,
|
|
@@ -780,8 +771,6 @@ class Class(Scope, Statement):
|
|
|
780
771
|
|
|
781
772
|
|
|
782
773
|
class Return(Statement):
|
|
783
|
-
child_elements = ["value"]
|
|
784
|
-
|
|
785
774
|
def __init__(self, value: Expression):
|
|
786
775
|
self.value = value
|
|
787
776
|
|
|
@@ -795,8 +784,6 @@ class Return(Statement):
|
|
|
795
784
|
class Assert(Statement):
|
|
796
785
|
"""An ``assert`` statement with an optional message."""
|
|
797
786
|
|
|
798
|
-
child_elements = ["test", "msg"]
|
|
799
|
-
|
|
800
787
|
def __init__(self, test: Expression, msg: Expression | None = None):
|
|
801
788
|
self.test = test
|
|
802
789
|
self.msg = msg
|
|
@@ -814,8 +801,6 @@ class Assert(Statement):
|
|
|
814
801
|
|
|
815
802
|
|
|
816
803
|
class If(Statement):
|
|
817
|
-
child_elements = ["if_blocks", "conditions", "else_block"]
|
|
818
|
-
|
|
819
804
|
def __init__(self, parent_scope: Scope, parent_block: Block | None = None):
|
|
820
805
|
# We model a "compound if statement" as a list of if blocks
|
|
821
806
|
# (if/elif/elif etc), each with their own condition, with a final else
|
|
@@ -867,8 +852,6 @@ class If(Statement):
|
|
|
867
852
|
|
|
868
853
|
|
|
869
854
|
class With(Statement):
|
|
870
|
-
child_elements = ["context_expr", "body"]
|
|
871
|
-
|
|
872
855
|
def __init__(
|
|
873
856
|
self,
|
|
874
857
|
context_expr: Expression,
|
|
@@ -901,8 +884,6 @@ class With(Statement):
|
|
|
901
884
|
|
|
902
885
|
|
|
903
886
|
class Try(Statement):
|
|
904
|
-
child_elements = ["catch_exceptions", "try_block", "except_block", "else_block"]
|
|
905
|
-
|
|
906
887
|
def __init__(self, catch_exceptions: Sequence[Expression], parent_scope: Scope):
|
|
907
888
|
self.catch_exceptions = catch_exceptions
|
|
908
889
|
self.try_block = Block(parent_scope)
|
|
@@ -1014,7 +995,7 @@ class Expression(CodeGenAst):
|
|
|
1014
995
|
def call(
|
|
1015
996
|
self,
|
|
1016
997
|
args: Sequence[Expression] | None = None,
|
|
1017
|
-
kwargs:
|
|
998
|
+
kwargs: Mapping[str, Expression] | None = None,
|
|
1018
999
|
) -> Call:
|
|
1019
1000
|
return Call(self, args or [], kwargs or {})
|
|
1020
1001
|
|
|
@@ -1022,7 +1003,7 @@ class Expression(CodeGenAst):
|
|
|
1022
1003
|
self,
|
|
1023
1004
|
attribute: str,
|
|
1024
1005
|
args: Sequence[Expression] | None = None,
|
|
1025
|
-
kwargs:
|
|
1006
|
+
kwargs: Mapping[str, Expression] | None = None,
|
|
1026
1007
|
) -> Call:
|
|
1027
1008
|
return self.attr(attribute).call(args, kwargs)
|
|
1028
1009
|
|
|
@@ -1098,8 +1079,6 @@ class Expression(CodeGenAst):
|
|
|
1098
1079
|
|
|
1099
1080
|
|
|
1100
1081
|
class String(Expression):
|
|
1101
|
-
child_elements = []
|
|
1102
|
-
|
|
1103
1082
|
def __init__(self, string_value: str):
|
|
1104
1083
|
self.string_value = string_value
|
|
1105
1084
|
|
|
@@ -1118,8 +1097,6 @@ class String(Expression):
|
|
|
1118
1097
|
|
|
1119
1098
|
|
|
1120
1099
|
class Bool(Expression):
|
|
1121
|
-
child_elements = []
|
|
1122
|
-
|
|
1123
1100
|
def __init__(self, value: bool):
|
|
1124
1101
|
self.value = value
|
|
1125
1102
|
|
|
@@ -1131,8 +1108,6 @@ class Bool(Expression):
|
|
|
1131
1108
|
|
|
1132
1109
|
|
|
1133
1110
|
class Bytes(Expression):
|
|
1134
|
-
child_elements = []
|
|
1135
|
-
|
|
1136
1111
|
def __init__(self, value: bytes):
|
|
1137
1112
|
self.value = value
|
|
1138
1113
|
|
|
@@ -1144,8 +1119,6 @@ class Bytes(Expression):
|
|
|
1144
1119
|
|
|
1145
1120
|
|
|
1146
1121
|
class Number(Expression):
|
|
1147
|
-
child_elements = []
|
|
1148
|
-
|
|
1149
1122
|
def __init__(self, number: int | float):
|
|
1150
1123
|
self.number = number
|
|
1151
1124
|
|
|
@@ -1157,9 +1130,7 @@ class Number(Expression):
|
|
|
1157
1130
|
|
|
1158
1131
|
|
|
1159
1132
|
class List(Expression):
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
def __init__(self, items: list[Expression]):
|
|
1133
|
+
def __init__(self, items: Sequence[Expression]):
|
|
1163
1134
|
self.items = items
|
|
1164
1135
|
|
|
1165
1136
|
def as_ast(self, *, include_comments: bool = False) -> py_ast.expr:
|
|
@@ -1167,8 +1138,6 @@ class List(Expression):
|
|
|
1167
1138
|
|
|
1168
1139
|
|
|
1169
1140
|
class Tuple(Expression):
|
|
1170
|
-
child_elements = ["items"]
|
|
1171
|
-
|
|
1172
1141
|
def __init__(self, items: Sequence[Expression]):
|
|
1173
1142
|
self.items = items
|
|
1174
1143
|
|
|
@@ -1177,8 +1146,6 @@ class Tuple(Expression):
|
|
|
1177
1146
|
|
|
1178
1147
|
|
|
1179
1148
|
class Set(Expression):
|
|
1180
|
-
child_elements = ["items"]
|
|
1181
|
-
|
|
1182
1149
|
def __init__(self, items: Sequence[Expression]):
|
|
1183
1150
|
self.items = items
|
|
1184
1151
|
|
|
@@ -1195,8 +1162,6 @@ class Set(Expression):
|
|
|
1195
1162
|
|
|
1196
1163
|
|
|
1197
1164
|
class Dict(Expression):
|
|
1198
|
-
child_elements = ["pairs"]
|
|
1199
|
-
|
|
1200
1165
|
def __init__(self, pairs: Sequence[tuple[Expression, Expression]]):
|
|
1201
1166
|
self.pairs = pairs
|
|
1202
1167
|
|
|
@@ -1209,8 +1174,6 @@ class Dict(Expression):
|
|
|
1209
1174
|
|
|
1210
1175
|
|
|
1211
1176
|
class StringJoinBase(Expression):
|
|
1212
|
-
child_elements = ["parts"]
|
|
1213
|
-
|
|
1214
1177
|
def __init__(self, parts: Sequence[Expression]):
|
|
1215
1178
|
self.parts = parts
|
|
1216
1179
|
|
|
@@ -1280,8 +1243,6 @@ StringJoin = FStringJoin
|
|
|
1280
1243
|
|
|
1281
1244
|
|
|
1282
1245
|
class Name(Expression):
|
|
1283
|
-
child_elements = []
|
|
1284
|
-
|
|
1285
1246
|
def __init__(self, name: str, scope: Scope):
|
|
1286
1247
|
if not scope.is_name_in_use(name):
|
|
1287
1248
|
raise AssertionError(f"Cannot refer to undefined name '{name}'")
|
|
@@ -1300,8 +1261,6 @@ class Name(Expression):
|
|
|
1300
1261
|
|
|
1301
1262
|
|
|
1302
1263
|
class Attr(Expression):
|
|
1303
|
-
child_elements = ["value"]
|
|
1304
|
-
|
|
1305
1264
|
def __init__(self, value: Expression, attribute: str) -> None:
|
|
1306
1265
|
self.value = value
|
|
1307
1266
|
if not allowable_name(attribute, allow_builtin=True):
|
|
@@ -1313,8 +1272,6 @@ class Attr(Expression):
|
|
|
1313
1272
|
|
|
1314
1273
|
|
|
1315
1274
|
class Starred(Expression):
|
|
1316
|
-
child_elements = ["value"]
|
|
1317
|
-
|
|
1318
1275
|
def __init__(self, value: Expression):
|
|
1319
1276
|
self.value = value
|
|
1320
1277
|
|
|
@@ -1328,7 +1285,7 @@ class Starred(Expression):
|
|
|
1328
1285
|
def function_call(
|
|
1329
1286
|
function_name: str,
|
|
1330
1287
|
args: Sequence[Expression],
|
|
1331
|
-
kwargs:
|
|
1288
|
+
kwargs: Mapping[str, Expression],
|
|
1332
1289
|
scope: Scope,
|
|
1333
1290
|
) -> Expression:
|
|
1334
1291
|
if not scope.is_name_in_use(function_name):
|
|
@@ -1340,17 +1297,15 @@ def function_call(
|
|
|
1340
1297
|
|
|
1341
1298
|
|
|
1342
1299
|
class Call(Expression):
|
|
1343
|
-
child_elements = ["value", "args", "kwargs"]
|
|
1344
|
-
|
|
1345
1300
|
def __init__(
|
|
1346
1301
|
self,
|
|
1347
1302
|
value: Expression,
|
|
1348
1303
|
args: Sequence[Expression],
|
|
1349
|
-
kwargs:
|
|
1304
|
+
kwargs: Mapping[str, Expression],
|
|
1350
1305
|
):
|
|
1351
1306
|
self.value = value
|
|
1352
1307
|
self.args = list(args)
|
|
1353
|
-
self.kwargs = kwargs
|
|
1308
|
+
self.kwargs = dict(kwargs)
|
|
1354
1309
|
|
|
1355
1310
|
def as_ast(self, *, include_comments: bool = False) -> py_ast.expr:
|
|
1356
1311
|
|
|
@@ -1406,14 +1361,12 @@ def method_call(
|
|
|
1406
1361
|
obj: Expression,
|
|
1407
1362
|
method_name: str,
|
|
1408
1363
|
args: Sequence[Expression],
|
|
1409
|
-
kwargs:
|
|
1364
|
+
kwargs: Mapping[str, Expression],
|
|
1410
1365
|
):
|
|
1411
1366
|
return obj.attr(method_name).call(args=args, kwargs=kwargs)
|
|
1412
1367
|
|
|
1413
1368
|
|
|
1414
1369
|
class Subscript(Expression):
|
|
1415
|
-
child_elements = ["value", "slice"]
|
|
1416
|
-
|
|
1417
1370
|
def __init__(self, value: Expression, slice: Expression):
|
|
1418
1371
|
self.value = value
|
|
1419
1372
|
self.slice = slice
|
|
@@ -1436,8 +1389,6 @@ class NoneExpr(Expression):
|
|
|
1436
1389
|
|
|
1437
1390
|
|
|
1438
1391
|
class BinaryOperator(Expression):
|
|
1439
|
-
child_elements = ["left", "right"]
|
|
1440
|
-
|
|
1441
1392
|
def __init__(self, left: Expression, right: Expression):
|
|
1442
1393
|
self.left = left
|
|
1443
1394
|
self.right = right
|
|
@@ -1579,19 +1530,47 @@ def simplify(codegen_ast: CodeGenAstType, simplifier: Callable[[CodeGenAstType,
|
|
|
1579
1530
|
def rewriting_traverse(
|
|
1580
1531
|
node: CodeGenAstType | Sequence[CodeGenAstType],
|
|
1581
1532
|
func: Callable[[CodeGenAstType], CodeGenAstType],
|
|
1533
|
+
_visited: set[int] | None = None,
|
|
1582
1534
|
):
|
|
1583
1535
|
"""
|
|
1584
|
-
Apply 'func' to node and all sub CodeGenAst nodes
|
|
1536
|
+
Apply 'func' to node and all sub CodeGenAst nodes.
|
|
1537
|
+
|
|
1538
|
+
Discovers child nodes by introspecting instance attributes rather than
|
|
1539
|
+
relying on a manually-maintained list. A *visited* set (keyed by
|
|
1540
|
+
``id()``) prevents infinite recursion through circular references
|
|
1541
|
+
(e.g. Block.scope → Function → body → Block).
|
|
1585
1542
|
"""
|
|
1543
|
+
if _visited is None:
|
|
1544
|
+
_visited = set()
|
|
1586
1545
|
if isinstance(node, (CodeGenAst, CodeGenAstList)):
|
|
1546
|
+
node_id = id(node)
|
|
1547
|
+
if node_id in _visited:
|
|
1548
|
+
return
|
|
1549
|
+
_visited.add(node_id)
|
|
1587
1550
|
new_node = func(node)
|
|
1588
1551
|
if new_node is not node:
|
|
1589
1552
|
morph_into(node, new_node)
|
|
1590
|
-
for
|
|
1591
|
-
|
|
1553
|
+
for value in node.__dict__.values():
|
|
1554
|
+
_traverse_value(value, func, _visited)
|
|
1592
1555
|
elif isinstance(node, (list, tuple)):
|
|
1593
1556
|
for i in node:
|
|
1594
|
-
rewriting_traverse(i, func)
|
|
1557
|
+
rewriting_traverse(i, func, _visited)
|
|
1558
|
+
|
|
1559
|
+
|
|
1560
|
+
def _traverse_value(
|
|
1561
|
+
value: object,
|
|
1562
|
+
func: Callable[[CodeGenAstType], CodeGenAstType],
|
|
1563
|
+
visited: set[int],
|
|
1564
|
+
) -> None:
|
|
1565
|
+
"""Recurse into a single attribute value, handling containers."""
|
|
1566
|
+
if isinstance(value, (CodeGenAst, CodeGenAstList)):
|
|
1567
|
+
rewriting_traverse(value, func, visited)
|
|
1568
|
+
elif isinstance(value, (list, tuple)):
|
|
1569
|
+
for item in value: # type: ignore[reportUnknownVariableType]
|
|
1570
|
+
_traverse_value(item, func, visited) # type: ignore[reportUnknownVariableType]
|
|
1571
|
+
elif isinstance(value, dict):
|
|
1572
|
+
for v in value.values(): # type: ignore[reportUnknownVariableType]
|
|
1573
|
+
_traverse_value(v, func, visited) # type: ignore[reportUnknownVariableType]
|
|
1595
1574
|
|
|
1596
1575
|
|
|
1597
1576
|
def morph_into(item: object, new_item: object) -> None:
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility to remove unused assignments from a Function body.
|
|
3
|
+
|
|
4
|
+
Uses ``rewriting_traverse`` to walk the codegen AST. Only handles a single
|
|
5
|
+
function scope — raises ``AssertionError`` if a nested ``Scope`` (e.g. inner
|
|
6
|
+
function or class) is encountered.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .codegen import (
|
|
12
|
+
Assignment,
|
|
13
|
+
Block,
|
|
14
|
+
CodeGenAst,
|
|
15
|
+
CodeGenAstList,
|
|
16
|
+
CodeGenAstType,
|
|
17
|
+
Function,
|
|
18
|
+
Name,
|
|
19
|
+
Scope,
|
|
20
|
+
rewriting_traverse,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _collect_blocks(func: Function) -> list[Block]:
|
|
25
|
+
"""Return every Block reachable from *func*.body, checking for nested scopes."""
|
|
26
|
+
blocks: list[Block] = []
|
|
27
|
+
seen: set[int] = set()
|
|
28
|
+
|
|
29
|
+
def _walk(obj: object) -> None:
|
|
30
|
+
obj_id = id(obj)
|
|
31
|
+
if obj_id in seen:
|
|
32
|
+
return
|
|
33
|
+
seen.add(obj_id)
|
|
34
|
+
|
|
35
|
+
if isinstance(obj, Scope) and obj is not func:
|
|
36
|
+
if type(obj) is not Scope:
|
|
37
|
+
raise AssertionError(f"remove_unused_assignments does not handle nested Scopes ({type(obj).__name__})")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
if isinstance(obj, Block):
|
|
41
|
+
blocks.append(obj)
|
|
42
|
+
|
|
43
|
+
# Recurse into attributes, finding Block children generically.
|
|
44
|
+
if isinstance(obj, (CodeGenAst, CodeGenAstList)):
|
|
45
|
+
for value in obj.__dict__.values():
|
|
46
|
+
_walk_value(value)
|
|
47
|
+
|
|
48
|
+
def _walk_value(value: object) -> None:
|
|
49
|
+
if isinstance(value, (CodeGenAst, CodeGenAstList, Scope)):
|
|
50
|
+
_walk(value)
|
|
51
|
+
elif isinstance(value, (list, tuple)):
|
|
52
|
+
for item in value: # type: ignore[reportUnknownVariableType]
|
|
53
|
+
_walk_value(item) # type: ignore[reportUnknownVariableType]
|
|
54
|
+
|
|
55
|
+
# Start from statements inside the body (not the Function itself, which is
|
|
56
|
+
# a Scope we need to skip).
|
|
57
|
+
for stmt in func.body.statements:
|
|
58
|
+
_walk_value(stmt)
|
|
59
|
+
# Also include the body block itself.
|
|
60
|
+
blocks.insert(0, func.body)
|
|
61
|
+
return blocks
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _collect_name_references(func: Function) -> set[str]:
|
|
65
|
+
"""Return the set of all name strings referenced as ``Name`` expressions."""
|
|
66
|
+
names: set[str] = set()
|
|
67
|
+
|
|
68
|
+
def _visitor(node: CodeGenAstType) -> CodeGenAstType:
|
|
69
|
+
if isinstance(node, Name):
|
|
70
|
+
names.add(node.name)
|
|
71
|
+
return node
|
|
72
|
+
|
|
73
|
+
rewriting_traverse(func.body, _visitor)
|
|
74
|
+
return names
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _assigned_names(blocks: list[Block]) -> set[str]:
|
|
78
|
+
"""Return all names that appear on the LHS of an ``_Assignment``."""
|
|
79
|
+
result: set[str] = set()
|
|
80
|
+
for block in blocks:
|
|
81
|
+
for stmt in block.statements:
|
|
82
|
+
if isinstance(stmt, Assignment):
|
|
83
|
+
result.add(stmt.name)
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _remove_once(func: Function, blocks: list[Block]) -> bool:
|
|
88
|
+
"""Remove unused assignment statements. Returns True if anything changed."""
|
|
89
|
+
referenced = _collect_name_references(func)
|
|
90
|
+
assigned = _assigned_names(blocks)
|
|
91
|
+
unused = assigned - referenced
|
|
92
|
+
if not unused:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
for block in blocks:
|
|
96
|
+
block.statements = [
|
|
97
|
+
stmt for stmt in block.statements if not (isinstance(stmt, Assignment) and stmt.name in unused)
|
|
98
|
+
]
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def remove_unused_assignments(func: Function) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Remove statements that assign to a name which is never read.
|
|
105
|
+
|
|
106
|
+
Works recursively: if removing ``x = y`` makes ``y`` unused, it will be
|
|
107
|
+
removed too. Raises ``AssertionError`` on nested scopes.
|
|
108
|
+
"""
|
|
109
|
+
blocks = _collect_blocks(func)
|
|
110
|
+
while _remove_once(func, blocks):
|
|
111
|
+
pass
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fluent-codegen
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A Python library for generating Python code via AST construction.
|
|
5
5
|
Author-email: Luke Plant <luke@lukeplant.me.uk>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Repository, https://github.com/spookylukey/fluent-codegen
|
|
8
8
|
Keywords: codegen,code-generation,ast,python,metaprogramming
|
|
9
|
-
Classifier: Development Status ::
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
@@ -5,10 +5,12 @@ src/fluent_codegen/__init__.py
|
|
|
5
5
|
src/fluent_codegen/ast_compat.py
|
|
6
6
|
src/fluent_codegen/codegen.py
|
|
7
7
|
src/fluent_codegen/py.typed
|
|
8
|
+
src/fluent_codegen/remove_unused_assignments.py
|
|
8
9
|
src/fluent_codegen/utils.py
|
|
9
10
|
src/fluent_codegen.egg-info/PKG-INFO
|
|
10
11
|
src/fluent_codegen.egg-info/SOURCES.txt
|
|
11
12
|
src/fluent_codegen.egg-info/dependency_links.txt
|
|
12
13
|
src/fluent_codegen.egg-info/top_level.txt
|
|
13
14
|
tests/test_codegen.py
|
|
14
|
-
tests/test_fizzbuzz_example.py
|
|
15
|
+
tests/test_fizzbuzz_example.py
|
|
16
|
+
tests/test_remove_unused.py
|
|
@@ -268,6 +268,65 @@ def test_function_add_args_reserves_name():
|
|
|
268
268
|
assert_code_equal(ref, "my_arg")
|
|
269
269
|
|
|
270
270
|
|
|
271
|
+
def test_function_args_property_empty():
|
|
272
|
+
module = codegen.Module()
|
|
273
|
+
func_name = module.scope.reserve_name("myfunc")
|
|
274
|
+
func = codegen.Function(func_name, parent_scope=module.scope)
|
|
275
|
+
assert func.args == ()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_function_args_property():
|
|
279
|
+
module = codegen.Module()
|
|
280
|
+
func_name = module.scope.reserve_name("myfunc")
|
|
281
|
+
func = codegen.Function(func_name, args=["a", "b"], parent_scope=module.scope)
|
|
282
|
+
assert len(func.args) == 2
|
|
283
|
+
assert func.args[0].name == "a"
|
|
284
|
+
assert func.args[1].name == "b"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_function_args_property_with_function_arg():
|
|
288
|
+
module = codegen.Module()
|
|
289
|
+
func_name = module.scope.reserve_name("myfunc")
|
|
290
|
+
func = codegen.Function(
|
|
291
|
+
func_name,
|
|
292
|
+
args=[
|
|
293
|
+
codegen.FunctionArg.positional("x"),
|
|
294
|
+
"y",
|
|
295
|
+
codegen.FunctionArg.keyword("z", default=codegen.Number(1)),
|
|
296
|
+
],
|
|
297
|
+
parent_scope=module.scope,
|
|
298
|
+
)
|
|
299
|
+
assert len(func.args) == 3
|
|
300
|
+
assert func.args[0].name == "x"
|
|
301
|
+
assert func.args[0].kind == codegen.ArgKind.POSITIONAL_ONLY
|
|
302
|
+
assert func.args[1].name == "y"
|
|
303
|
+
assert func.args[1].kind == codegen.ArgKind.POSITIONAL_OR_KEYWORD
|
|
304
|
+
assert func.args[2].name == "z"
|
|
305
|
+
assert func.args[2].kind == codegen.ArgKind.KEYWORD_ONLY
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_function_args_property_after_add_args():
|
|
309
|
+
module = codegen.Module()
|
|
310
|
+
func_name = module.scope.reserve_name("myfunc")
|
|
311
|
+
func = codegen.Function(func_name, args=["a"], parent_scope=module.scope)
|
|
312
|
+
assert len(func.args) == 1
|
|
313
|
+
func.add_args(["b", "c"])
|
|
314
|
+
assert len(func.args) == 3
|
|
315
|
+
assert func.args[2].name == "c"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_function_args_property_is_readonly():
|
|
319
|
+
"""Mutating the returned tuple should not affect the function's internal args."""
|
|
320
|
+
module = codegen.Module()
|
|
321
|
+
func_name = module.scope.reserve_name("myfunc")
|
|
322
|
+
func = codegen.Function(func_name, args=["a", "b"], parent_scope=module.scope)
|
|
323
|
+
args = func.args
|
|
324
|
+
assert len(args) == 2
|
|
325
|
+
# tuple is immutable, so this should be a fresh copy each time
|
|
326
|
+
assert func.args is not func.args or isinstance(func.args, tuple)
|
|
327
|
+
assert len(func.args) == 2
|
|
328
|
+
|
|
329
|
+
|
|
271
330
|
def test_function_args_name_reserved_check():
|
|
272
331
|
module = codegen.Module()
|
|
273
332
|
module.scope.reserve_function_arg_name("my_arg")
|
|
@@ -364,7 +423,7 @@ def test_create_assignment_bad():
|
|
|
364
423
|
name = module.scope.reserve_name("x")
|
|
365
424
|
module.create_assignment(name, codegen.String("a string"))
|
|
366
425
|
stmt = module.statements[0]
|
|
367
|
-
assert isinstance(stmt, codegen.
|
|
426
|
+
assert isinstance(stmt, codegen.Assignment)
|
|
368
427
|
stmt.name = "something with a space"
|
|
369
428
|
with pytest.raises(AssertionError):
|
|
370
429
|
as_source_code(module)
|
|
@@ -437,19 +496,19 @@ def test_create_annotation_duplicate_name_gets_renamed():
|
|
|
437
496
|
def test_create_annotation_bad_name():
|
|
438
497
|
module = codegen.Module()
|
|
439
498
|
# Force a bad name through by directly constructing _Annotation
|
|
440
|
-
from fluent_codegen.codegen import
|
|
499
|
+
from fluent_codegen.codegen import Annotation
|
|
441
500
|
|
|
442
|
-
stmt =
|
|
501
|
+
stmt = Annotation("bad name", module.scope.name("int"))
|
|
443
502
|
with pytest.raises(AssertionError):
|
|
444
503
|
as_source_code(stmt)
|
|
445
504
|
|
|
446
505
|
|
|
447
506
|
def test_annotation_has_assignment_for_name():
|
|
448
|
-
from fluent_codegen.codegen import
|
|
507
|
+
from fluent_codegen.codegen import Annotation
|
|
449
508
|
|
|
450
509
|
scope = codegen.Scope()
|
|
451
510
|
scope.reserve_name("int")
|
|
452
|
-
stmt =
|
|
511
|
+
stmt = Annotation("x", scope.name("int"))
|
|
453
512
|
assert stmt.has_assignment_for_name("x") is True
|
|
454
513
|
assert stmt.has_assignment_for_name("y") is False
|
|
455
514
|
|
|
@@ -1349,6 +1408,440 @@ def test_rewriting_traverse_tuple():
|
|
|
1349
1408
|
codegen.rewriting_traverse(t, lambda n: n)
|
|
1350
1409
|
|
|
1351
1410
|
|
|
1411
|
+
def _collect_traversed_types(root):
|
|
1412
|
+
"""Helper: traverse a tree and return list of (class_name) for every node visited."""
|
|
1413
|
+
visited = []
|
|
1414
|
+
|
|
1415
|
+
def visitor(node):
|
|
1416
|
+
visited.append(type(node).__name__)
|
|
1417
|
+
return node
|
|
1418
|
+
|
|
1419
|
+
codegen.rewriting_traverse(root, visitor)
|
|
1420
|
+
return visited
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
def test_rewriting_traverse_complex_module():
|
|
1424
|
+
"""Test traversal visits all nodes in a complex module with nested structures."""
|
|
1425
|
+
module = codegen.Module()
|
|
1426
|
+
scope = module.scope
|
|
1427
|
+
|
|
1428
|
+
# Function with decorators, return type, body containing if/else, assignments, returns
|
|
1429
|
+
scope.reserve_name("my_decorator")
|
|
1430
|
+
decorator = codegen.Name("my_decorator", scope)
|
|
1431
|
+
func, _ = module.create_function(
|
|
1432
|
+
"process",
|
|
1433
|
+
args=["items", "flag"],
|
|
1434
|
+
decorators=[decorator],
|
|
1435
|
+
return_type=codegen.Name("str", scope),
|
|
1436
|
+
)
|
|
1437
|
+
n_items = codegen.Name("items", func)
|
|
1438
|
+
n_flag = codegen.Name("flag", func)
|
|
1439
|
+
|
|
1440
|
+
# Assignment with type hint
|
|
1441
|
+
result_name = func.body.scope.reserve_name("result")
|
|
1442
|
+
func.body.create_assignment(result_name, codegen.String(""), type_hint=codegen.Name("str", func))
|
|
1443
|
+
|
|
1444
|
+
# If/elif/else
|
|
1445
|
+
if_stmt = func.body.create_if()
|
|
1446
|
+
branch1 = if_stmt.create_if_branch(n_flag.eq(codegen.Bool(True)))
|
|
1447
|
+
branch1.create_assignment(
|
|
1448
|
+
result_name,
|
|
1449
|
+
codegen.function_call("str", [n_items], {}, func),
|
|
1450
|
+
allow_multiple=True,
|
|
1451
|
+
)
|
|
1452
|
+
branch2 = if_stmt.create_if_branch(n_flag.eq(codegen.Bool(False)))
|
|
1453
|
+
branch2.create_return(codegen.String("none"))
|
|
1454
|
+
if_stmt.else_block.create_return(codegen.String("default"))
|
|
1455
|
+
|
|
1456
|
+
# Return
|
|
1457
|
+
func.body.create_return(codegen.Name("result", func))
|
|
1458
|
+
|
|
1459
|
+
visited = _collect_traversed_types(module)
|
|
1460
|
+
|
|
1461
|
+
# Verify key node types are all visited
|
|
1462
|
+
assert "Module" in visited
|
|
1463
|
+
assert "Function" in visited
|
|
1464
|
+
assert "Block" in visited
|
|
1465
|
+
assert "If" in visited
|
|
1466
|
+
assert "Equals" in visited
|
|
1467
|
+
assert "String" in visited
|
|
1468
|
+
assert "Bool" in visited
|
|
1469
|
+
assert "Return" in visited
|
|
1470
|
+
assert "Call" in visited
|
|
1471
|
+
assert "Name" in visited
|
|
1472
|
+
|
|
1473
|
+
|
|
1474
|
+
def test_rewriting_traverse_call_with_kwargs():
|
|
1475
|
+
"""Test that kwargs dict values in Call nodes are traversed."""
|
|
1476
|
+
scope = codegen.Scope()
|
|
1477
|
+
func_name = scope.create_name("my_func")
|
|
1478
|
+
call = codegen.Call(
|
|
1479
|
+
func_name,
|
|
1480
|
+
[codegen.String("positional")],
|
|
1481
|
+
{"key1": codegen.String("val1"), "key2": codegen.Number(42)},
|
|
1482
|
+
)
|
|
1483
|
+
|
|
1484
|
+
visited = _collect_traversed_types(call)
|
|
1485
|
+
assert "Call" in visited
|
|
1486
|
+
assert "Name" in visited
|
|
1487
|
+
# The positional arg
|
|
1488
|
+
assert visited.count("String") >= 2 # "positional" and "val1"
|
|
1489
|
+
assert "Number" in visited # 42 from kwargs
|
|
1490
|
+
|
|
1491
|
+
|
|
1492
|
+
def test_rewriting_traverse_replaces_in_kwargs():
|
|
1493
|
+
"""Test that rewriting_traverse can replace nodes inside Call kwargs."""
|
|
1494
|
+
scope = codegen.Scope()
|
|
1495
|
+
func_name = scope.create_name("f")
|
|
1496
|
+
call = codegen.Call(
|
|
1497
|
+
func_name,
|
|
1498
|
+
[],
|
|
1499
|
+
{"k": codegen.String("old")},
|
|
1500
|
+
)
|
|
1501
|
+
|
|
1502
|
+
def replace_old(node):
|
|
1503
|
+
if isinstance(node, codegen.String) and node.string_value == "old":
|
|
1504
|
+
return codegen.String("new")
|
|
1505
|
+
return node
|
|
1506
|
+
|
|
1507
|
+
codegen.rewriting_traverse(call, replace_old)
|
|
1508
|
+
assert_code_equal(call, "f(k='new')")
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
def test_rewriting_traverse_function_decorators_and_return_type():
|
|
1512
|
+
"""Test that decorators and return_type on Function are traversed."""
|
|
1513
|
+
scope = codegen.Scope()
|
|
1514
|
+
decorator = codegen.String("deco_marker")
|
|
1515
|
+
ret_type = codegen.String("ret_marker")
|
|
1516
|
+
func = codegen.Function("myfunc", parent_scope=scope, decorators=[decorator], return_type=ret_type)
|
|
1517
|
+
|
|
1518
|
+
visited = _collect_traversed_types(func)
|
|
1519
|
+
assert visited.count("String") == 2 # decorator + return_type
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
def test_rewriting_traverse_replaces_in_decorator():
|
|
1523
|
+
"""Test replacing a decorator expression via traverse."""
|
|
1524
|
+
scope = codegen.Scope()
|
|
1525
|
+
scope.reserve_name("old_deco")
|
|
1526
|
+
scope.reserve_name("new_deco")
|
|
1527
|
+
func = codegen.Function(
|
|
1528
|
+
"myfunc",
|
|
1529
|
+
parent_scope=scope,
|
|
1530
|
+
decorators=[codegen.Name("old_deco", scope)],
|
|
1531
|
+
)
|
|
1532
|
+
|
|
1533
|
+
def replace_deco(node):
|
|
1534
|
+
if isinstance(node, codegen.Name) and node.name == "old_deco":
|
|
1535
|
+
return codegen.Name("new_deco", scope)
|
|
1536
|
+
return node
|
|
1537
|
+
|
|
1538
|
+
codegen.rewriting_traverse(func, replace_deco)
|
|
1539
|
+
decorator = func.decorators[0]
|
|
1540
|
+
assert isinstance(decorator, codegen.Name)
|
|
1541
|
+
assert decorator.name == "new_deco"
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
def test_rewriting_traverse_assignment_type_hint():
|
|
1545
|
+
"""Test that type_hint on _Assignment is traversed."""
|
|
1546
|
+
module = codegen.Module()
|
|
1547
|
+
name = module.scope.reserve_name("x")
|
|
1548
|
+
module.create_assignment(name, codegen.String("hello"), type_hint=codegen.Name("str", module.scope))
|
|
1549
|
+
|
|
1550
|
+
visited = _collect_traversed_types(module)
|
|
1551
|
+
# The type hint Name("str") should be visited
|
|
1552
|
+
assert visited.count("Name") >= 1
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
def test_rewriting_traverse_annotation():
|
|
1556
|
+
"""Test that annotation Expression on _Annotation is traversed."""
|
|
1557
|
+
module = codegen.Module()
|
|
1558
|
+
name = module.scope.reserve_name("x")
|
|
1559
|
+
module.create_annotation(name, codegen.Name("int", module.scope))
|
|
1560
|
+
|
|
1561
|
+
visited = _collect_traversed_types(module)
|
|
1562
|
+
# Module -> Block(statements) -> _Annotation -> annotation(Name)
|
|
1563
|
+
assert "Name" in visited
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
def test_rewriting_traverse_with_statement():
|
|
1567
|
+
"""Test traversal into With statement's context_expr, target, and body."""
|
|
1568
|
+
module = codegen.Module()
|
|
1569
|
+
module.scope.reserve_name("ctx_manager")
|
|
1570
|
+
module.scope.reserve_name("f")
|
|
1571
|
+
ctx = codegen.function_call("ctx_manager", [codegen.String("file.txt")], {}, module.scope)
|
|
1572
|
+
with_stmt = module.create_with(ctx, target=codegen.Name("f", module.scope))
|
|
1573
|
+
with_stmt.body.create_return(codegen.String("done"))
|
|
1574
|
+
|
|
1575
|
+
visited = _collect_traversed_types(module)
|
|
1576
|
+
assert "With" in visited
|
|
1577
|
+
assert "Call" in visited
|
|
1578
|
+
assert "Return" in visited
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
def test_rewriting_traverse_try_except():
|
|
1582
|
+
"""Test traversal into Try blocks."""
|
|
1583
|
+
module = codegen.Module()
|
|
1584
|
+
exc = codegen.Name("ValueError", module.scope)
|
|
1585
|
+
try_stmt = codegen.Try([exc], module.scope)
|
|
1586
|
+
try_stmt.try_block.create_return(codegen.String("tried"))
|
|
1587
|
+
try_stmt.except_block.create_return(codegen.String("caught"))
|
|
1588
|
+
try_stmt.else_block.create_return(codegen.String("else"))
|
|
1589
|
+
module.add_statement(try_stmt)
|
|
1590
|
+
|
|
1591
|
+
visited = _collect_traversed_types(module)
|
|
1592
|
+
assert "Try" in visited
|
|
1593
|
+
assert visited.count("Block") >= 3 # try, except, else blocks
|
|
1594
|
+
assert visited.count("Return") >= 3
|
|
1595
|
+
assert visited.count("String") >= 3
|
|
1596
|
+
|
|
1597
|
+
|
|
1598
|
+
def test_rewriting_traverse_dict_expression():
|
|
1599
|
+
"""Test traversal into Dict pairs (tuples of expressions)."""
|
|
1600
|
+
d = codegen.Dict(
|
|
1601
|
+
[
|
|
1602
|
+
(codegen.String("k1"), codegen.Number(1)),
|
|
1603
|
+
(codegen.String("k2"), codegen.Number(2)),
|
|
1604
|
+
]
|
|
1605
|
+
)
|
|
1606
|
+
|
|
1607
|
+
visited = _collect_traversed_types(d)
|
|
1608
|
+
assert visited.count("String") == 2
|
|
1609
|
+
assert visited.count("Number") == 2
|
|
1610
|
+
|
|
1611
|
+
|
|
1612
|
+
def test_rewriting_traverse_nested_binary_ops():
|
|
1613
|
+
"""Test deep nesting of binary operators."""
|
|
1614
|
+
# (1 + 2) * (3 - 4)
|
|
1615
|
+
expr = codegen.Number(1).add(codegen.Number(2)).mul(codegen.Number(3).sub(codegen.Number(4)))
|
|
1616
|
+
|
|
1617
|
+
visited = _collect_traversed_types(expr)
|
|
1618
|
+
assert visited.count("Number") == 4
|
|
1619
|
+
assert "Mul" in visited
|
|
1620
|
+
assert "Add" in visited
|
|
1621
|
+
assert "Sub" in visited
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
def test_rewriting_traverse_subscript():
|
|
1625
|
+
"""Test traversal into Subscript value and slice."""
|
|
1626
|
+
scope = codegen.Scope()
|
|
1627
|
+
scope.reserve_name("items")
|
|
1628
|
+
expr = codegen.Name("items", scope).subscript(codegen.Number(0))
|
|
1629
|
+
|
|
1630
|
+
visited = _collect_traversed_types(expr)
|
|
1631
|
+
assert "Subscript" in visited
|
|
1632
|
+
assert "Name" in visited
|
|
1633
|
+
assert "Number" in visited
|
|
1634
|
+
|
|
1635
|
+
|
|
1636
|
+
def test_rewriting_traverse_attr():
|
|
1637
|
+
"""Test traversal into Attr value."""
|
|
1638
|
+
scope = codegen.Scope()
|
|
1639
|
+
scope.reserve_name("obj")
|
|
1640
|
+
expr = codegen.Name("obj", scope).attr("prop")
|
|
1641
|
+
|
|
1642
|
+
visited = _collect_traversed_types(expr)
|
|
1643
|
+
assert "Attr" in visited
|
|
1644
|
+
assert "Name" in visited
|
|
1645
|
+
|
|
1646
|
+
|
|
1647
|
+
def test_rewriting_traverse_starred():
|
|
1648
|
+
"""Test traversal into Starred value."""
|
|
1649
|
+
scope = codegen.Scope()
|
|
1650
|
+
scope.reserve_name("args")
|
|
1651
|
+
expr = codegen.Name("args", scope).starred()
|
|
1652
|
+
|
|
1653
|
+
visited = _collect_traversed_types(expr)
|
|
1654
|
+
assert "Starred" in visited
|
|
1655
|
+
assert "Name" in visited
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
def test_rewriting_traverse_list_tuple_set():
|
|
1659
|
+
"""Test traversal into List, Tuple, Set items."""
|
|
1660
|
+
for cls in [codegen.List, codegen.Tuple, codegen.Set]:
|
|
1661
|
+
coll = cls([codegen.String("a"), codegen.Number(1)])
|
|
1662
|
+
visited = _collect_traversed_types(coll)
|
|
1663
|
+
assert "String" in visited
|
|
1664
|
+
assert "Number" in visited
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
def test_rewriting_traverse_class_with_bases_and_decorators():
|
|
1668
|
+
"""Test traversal into Class body, bases, and decorators."""
|
|
1669
|
+
module = codegen.Module()
|
|
1670
|
+
module.scope.reserve_name("BaseClass")
|
|
1671
|
+
module.scope.reserve_name("my_class_deco")
|
|
1672
|
+
base = codegen.Name("BaseClass", module.scope)
|
|
1673
|
+
deco = codegen.Name("my_class_deco", module.scope)
|
|
1674
|
+
cls, _ = module.create_class("MyClass", bases=[base], decorators=[deco])
|
|
1675
|
+
|
|
1676
|
+
visited = _collect_traversed_types(module)
|
|
1677
|
+
assert "Class" in visited
|
|
1678
|
+
assert "Block" in visited
|
|
1679
|
+
# bases and decorators should be visited
|
|
1680
|
+
assert visited.count("Name") >= 2
|
|
1681
|
+
|
|
1682
|
+
|
|
1683
|
+
def test_rewriting_traverse_assert():
|
|
1684
|
+
"""Test traversal into Assert test and msg."""
|
|
1685
|
+
module = codegen.Module()
|
|
1686
|
+
module.create_assert(codegen.Bool(True), codegen.String("oops"))
|
|
1687
|
+
|
|
1688
|
+
visited = _collect_traversed_types(module)
|
|
1689
|
+
assert "Assert" in visited
|
|
1690
|
+
assert "Bool" in visited
|
|
1691
|
+
assert "String" in visited
|
|
1692
|
+
|
|
1693
|
+
|
|
1694
|
+
def test_rewriting_traverse_fstring():
|
|
1695
|
+
"""Test traversal into FStringJoin parts."""
|
|
1696
|
+
scope = codegen.Scope()
|
|
1697
|
+
scope.reserve_name("world")
|
|
1698
|
+
fstr = codegen.FStringJoin([codegen.String("hello "), codegen.Name("world", scope)])
|
|
1699
|
+
|
|
1700
|
+
visited = _collect_traversed_types(fstr)
|
|
1701
|
+
assert "FStringJoin" in visited
|
|
1702
|
+
assert "String" in visited
|
|
1703
|
+
assert "Name" in visited
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
def test_rewriting_traverse_deeply_nested_codegen_tree():
|
|
1707
|
+
"""
|
|
1708
|
+
Build a complex, deeply-nested codegen tree and verify that rewriting_traverse
|
|
1709
|
+
visits every single CodeGenAst/CodeGenAstList node by replacing all String
|
|
1710
|
+
values with 'REPLACED'.
|
|
1711
|
+
"""
|
|
1712
|
+
module = codegen.Module()
|
|
1713
|
+
scope = module.scope
|
|
1714
|
+
|
|
1715
|
+
# Import
|
|
1716
|
+
module.create_import("os")
|
|
1717
|
+
module.create_import_from(from_="sys", import_="argv")
|
|
1718
|
+
|
|
1719
|
+
# Class with decorator and base
|
|
1720
|
+
scope.reserve_name("dataclass")
|
|
1721
|
+
class_deco = codegen.Name("dataclass", scope)
|
|
1722
|
+
base = codegen.Name("object", scope)
|
|
1723
|
+
cls, _ = module.create_class("Config", bases=[base], decorators=[class_deco])
|
|
1724
|
+
field_name = cls.body.scope.reserve_name("name")
|
|
1725
|
+
cls.body.create_assignment(field_name, codegen.String("default_name"))
|
|
1726
|
+
|
|
1727
|
+
# Function with kwargs call, nested if, try/except, with, assert
|
|
1728
|
+
scope.reserve_name("process")
|
|
1729
|
+
func, _ = module.create_function(
|
|
1730
|
+
"run",
|
|
1731
|
+
args=["config"],
|
|
1732
|
+
return_type=codegen.Name("bool", scope),
|
|
1733
|
+
)
|
|
1734
|
+
config = codegen.Name("config", func)
|
|
1735
|
+
|
|
1736
|
+
# Assignment with function call using kwargs
|
|
1737
|
+
result_name = func.body.scope.reserve_name("result")
|
|
1738
|
+
func.body.create_assignment(
|
|
1739
|
+
result_name,
|
|
1740
|
+
codegen.function_call(
|
|
1741
|
+
"process",
|
|
1742
|
+
[config.attr("name")],
|
|
1743
|
+
{"timeout": codegen.Number(30)},
|
|
1744
|
+
func,
|
|
1745
|
+
),
|
|
1746
|
+
)
|
|
1747
|
+
|
|
1748
|
+
# Nested if
|
|
1749
|
+
if_stmt = func.body.create_if()
|
|
1750
|
+
branch = if_stmt.create_if_branch(codegen.Name("result", func).eq(codegen.String("ok")))
|
|
1751
|
+
branch.create_return(codegen.Bool(True))
|
|
1752
|
+
|
|
1753
|
+
# Try/except in else
|
|
1754
|
+
try_stmt = codegen.Try([codegen.Name("Exception", func)], func)
|
|
1755
|
+
try_stmt.try_block.create_return(codegen.Bool(False))
|
|
1756
|
+
try_stmt.except_block.create_return(codegen.Bool(False))
|
|
1757
|
+
if_stmt.else_block.add_statement(try_stmt)
|
|
1758
|
+
|
|
1759
|
+
# With statement
|
|
1760
|
+
func.reserve_name("open_log")
|
|
1761
|
+
ctx_expr = codegen.function_call("open_log", [codegen.String("log.txt")], {}, func)
|
|
1762
|
+
with_block = func.body.create_with(ctx_expr)
|
|
1763
|
+
with_block.body.create_return(codegen.String("logged"))
|
|
1764
|
+
|
|
1765
|
+
# Assert
|
|
1766
|
+
func.body.create_assert(
|
|
1767
|
+
codegen.Name("result", func).ne(codegen.NoneExpr()),
|
|
1768
|
+
codegen.String("must not be None"),
|
|
1769
|
+
)
|
|
1770
|
+
|
|
1771
|
+
# Now do a rewriting_traverse that replaces all Strings
|
|
1772
|
+
string_count = [0]
|
|
1773
|
+
|
|
1774
|
+
def count_and_replace_strings(node):
|
|
1775
|
+
if isinstance(node, codegen.String):
|
|
1776
|
+
string_count[0] += 1
|
|
1777
|
+
return codegen.String("REPLACED")
|
|
1778
|
+
return node
|
|
1779
|
+
|
|
1780
|
+
codegen.rewriting_traverse(module, count_and_replace_strings)
|
|
1781
|
+
|
|
1782
|
+
# We should have found all string literals in the tree
|
|
1783
|
+
assert string_count[0] >= 5 # default_name, ok, log.txt, logged, must not be None
|
|
1784
|
+
|
|
1785
|
+
# Verify ALL strings are now REPLACED in the generated source
|
|
1786
|
+
source = module.as_python_source()
|
|
1787
|
+
for original in ["default_name", "'ok'", "log.txt", "logged", "must not be None"]:
|
|
1788
|
+
assert original not in source, f"String '{original}' should have been replaced but found in: {source}"
|
|
1789
|
+
assert "REPLACED" in source
|
|
1790
|
+
|
|
1791
|
+
|
|
1792
|
+
def test_rewriting_traverse_replace_number_in_nested_kwargs():
|
|
1793
|
+
"""
|
|
1794
|
+
Specifically test that numbers deep inside kwargs of a Call inside a
|
|
1795
|
+
function body can be found and replaced.
|
|
1796
|
+
"""
|
|
1797
|
+
module = codegen.Module()
|
|
1798
|
+
module.scope.reserve_name("setup")
|
|
1799
|
+
func, _ = module.create_function("f", args=[])
|
|
1800
|
+
func.body.create_assignment(
|
|
1801
|
+
func.body.scope.reserve_name("x"),
|
|
1802
|
+
codegen.function_call(
|
|
1803
|
+
"setup",
|
|
1804
|
+
[],
|
|
1805
|
+
{"timeout": codegen.Number(99), "retries": codegen.Number(3)},
|
|
1806
|
+
func,
|
|
1807
|
+
),
|
|
1808
|
+
)
|
|
1809
|
+
|
|
1810
|
+
def double_numbers(node):
|
|
1811
|
+
if isinstance(node, codegen.Number):
|
|
1812
|
+
return codegen.Number(node.number * 2)
|
|
1813
|
+
return node
|
|
1814
|
+
|
|
1815
|
+
codegen.rewriting_traverse(module, double_numbers)
|
|
1816
|
+
source = module.as_python_source()
|
|
1817
|
+
assert "198" in source # 99 * 2
|
|
1818
|
+
assert "6" in source # 3 * 2
|
|
1819
|
+
assert "99" not in source
|
|
1820
|
+
|
|
1821
|
+
|
|
1822
|
+
def test_rewriting_traverse_none_expr():
|
|
1823
|
+
"""Test that NoneExpr nodes are visited (they previously lacked child_elements)."""
|
|
1824
|
+
scope = codegen.Scope()
|
|
1825
|
+
scope.reserve_name("x")
|
|
1826
|
+
expr = codegen.Name("x", scope).eq(codegen.NoneExpr())
|
|
1827
|
+
|
|
1828
|
+
visited = _collect_traversed_types(expr)
|
|
1829
|
+
assert "NoneExpr" in visited
|
|
1830
|
+
assert "Equals" in visited
|
|
1831
|
+
assert "Name" in visited
|
|
1832
|
+
|
|
1833
|
+
|
|
1834
|
+
def test_rewriting_traverse_import_nodes():
|
|
1835
|
+
"""Test that Import and ImportFrom nodes are visited (they previously lacked child_elements)."""
|
|
1836
|
+
module = codegen.Module()
|
|
1837
|
+
module.create_import("os")
|
|
1838
|
+
module.create_import_from(from_="sys", import_="argv")
|
|
1839
|
+
|
|
1840
|
+
visited = _collect_traversed_types(module)
|
|
1841
|
+
assert "Import" in visited
|
|
1842
|
+
assert "ImportFrom" in visited
|
|
1843
|
+
|
|
1844
|
+
|
|
1352
1845
|
# --- auto() tests ---
|
|
1353
1846
|
|
|
1354
1847
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from fluent_codegen.codegen import (
|
|
4
|
+
Function,
|
|
5
|
+
Name,
|
|
6
|
+
Number,
|
|
7
|
+
Scope,
|
|
8
|
+
String,
|
|
9
|
+
constants,
|
|
10
|
+
)
|
|
11
|
+
from fluent_codegen.remove_unused_assignments import remove_unused_assignments
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def make_func(parent_scope=None):
|
|
15
|
+
"""Helper: create a Function named 'f' with no args."""
|
|
16
|
+
if parent_scope is None:
|
|
17
|
+
parent_scope = Scope()
|
|
18
|
+
parent_scope.reserve_name("f")
|
|
19
|
+
func = Function("f", args=[], parent_scope=parent_scope)
|
|
20
|
+
return func
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def source(func):
|
|
24
|
+
return func.as_python_source().strip()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ── positive cases (something gets removed) ──────────────────────────
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_remove_simple_unused():
|
|
31
|
+
func = make_func()
|
|
32
|
+
x = func.body.scope.create_name("x")
|
|
33
|
+
func.body.create_assignment(x, Number(1))
|
|
34
|
+
func.body.create_return(String("done"))
|
|
35
|
+
|
|
36
|
+
remove_unused_assignments(func)
|
|
37
|
+
|
|
38
|
+
assert source(func) == "def f():\n return 'done'"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_cascading_removal():
|
|
42
|
+
"""x = y, y = 1; x unused ⇒ remove x = y ⇒ y now unused ⇒ remove y = 1."""
|
|
43
|
+
func = make_func()
|
|
44
|
+
y = func.body.scope.create_name("y")
|
|
45
|
+
func.body.create_assignment(y, Number(1))
|
|
46
|
+
x = func.body.scope.create_name("x")
|
|
47
|
+
func.body.create_assignment(x, Name("y", func.body.scope))
|
|
48
|
+
func.body.create_return(String("done"))
|
|
49
|
+
|
|
50
|
+
remove_unused_assignments(func)
|
|
51
|
+
|
|
52
|
+
assert source(func) == "def f():\n return 'done'"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_remove_unused_in_if_block():
|
|
56
|
+
"""Unused assignment inside an if-branch is removed."""
|
|
57
|
+
func = make_func()
|
|
58
|
+
x = func.body.scope.create_name("x")
|
|
59
|
+
if_stmt = func.body.create_if()
|
|
60
|
+
branch = if_stmt.create_if_branch(constants.True_)
|
|
61
|
+
branch.create_assignment(x, Number(42))
|
|
62
|
+
func.body.create_return(String("done"))
|
|
63
|
+
|
|
64
|
+
remove_unused_assignments(func)
|
|
65
|
+
|
|
66
|
+
# The assignment in the if-branch should be gone; branch body is now empty
|
|
67
|
+
src = source(func)
|
|
68
|
+
assert "x = 42" not in src
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_remove_unused_in_try_block():
|
|
72
|
+
"""Unused assignment inside a try block is removed."""
|
|
73
|
+
from fluent_codegen.codegen import Try
|
|
74
|
+
|
|
75
|
+
func = make_func()
|
|
76
|
+
x = func.body.scope.create_name("x")
|
|
77
|
+
scope = func.body.scope
|
|
78
|
+
scope.reserve_name("Exception", is_builtin=True)
|
|
79
|
+
try_stmt = Try(
|
|
80
|
+
catch_exceptions=[Name("Exception", scope)],
|
|
81
|
+
parent_scope=scope,
|
|
82
|
+
)
|
|
83
|
+
try_stmt.try_block.create_assignment(x, Number(1))
|
|
84
|
+
func.body.add_statement(try_stmt)
|
|
85
|
+
func.body.create_return(String("ok"))
|
|
86
|
+
|
|
87
|
+
remove_unused_assignments(func)
|
|
88
|
+
|
|
89
|
+
src = source(func)
|
|
90
|
+
assert "x = 1" not in src
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ── negative cases (nothing should be removed) ───────────────────────
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_keep_used_assignment():
|
|
97
|
+
func = make_func()
|
|
98
|
+
x = func.body.scope.create_name("x")
|
|
99
|
+
func.body.create_assignment(x, Number(1))
|
|
100
|
+
func.body.create_return(Name("x", func.body.scope))
|
|
101
|
+
|
|
102
|
+
remove_unused_assignments(func)
|
|
103
|
+
|
|
104
|
+
assert source(func) == "def f():\n x = 1\n return x"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_keep_used_in_nested_block():
|
|
108
|
+
"""x assigned at top level, used inside an if-branch — keep it."""
|
|
109
|
+
func = make_func()
|
|
110
|
+
x = func.body.scope.create_name("x")
|
|
111
|
+
func.body.create_assignment(x, Number(1))
|
|
112
|
+
if_stmt = func.body.create_if()
|
|
113
|
+
branch = if_stmt.create_if_branch(constants.True_)
|
|
114
|
+
branch.create_return(Name("x", func.body.scope))
|
|
115
|
+
|
|
116
|
+
remove_unused_assignments(func)
|
|
117
|
+
|
|
118
|
+
src = source(func)
|
|
119
|
+
assert "x = 1" in src
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_keep_function_args():
|
|
123
|
+
"""Function arguments should never be removed even if unused in body."""
|
|
124
|
+
scope = Scope()
|
|
125
|
+
scope.reserve_name("g")
|
|
126
|
+
func = Function("g", args=["a"], parent_scope=scope)
|
|
127
|
+
func.body.create_return(String("hi"))
|
|
128
|
+
|
|
129
|
+
remove_unused_assignments(func)
|
|
130
|
+
|
|
131
|
+
assert source(func) == "def g(a):\n return 'hi'"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_keep_when_used_in_another_assignment_value():
|
|
135
|
+
"""y = 1; x = y; return x ⇒ y is used (in x's value), so keep both."""
|
|
136
|
+
func = make_func()
|
|
137
|
+
y = func.body.scope.create_name("y")
|
|
138
|
+
func.body.create_assignment(y, Number(1))
|
|
139
|
+
x = func.body.scope.create_name("x")
|
|
140
|
+
func.body.create_assignment(x, Name("y", func.body.scope))
|
|
141
|
+
func.body.create_return(Name("x", func.body.scope))
|
|
142
|
+
|
|
143
|
+
remove_unused_assignments(func)
|
|
144
|
+
|
|
145
|
+
src = source(func)
|
|
146
|
+
assert "y = 1" in src
|
|
147
|
+
assert "x = y" in src
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ── error case: nested scope ─────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_raises_on_nested_function():
|
|
154
|
+
func = make_func()
|
|
155
|
+
func.body.create_function("inner", args=[])
|
|
156
|
+
|
|
157
|
+
with pytest.raises(AssertionError, match="nested.*[Ss]cope"):
|
|
158
|
+
remove_unused_assignments(func)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_raises_on_nested_class():
|
|
162
|
+
func = make_func()
|
|
163
|
+
func.body.create_class("MyClass")
|
|
164
|
+
|
|
165
|
+
with pytest.raises(AssertionError, match="nested.*[Ss]cope"):
|
|
166
|
+
remove_unused_assignments(func)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fluent_codegen-0.2 → fluent_codegen-0.3.0}/src/fluent_codegen.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|