openrewrite-migrate-python 0.15.2__py3-none-any.whl → 0.16.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openrewrite_code_quality_python/__init__.py +10 -10
- openrewrite_code_quality_python/cleanup/__init__.py +207 -0
- openrewrite_code_quality_python/cleanup/_literals.py +23 -0
- openrewrite_code_quality_python/cleanup/assign_return.py +153 -0
- openrewrite_code_quality_python/cleanup/attr_access.py +234 -0
- openrewrite_code_quality_python/cleanup/boolean_simplification.py +910 -0
- openrewrite_code_quality_python/cleanup/collection_literals.py +389 -0
- openrewrite_code_quality_python/cleanup/comparison_simplification.py +311 -0
- openrewrite_code_quality_python/cleanup/complex_transforms.py +278 -0
- openrewrite_code_quality_python/cleanup/comprehension_cleanup.py +953 -0
- openrewrite_code_quality_python/cleanup/conditionals.py +859 -0
- openrewrite_code_quality_python/cleanup/datetime_cleanup.py +88 -0
- openrewrite_code_quality_python/cleanup/dict_cleanup.py +589 -0
- openrewrite_code_quality_python/cleanup/exception_cleanup.py +279 -0
- openrewrite_code_quality_python/cleanup/expression_simplification.py +450 -0
- openrewrite_code_quality_python/cleanup/logger_warn.py +89 -0
- openrewrite_code_quality_python/cleanup/loop_cleanup.py +174 -0
- openrewrite_code_quality_python/cleanup/merge_nested_with.py +181 -0
- openrewrite_code_quality_python/cleanup/merge_startswith_endswith.py +199 -0
- openrewrite_code_quality_python/cleanup/naming_conventions.py +291 -0
- openrewrite_code_quality_python/cleanup/negated_operators.py +161 -0
- openrewrite_code_quality_python/cleanup/none_and_identity.py +323 -0
- openrewrite_code_quality_python/cleanup/object_inheritance.py +89 -0
- openrewrite_code_quality_python/cleanup/pandas_cleanup.py +213 -0
- openrewrite_code_quality_python/cleanup/python_best_practices.py +269 -0
- openrewrite_code_quality_python/cleanup/range_cleanup.py +142 -0
- openrewrite_code_quality_python/cleanup/redundant_code.py +180 -0
- openrewrite_code_quality_python/cleanup/remove_trailing_return_none.py +167 -0
- openrewrite_code_quality_python/cleanup/string_cleanup.py +958 -0
- openrewrite_code_quality_python/cleanup/unreachable_code.py +440 -0
- openrewrite_code_quality_python/cleanup/use_items.py +143 -0
- openrewrite_code_quality_python/cleanup/use_list_clear.py +129 -0
- openrewrite_code_quality_python/cleanup/yoda_conditions.py +140 -0
- openrewrite_code_quality_python/codequality/_trivia.py +24 -1
- openrewrite_code_quality_python/codequality/_truth_value.py +83 -0
- openrewrite_code_quality_python/codequality/boolean_checks_not_inverted.py +27 -48
- openrewrite_code_quality_python/codequality/simplify_boolean_literal.py +23 -15
- openrewrite_migrate_python/migrate/__init__.py +3 -0
- openrewrite_migrate_python/migrate/upgrade_to_pandas.py +51 -0
- openrewrite_migrate_python/migrate/upgrade_to_python39.py +10 -1
- {openrewrite_migrate_python-0.15.2.dist-info → openrewrite_migrate_python-0.16.0.dist-info}/METADATA +1 -1
- {openrewrite_migrate_python-0.15.2.dist-info → openrewrite_migrate_python-0.16.0.dist-info}/RECORD +45 -11
- {openrewrite_migrate_python-0.15.2.dist-info → openrewrite_migrate_python-0.16.0.dist-info}/WHEEL +0 -0
- {openrewrite_migrate_python-0.15.2.dist-info → openrewrite_migrate_python-0.16.0.dist-info}/entry_points.txt +0 -0
- {openrewrite_migrate_python-0.15.2.dist-info → openrewrite_migrate_python-0.16.0.dist-info}/top_level.txt +0 -0
|
@@ -21,15 +21,15 @@ def activate(marketplace: RecipeMarketplace) -> None:
|
|
|
21
21
|
"""
|
|
22
22
|
from rewrite.decorators import get_recipe_category
|
|
23
23
|
|
|
24
|
+
from . import cleanup as cleanup_pkg
|
|
24
25
|
from . import codequality as codequality_pkg
|
|
25
26
|
|
|
26
|
-
# Auto-discover all Recipe subclasses from the codequality
|
|
27
|
-
for
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
marketplace.install(obj, category)
|
|
27
|
+
# Auto-discover all Recipe subclasses from the codequality and cleanup packages
|
|
28
|
+
for pkg in (codequality_pkg, cleanup_pkg):
|
|
29
|
+
for module_info in pkgutil.walk_packages(pkg.__path__, prefix=pkg.__name__ + "."):
|
|
30
|
+
module = importlib.import_module(module_info.name)
|
|
31
|
+
for _name, obj in inspect.getmembers(module, inspect.isclass):
|
|
32
|
+
if issubclass(obj, Recipe) and obj is not Recipe and obj.__module__ == module.__name__:
|
|
33
|
+
category = get_recipe_category(obj)
|
|
34
|
+
if category is not None:
|
|
35
|
+
marketplace.install(obj, category)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Python cleanup recipes."""
|
|
2
|
+
|
|
3
|
+
from .assign_return import RemoveUnnecessaryAssign
|
|
4
|
+
from .attr_access import (
|
|
5
|
+
ReplaceGetAttrWithConstant,
|
|
6
|
+
ReplaceSetAttrWithConstant,
|
|
7
|
+
)
|
|
8
|
+
from .boolean_simplification import (
|
|
9
|
+
BooleanIfExpIdentity,
|
|
10
|
+
CollectionToBool,
|
|
11
|
+
DeMorgan,
|
|
12
|
+
RemoveRedundantBoolean,
|
|
13
|
+
SimplifyBooleanReturn,
|
|
14
|
+
SimplifyLenComparison,
|
|
15
|
+
)
|
|
16
|
+
from .collection_literals import (
|
|
17
|
+
CollectionIntoSet,
|
|
18
|
+
DictLiteral,
|
|
19
|
+
ListLiteral,
|
|
20
|
+
TupleLiteral,
|
|
21
|
+
UnwrapIterableConstruction,
|
|
22
|
+
)
|
|
23
|
+
from .comparison_simplification import (
|
|
24
|
+
ChainCompares,
|
|
25
|
+
MergeComparisons,
|
|
26
|
+
SimplifySubstringSearch,
|
|
27
|
+
)
|
|
28
|
+
from .complex_transforms import (
|
|
29
|
+
SwapVariable,
|
|
30
|
+
TernaryToIfExpression,
|
|
31
|
+
)
|
|
32
|
+
from .comprehension_cleanup import (
|
|
33
|
+
CollectionBuiltinToComprehension,
|
|
34
|
+
ComprehensionToGenerator,
|
|
35
|
+
ConvertAnyToIn,
|
|
36
|
+
IdentityComprehension,
|
|
37
|
+
InvertAnyAll,
|
|
38
|
+
InvertAnyAllBody,
|
|
39
|
+
SimplifyConstantSum,
|
|
40
|
+
SimplifyGenerator,
|
|
41
|
+
)
|
|
42
|
+
from .conditionals import (
|
|
43
|
+
AssignIfExp,
|
|
44
|
+
MergeElseIfIntoElif,
|
|
45
|
+
MergeIsinstance,
|
|
46
|
+
RemovePassElif,
|
|
47
|
+
RemoveRedundantIf,
|
|
48
|
+
RemoveUnnecessaryElse,
|
|
49
|
+
SwapIfElseBranches,
|
|
50
|
+
)
|
|
51
|
+
from .datetime_cleanup import UseDatetimeNowNotToday
|
|
52
|
+
from .dict_cleanup import (
|
|
53
|
+
RemoveDictKeys,
|
|
54
|
+
RemoveDuplicateDictKey,
|
|
55
|
+
RemoveDuplicateSetKey,
|
|
56
|
+
RemoveRedundantConstructorInDictUnion,
|
|
57
|
+
SimplifyDictionaryUpdate,
|
|
58
|
+
UseDictionaryUnion,
|
|
59
|
+
)
|
|
60
|
+
from .exception_cleanup import (
|
|
61
|
+
RaiseFromPreviousError,
|
|
62
|
+
SimplifySingleExceptionTuple,
|
|
63
|
+
)
|
|
64
|
+
from .expression_simplification import (
|
|
65
|
+
AugAssign,
|
|
66
|
+
OrIfExpIdentity,
|
|
67
|
+
RemoveRedundantSliceIndex,
|
|
68
|
+
SimplifyNegativeIndex,
|
|
69
|
+
SwapIfExpression,
|
|
70
|
+
)
|
|
71
|
+
from .logger_warn import ReplaceLoggerWarn
|
|
72
|
+
from .loop_cleanup import UselessElseOnLoop
|
|
73
|
+
from .merge_nested_with import MergeNestedWithStatements
|
|
74
|
+
from .merge_startswith_endswith import MergeStartswithEndswith
|
|
75
|
+
from .naming_conventions import (
|
|
76
|
+
ClassMethodFirstArgName,
|
|
77
|
+
InstanceMethodFirstArgName,
|
|
78
|
+
)
|
|
79
|
+
from .negated_operators import (
|
|
80
|
+
ReplaceNotInTest,
|
|
81
|
+
ReplaceNotIsTest,
|
|
82
|
+
)
|
|
83
|
+
from .none_and_identity import (
|
|
84
|
+
BinOpIdentity,
|
|
85
|
+
EqualityIdentity,
|
|
86
|
+
NoneCompare,
|
|
87
|
+
SquareIdentity,
|
|
88
|
+
)
|
|
89
|
+
from .object_inheritance import RemoveUselessObjectInheritance
|
|
90
|
+
from .pandas_cleanup import (
|
|
91
|
+
DataframeAppendToConcat,
|
|
92
|
+
ReplaceApplyWithMethodCall,
|
|
93
|
+
)
|
|
94
|
+
from .python_best_practices import PythonBestPractices
|
|
95
|
+
from .range_cleanup import (
|
|
96
|
+
RemoveUnitStepFromRange,
|
|
97
|
+
RemoveZeroFromRange,
|
|
98
|
+
)
|
|
99
|
+
from .redundant_code import (
|
|
100
|
+
RemoveRedundantContinue,
|
|
101
|
+
RemoveRedundantPathExists,
|
|
102
|
+
)
|
|
103
|
+
from .remove_trailing_return_none import RemoveTrailingReturnNone
|
|
104
|
+
from .string_cleanup import (
|
|
105
|
+
RemoveNoneFromDefaultGet,
|
|
106
|
+
RemoveRedundantFstring,
|
|
107
|
+
RemoveStrFromFstring,
|
|
108
|
+
RemoveStrFromPrint,
|
|
109
|
+
SimplifyFstringFormatting,
|
|
110
|
+
StrPrefixSuffix,
|
|
111
|
+
UseGetitemForReMatchGroups,
|
|
112
|
+
UseStringRemoveAffix,
|
|
113
|
+
)
|
|
114
|
+
from .unreachable_code import (
|
|
115
|
+
RemoveAssertTrue,
|
|
116
|
+
RemoveEmptyNestedBlock,
|
|
117
|
+
RemoveUnreachableCode,
|
|
118
|
+
)
|
|
119
|
+
from .use_items import UseItemsForKeyValueIteration
|
|
120
|
+
from .use_list_clear import UseListClear
|
|
121
|
+
from .yoda_conditions import ReplaceYodaConditions
|
|
122
|
+
|
|
123
|
+
__all__ = [
|
|
124
|
+
"AssignIfExp",
|
|
125
|
+
"AugAssign",
|
|
126
|
+
"BinOpIdentity",
|
|
127
|
+
"BooleanIfExpIdentity",
|
|
128
|
+
"ChainCompares",
|
|
129
|
+
"ClassMethodFirstArgName",
|
|
130
|
+
"CollectionBuiltinToComprehension",
|
|
131
|
+
"CollectionIntoSet",
|
|
132
|
+
"CollectionToBool",
|
|
133
|
+
"ComprehensionToGenerator",
|
|
134
|
+
"ConvertAnyToIn",
|
|
135
|
+
"DataframeAppendToConcat",
|
|
136
|
+
"DeMorgan",
|
|
137
|
+
"DictLiteral",
|
|
138
|
+
"EqualityIdentity",
|
|
139
|
+
"IdentityComprehension",
|
|
140
|
+
"InstanceMethodFirstArgName",
|
|
141
|
+
"InvertAnyAll",
|
|
142
|
+
"InvertAnyAllBody",
|
|
143
|
+
"ListLiteral",
|
|
144
|
+
"MergeComparisons",
|
|
145
|
+
"MergeElseIfIntoElif",
|
|
146
|
+
"MergeIsinstance",
|
|
147
|
+
"MergeNestedWithStatements",
|
|
148
|
+
"MergeStartswithEndswith",
|
|
149
|
+
"NoneCompare",
|
|
150
|
+
"OrIfExpIdentity",
|
|
151
|
+
"PythonBestPractices",
|
|
152
|
+
"RaiseFromPreviousError",
|
|
153
|
+
"RemoveAssertTrue",
|
|
154
|
+
"RemoveDictKeys",
|
|
155
|
+
"RemoveDuplicateDictKey",
|
|
156
|
+
"RemoveDuplicateSetKey",
|
|
157
|
+
"RemoveEmptyNestedBlock",
|
|
158
|
+
"RemoveNoneFromDefaultGet",
|
|
159
|
+
"RemovePassElif",
|
|
160
|
+
"RemoveRedundantBoolean",
|
|
161
|
+
"RemoveRedundantConstructorInDictUnion",
|
|
162
|
+
"RemoveRedundantContinue",
|
|
163
|
+
"RemoveRedundantFstring",
|
|
164
|
+
"RemoveRedundantIf",
|
|
165
|
+
"RemoveRedundantPathExists",
|
|
166
|
+
"RemoveRedundantSliceIndex",
|
|
167
|
+
"RemoveStrFromFstring",
|
|
168
|
+
"RemoveStrFromPrint",
|
|
169
|
+
"RemoveTrailingReturnNone",
|
|
170
|
+
"RemoveUnitStepFromRange",
|
|
171
|
+
"RemoveUnnecessaryAssign",
|
|
172
|
+
"RemoveUnnecessaryElse",
|
|
173
|
+
"RemoveUnreachableCode",
|
|
174
|
+
"RemoveUselessObjectInheritance",
|
|
175
|
+
"RemoveZeroFromRange",
|
|
176
|
+
"ReplaceApplyWithMethodCall",
|
|
177
|
+
"ReplaceGetAttrWithConstant",
|
|
178
|
+
"ReplaceLoggerWarn",
|
|
179
|
+
"ReplaceNotInTest",
|
|
180
|
+
"ReplaceNotIsTest",
|
|
181
|
+
"ReplaceSetAttrWithConstant",
|
|
182
|
+
"ReplaceYodaConditions",
|
|
183
|
+
"SimplifyBooleanReturn",
|
|
184
|
+
"SimplifyConstantSum",
|
|
185
|
+
"SimplifyDictionaryUpdate",
|
|
186
|
+
"SimplifyFstringFormatting",
|
|
187
|
+
"SimplifyGenerator",
|
|
188
|
+
"SimplifyLenComparison",
|
|
189
|
+
"SimplifyNegativeIndex",
|
|
190
|
+
"SimplifySingleExceptionTuple",
|
|
191
|
+
"SimplifySubstringSearch",
|
|
192
|
+
"SquareIdentity",
|
|
193
|
+
"StrPrefixSuffix",
|
|
194
|
+
"SwapIfElseBranches",
|
|
195
|
+
"SwapIfExpression",
|
|
196
|
+
"SwapVariable",
|
|
197
|
+
"TernaryToIfExpression",
|
|
198
|
+
"TupleLiteral",
|
|
199
|
+
"UnwrapIterableConstruction",
|
|
200
|
+
"UseDatetimeNowNotToday",
|
|
201
|
+
"UseDictionaryUnion",
|
|
202
|
+
"UseGetitemForReMatchGroups",
|
|
203
|
+
"UseItemsForKeyValueIteration",
|
|
204
|
+
"UseListClear",
|
|
205
|
+
"UseStringRemoveAffix",
|
|
206
|
+
"UselessElseOnLoop",
|
|
207
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Classify literals by their source text, which tells `str` from `bytes` where the type does not."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from rewrite.java.tree import Literal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _prefix(source: str) -> str:
|
|
9
|
+
return source[:len(source) - len(source.lstrip("bBrRuU"))]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_string_literal(expr: Any) -> bool:
|
|
13
|
+
"""Whether ``expr`` is a ``str`` or ``bytes`` literal, prefixed or not."""
|
|
14
|
+
if not isinstance(expr, Literal) or not expr.value_source:
|
|
15
|
+
return False
|
|
16
|
+
source = expr.value_source
|
|
17
|
+
prefix = _prefix(source)
|
|
18
|
+
return len(prefix) <= 2 and source[len(prefix):len(prefix) + 1] in ('"', "'")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_bytes_literal(expr: Literal) -> bool:
|
|
22
|
+
"""Whether ``expr`` is a ``b``-prefixed literal, which the parser types as ``str``."""
|
|
23
|
+
return "b" in _prefix(expr.value_source or "").lower()
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Recipe for RET504: Remove unnecessary assignment before return.
|
|
2
|
+
|
|
3
|
+
Detects the pattern where a variable is assigned and then immediately
|
|
4
|
+
returned, replacing it with a direct return of the expression.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
Before:
|
|
8
|
+
def foo():
|
|
9
|
+
x = expr
|
|
10
|
+
return x
|
|
11
|
+
|
|
12
|
+
After:
|
|
13
|
+
def foo():
|
|
14
|
+
return expr
|
|
15
|
+
|
|
16
|
+
See: https://docs.astral.sh/ruff/rules/unnecessary-assign/
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import Any, List
|
|
20
|
+
|
|
21
|
+
from rewrite import ExecutionContext, Recipe, TreeVisitor
|
|
22
|
+
from rewrite.category import CategoryDescriptor
|
|
23
|
+
from rewrite.decorators import categorize
|
|
24
|
+
from rewrite.execution import InMemoryExecutionContext
|
|
25
|
+
from rewrite.marketplace import Python
|
|
26
|
+
from rewrite.python.tree import StatementExpression
|
|
27
|
+
from rewrite.python.visitor import PythonVisitor
|
|
28
|
+
from rewrite.java.tree import Assignment, Block, Identifier, J, MethodDeclaration, Return
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_Cleanup = [*Python, CategoryDescriptor(display_name="Cleanup")]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@categorize(_Cleanup)
|
|
35
|
+
class RemoveUnnecessaryAssign(Recipe):
|
|
36
|
+
"""
|
|
37
|
+
Remove unnecessary variable assignment before return.
|
|
38
|
+
|
|
39
|
+
When a variable is assigned a value and then immediately returned
|
|
40
|
+
with no other uses, the assignment is unnecessary and the expression
|
|
41
|
+
can be returned directly.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
Before:
|
|
45
|
+
def foo():
|
|
46
|
+
result = compute()
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
After:
|
|
50
|
+
def foo():
|
|
51
|
+
return compute()
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def name(self) -> str:
|
|
56
|
+
return "org.openrewrite.python.cleanup.RemoveUnnecessaryAssign"
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def display_name(self) -> str:
|
|
60
|
+
return "Remove unnecessary assignment before return"
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def description(self) -> str:
|
|
64
|
+
return (
|
|
65
|
+
"Return the value directly where a variable is assigned and then "
|
|
66
|
+
"immediately returned. A name that appears anywhere else in "
|
|
67
|
+
"the function, a `yield` value, and a comment before the `return` are left alone."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def tags(self) -> List[str]:
|
|
72
|
+
return ["python", "cleanup", "ruff", "RET504"]
|
|
73
|
+
|
|
74
|
+
def editor(self) -> TreeVisitor[Any, ExecutionContext]:
|
|
75
|
+
class Visitor(PythonVisitor[ExecutionContext]):
|
|
76
|
+
def visit_block(self, block: Block, p: ExecutionContext) -> J:
|
|
77
|
+
visited = super().visit_block(block, p)
|
|
78
|
+
if not isinstance(visited, Block):
|
|
79
|
+
return visited
|
|
80
|
+
block = visited
|
|
81
|
+
|
|
82
|
+
stmts = block.statements
|
|
83
|
+
if len(stmts) < 2:
|
|
84
|
+
return block
|
|
85
|
+
|
|
86
|
+
# Only check the last two statements in the block
|
|
87
|
+
i = len(stmts) - 2
|
|
88
|
+
assign_stmt = stmts[i]
|
|
89
|
+
return_stmt = stmts[i + 1]
|
|
90
|
+
|
|
91
|
+
if not isinstance(assign_stmt, Assignment):
|
|
92
|
+
return block
|
|
93
|
+
if not isinstance(return_stmt, Return):
|
|
94
|
+
return block
|
|
95
|
+
|
|
96
|
+
# Check that the assignment target is a simple identifier
|
|
97
|
+
if not isinstance(assign_stmt.variable, Identifier):
|
|
98
|
+
return block
|
|
99
|
+
assign_name = assign_stmt.variable.simple_name
|
|
100
|
+
|
|
101
|
+
# Check that the return expression is the same identifier
|
|
102
|
+
returned = return_stmt.expression
|
|
103
|
+
if not isinstance(returned, Identifier) or returned.simple_name != assign_name:
|
|
104
|
+
return block
|
|
105
|
+
|
|
106
|
+
# A handler, a closure or a global/nonlocal binding may read the value,
|
|
107
|
+
# so only the assignment and the return may name it.
|
|
108
|
+
method = self.cursor.first_enclosing(MethodDeclaration)
|
|
109
|
+
if method is None or count_mentions(method, assign_name) != 2:
|
|
110
|
+
return block
|
|
111
|
+
if isinstance(assign_stmt.assignment, StatementExpression):
|
|
112
|
+
return block
|
|
113
|
+
if return_stmt.prefix.comments:
|
|
114
|
+
return block
|
|
115
|
+
|
|
116
|
+
# Build the new return statement with the assignment's RHS
|
|
117
|
+
# Preserve the prefix (whitespace) from the assignment statement
|
|
118
|
+
rhs = assign_stmt.assignment
|
|
119
|
+
new_return = return_stmt.replace(
|
|
120
|
+
_prefix=assign_stmt.prefix,
|
|
121
|
+
_expression=rhs,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Build new padded statement list: remove assignment, replace return
|
|
125
|
+
padded_stmts = block.padding.statements
|
|
126
|
+
new_padded = []
|
|
127
|
+
for j, ps in enumerate(padded_stmts):
|
|
128
|
+
if j == i:
|
|
129
|
+
continue
|
|
130
|
+
elif j == i + 1:
|
|
131
|
+
new_padded.append(ps.replace(element=new_return))
|
|
132
|
+
else:
|
|
133
|
+
new_padded.append(ps)
|
|
134
|
+
|
|
135
|
+
return block.padding.replace(_statements=new_padded)
|
|
136
|
+
|
|
137
|
+
return Visitor()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def count_mentions(tree: J, name: str) -> int:
|
|
141
|
+
"""How many identifiers in ``tree`` spell ``name``."""
|
|
142
|
+
|
|
143
|
+
class NameCounter(PythonVisitor[ExecutionContext]):
|
|
144
|
+
count = 0
|
|
145
|
+
|
|
146
|
+
def visit_identifier(self, ident: Identifier, p: ExecutionContext) -> J:
|
|
147
|
+
if ident.simple_name == name:
|
|
148
|
+
self.count += 1
|
|
149
|
+
return ident
|
|
150
|
+
|
|
151
|
+
counter = NameCounter()
|
|
152
|
+
counter.visit(tree, InMemoryExecutionContext())
|
|
153
|
+
return counter.count
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recipes that replace ``getattr()`` / ``setattr()`` calls with direct attribute
|
|
3
|
+
access or assignment when the attribute name is a constant valid identifier.
|
|
4
|
+
|
|
5
|
+
- ``getattr(x, "attr")`` becomes ``x.attr`` (B009)
|
|
6
|
+
- ``setattr(x, "attr", val)`` becomes ``x.attr = val`` (B010)
|
|
7
|
+
|
|
8
|
+
See:
|
|
9
|
+
- https://docs.astral.sh/ruff/rules/getattr-with-constant/
|
|
10
|
+
- https://docs.astral.sh/ruff/rules/set-attr-with-constant/
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import keyword
|
|
14
|
+
from functools import lru_cache
|
|
15
|
+
from typing import Any, List, Optional
|
|
16
|
+
|
|
17
|
+
from rewrite import ExecutionContext, Preconditions, Recipe, TreeVisitor
|
|
18
|
+
from rewrite.category import CategoryDescriptor
|
|
19
|
+
from rewrite.decorators import categorize
|
|
20
|
+
from rewrite.markers import Markers
|
|
21
|
+
from rewrite.marketplace import Python
|
|
22
|
+
from rewrite.python.method_matcher import MethodMatcher
|
|
23
|
+
from rewrite.python.preconditions import uses_method
|
|
24
|
+
from rewrite.python.visitor import PythonVisitor
|
|
25
|
+
from rewrite.python.template import Template, template, capture
|
|
26
|
+
from rewrite.java.support_types import JLeftPadded, Space
|
|
27
|
+
from rewrite.java.tree import (
|
|
28
|
+
ArrayAccess,
|
|
29
|
+
Block,
|
|
30
|
+
FieldAccess,
|
|
31
|
+
Identifier,
|
|
32
|
+
J,
|
|
33
|
+
Literal,
|
|
34
|
+
MethodInvocation,
|
|
35
|
+
Parentheses,
|
|
36
|
+
)
|
|
37
|
+
from rewrite.python.tree import CompilationUnit
|
|
38
|
+
from rewrite.utils import random_id
|
|
39
|
+
|
|
40
|
+
from openrewrite_code_quality_python.codequality._side_effects import is_side_effect_free
|
|
41
|
+
from openrewrite_code_quality_python.codequality._trivia import has_comments
|
|
42
|
+
|
|
43
|
+
_Cleanup = [*Python, CategoryDescriptor(display_name="Cleanup")]
|
|
44
|
+
|
|
45
|
+
_GETATTR_PATTERN = "builtins getattr(..)"
|
|
46
|
+
_SETATTR_PATTERN = "builtins setattr(..)"
|
|
47
|
+
_GETATTR = MethodMatcher.create(_GETATTR_PATTERN)
|
|
48
|
+
_SETATTR = MethodMatcher.create(_SETATTR_PATTERN)
|
|
49
|
+
|
|
50
|
+
_obj = capture('obj')
|
|
51
|
+
_val = capture('val')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@lru_cache(maxsize=None)
|
|
55
|
+
def _assignment_template(attr_name: str) -> Template:
|
|
56
|
+
return template(f"{_obj}.{attr_name} = {_val}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _constant_attribute_name(method: MethodInvocation) -> Optional[str]:
|
|
60
|
+
"""The attribute name a builtin ``getattr``/``setattr`` call spells as a literal,
|
|
61
|
+
when attribute syntax reads the same attribute."""
|
|
62
|
+
attr_arg = method.arguments[1]
|
|
63
|
+
attr_name = attr_arg.value if isinstance(attr_arg, Literal) else None
|
|
64
|
+
if (
|
|
65
|
+
not isinstance(attr_name, str)
|
|
66
|
+
or not attr_name.isidentifier()
|
|
67
|
+
or keyword.iskeyword(attr_name)
|
|
68
|
+
# Attribute syntax NFKC-normalizes a non-ASCII name, `getattr` does not.
|
|
69
|
+
or not attr_name.isascii()
|
|
70
|
+
# Inside a class `self.__x` is mangled to `self._Class__x`, but
|
|
71
|
+
# `getattr(self, "__x")` looks up the literal `__x`.
|
|
72
|
+
or (attr_name.startswith('__') and not attr_name.endswith('__'))
|
|
73
|
+
or has_comments(method.replace(_prefix=Space.EMPTY))
|
|
74
|
+
):
|
|
75
|
+
return None
|
|
76
|
+
return attr_name
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@categorize(_Cleanup)
|
|
80
|
+
class ReplaceGetAttrWithConstant(Recipe):
|
|
81
|
+
"""
|
|
82
|
+
Replace `getattr(x, "attr")` with `x.attr`.
|
|
83
|
+
|
|
84
|
+
The `getattr()` builtin with a constant string attribute name that is a
|
|
85
|
+
valid Python identifier can be replaced with direct attribute access for
|
|
86
|
+
clarity and brevity.
|
|
87
|
+
|
|
88
|
+
Only applies when:
|
|
89
|
+
- There are exactly 2 arguments (no default value)
|
|
90
|
+
- The second argument is a string literal
|
|
91
|
+
- The string value is an ASCII Python identifier (not a keyword)
|
|
92
|
+
- The object is a name, attribute, call, subscript or parenthesized expression
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
Before:
|
|
96
|
+
result = getattr(obj, "name")
|
|
97
|
+
|
|
98
|
+
After:
|
|
99
|
+
result = obj.name
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def name(self) -> str:
|
|
104
|
+
return "org.openrewrite.python.cleanup.ReplaceGetAttrWithConstant"
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def display_name(self) -> str:
|
|
108
|
+
return "Replace `getattr(x, 'attr')` with `x.attr`"
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def description(self) -> str:
|
|
112
|
+
return (
|
|
113
|
+
"Replace `getattr(x, \"attr\")` calls with `x.attr` when the attribute "
|
|
114
|
+
"name is a constant string that is a valid Python identifier. "
|
|
115
|
+
"It leaves alone a shadowed `getattr`, a non-ASCII or name-mangled name, "
|
|
116
|
+
"an object that would need parentheses, and a call holding comments."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def tags(self) -> List[str]:
|
|
121
|
+
return ["python", "cleanup", "ruff", "B009"]
|
|
122
|
+
|
|
123
|
+
def editor(self) -> TreeVisitor[Any, ExecutionContext]:
|
|
124
|
+
class Visitor(PythonVisitor[ExecutionContext]):
|
|
125
|
+
def visit_method_invocation(
|
|
126
|
+
self, method: MethodInvocation, p: ExecutionContext
|
|
127
|
+
) -> J:
|
|
128
|
+
m = super().visit_method_invocation(method, p)
|
|
129
|
+
if (
|
|
130
|
+
not isinstance(m, MethodInvocation)
|
|
131
|
+
or not _GETATTR.matches(m, cursor=self.cursor)
|
|
132
|
+
or len(m.arguments) != 2
|
|
133
|
+
):
|
|
134
|
+
return m
|
|
135
|
+
attr_name = _constant_attribute_name(m)
|
|
136
|
+
obj_expr = m.arguments[0]
|
|
137
|
+
if attr_name is None or not isinstance(
|
|
138
|
+
obj_expr, (Identifier, FieldAccess, MethodInvocation, ArrayAccess, Parentheses)
|
|
139
|
+
):
|
|
140
|
+
return m
|
|
141
|
+
|
|
142
|
+
attr_identifier = Identifier(
|
|
143
|
+
_id=random_id(),
|
|
144
|
+
_prefix=Space.EMPTY,
|
|
145
|
+
_markers=Markers.EMPTY,
|
|
146
|
+
_annotations=[],
|
|
147
|
+
_simple_name=attr_name,
|
|
148
|
+
_type=None,
|
|
149
|
+
_field_type=None,
|
|
150
|
+
)
|
|
151
|
+
return FieldAccess(
|
|
152
|
+
_id=random_id(),
|
|
153
|
+
_prefix=m.prefix,
|
|
154
|
+
_markers=Markers.EMPTY,
|
|
155
|
+
_target=obj_expr.replace(_prefix=Space.EMPTY),
|
|
156
|
+
_name=JLeftPadded(
|
|
157
|
+
_before=Space.EMPTY,
|
|
158
|
+
_element=attr_identifier,
|
|
159
|
+
_markers=Markers.EMPTY,
|
|
160
|
+
),
|
|
161
|
+
_type=None,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return Preconditions.check(uses_method(_GETATTR_PATTERN), Visitor())
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@categorize(_Cleanup)
|
|
168
|
+
class ReplaceSetAttrWithConstant(Recipe):
|
|
169
|
+
"""
|
|
170
|
+
Replace `setattr(x, "attr", value)` with `x.attr = value`.
|
|
171
|
+
|
|
172
|
+
When `setattr()` is called as a statement with a constant string attribute
|
|
173
|
+
name that is a valid Python identifier, it can be replaced with a direct
|
|
174
|
+
attribute assignment for better readability.
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
Before:
|
|
178
|
+
setattr(obj, "name", value)
|
|
179
|
+
|
|
180
|
+
After:
|
|
181
|
+
obj.name = value
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def name(self) -> str:
|
|
186
|
+
return "org.openrewrite.python.cleanup.ReplaceSetAttrWithConstant"
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def display_name(self) -> str:
|
|
190
|
+
return "Replace `setattr()` with constant attribute assignment"
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def description(self) -> str:
|
|
194
|
+
return (
|
|
195
|
+
"Replace a `setattr(x, \"attr\", value)` statement with `x.attr = value` "
|
|
196
|
+
"when the attribute name is a constant valid Python identifier. "
|
|
197
|
+
"It leaves alone a shadowed `setattr`, a non-ASCII or name-mangled name, "
|
|
198
|
+
"a call used as an expression, a call holding comments, and an object or value "
|
|
199
|
+
"that may have side effects, since the assignment evaluates the value first."
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def tags(self) -> List[str]:
|
|
204
|
+
return ["python", "cleanup", "ruff", "B010"]
|
|
205
|
+
|
|
206
|
+
def editor(self) -> TreeVisitor[Any, ExecutionContext]:
|
|
207
|
+
class Visitor(PythonVisitor[ExecutionContext]):
|
|
208
|
+
def visit_method_invocation(
|
|
209
|
+
self, method: MethodInvocation, p: ExecutionContext
|
|
210
|
+
) -> J:
|
|
211
|
+
m = super().visit_method_invocation(method, p)
|
|
212
|
+
if (
|
|
213
|
+
not isinstance(m, MethodInvocation)
|
|
214
|
+
or not _SETATTR.matches(m, cursor=self.cursor)
|
|
215
|
+
or len(m.arguments) != 3
|
|
216
|
+
or not isinstance(
|
|
217
|
+
self.cursor.parent_tree_cursor().value, (Block, CompilationUnit)
|
|
218
|
+
)
|
|
219
|
+
):
|
|
220
|
+
return m
|
|
221
|
+
attr_name = _constant_attribute_name(m)
|
|
222
|
+
obj_expr, _, val_expr = m.arguments
|
|
223
|
+
if (
|
|
224
|
+
attr_name is None
|
|
225
|
+
or not is_side_effect_free(obj_expr)
|
|
226
|
+
or not is_side_effect_free(val_expr)
|
|
227
|
+
):
|
|
228
|
+
return m
|
|
229
|
+
|
|
230
|
+
return _assignment_template(attr_name).apply(
|
|
231
|
+
self.cursor, values={'obj': obj_expr, 'val': val_expr}
|
|
232
|
+
) or m
|
|
233
|
+
|
|
234
|
+
return Preconditions.check(uses_method(_SETATTR_PATTERN), Visitor())
|