auto-walrus 0.3.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.
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: auto-walrus
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Automatically apply the awesome walrus operator
|
|
5
|
+
Project-URL: Homepage, https://github.com/MarcoGorelli/auto-walrus
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/MarcoGorelli/auto-walrus
|
|
7
|
+
Author-email: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com>
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
<h1 align="center">
|
|
16
|
+
auto-walrus
|
|
17
|
+
</h1>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<img width="458" alt="auto-walrus" src="https://user-images.githubusercontent.com/33491632/195613331-f7442140-09da-4376-90aa-2ac4aaa242fa.png">
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
auto-walrus
|
|
24
|
+
===========
|
|
25
|
+
[](https://github.com/MarcoGorelli/auto-walrus/actions?workflow=tox)
|
|
26
|
+
[](https://codecov.io/gh/MarcoGorelli/auto-walrus)
|
|
27
|
+
[](https://results.pre-commit.ci/latest/github/MarcoGorelli/auto-walrus/main)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
A tool and pre-commit hook to automatically apply the awesome walrus operator.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```console
|
|
36
|
+
pip install auto-walrus
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage as a pre-commit hook
|
|
40
|
+
|
|
41
|
+
See [pre-commit](https://github.com/pre-commit/pre-commit) for instructions
|
|
42
|
+
|
|
43
|
+
Sample `.pre-commit-config.yaml`:
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
- repo: https://github.com/MarcoGorelli/auto-walrus
|
|
47
|
+
rev: v0.2.2
|
|
48
|
+
hooks:
|
|
49
|
+
- id: auto-walrus
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Command-line example
|
|
53
|
+
|
|
54
|
+
```console
|
|
55
|
+
auto-walrus myfile.py
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```diff
|
|
59
|
+
- n = 10
|
|
60
|
+
- if n > 3:
|
|
61
|
+
+ if (n := 10) > 3:
|
|
62
|
+
print(n)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
Using the walrus operator can result in longer lines. Lines longer than what you
|
|
68
|
+
pass to ``--line-length`` won't be rewritten to use walrus operators.
|
|
69
|
+
|
|
70
|
+
E.g.
|
|
71
|
+
```
|
|
72
|
+
auto-walrus myfile_1.py myfile_2.py --line-length 89
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Lines with comments won't be rewritten.
|
|
76
|
+
|
|
77
|
+
## Used by
|
|
78
|
+
|
|
79
|
+
To my great surprise, this is being used by:
|
|
80
|
+
|
|
81
|
+
- https://github.com/python-graphblas/python-graphblas
|
|
82
|
+
- https://github.com/Remi-Gau/bids2cite
|
|
83
|
+
- https://github.com/TheAlgorithms/Python
|
|
84
|
+
- https://github.com/apache/superset
|
|
85
|
+
|
|
86
|
+
Anyone else? Please let me know, or you can open a pull request to add yourself.
|
|
87
|
+
|
|
88
|
+
## Testimonials
|
|
89
|
+
|
|
90
|
+
**Christopher Redwine**, [Senior Software Engineer at TechnologyAdvice](https://github.com/chrisRedwine)
|
|
91
|
+
|
|
92
|
+
> hmm, i dunno about this one chief
|
|
93
|
+
|
|
94
|
+
**Michael Kennedy & Brian Okken**, [hosts of the Python Bytes podcast](https://pythonbytes.fm/):
|
|
95
|
+
|
|
96
|
+
> I kind of like this being separate from other tools
|
|
97
|
+
|
|
98
|
+
**Someone on Discord**
|
|
99
|
+
|
|
100
|
+
> you're a monster
|
|
101
|
+
|
|
102
|
+
**Will McGugan**, [CEO / Founder of http://Textualize.io](https://www.willmcgugan.com/):
|
|
103
|
+
|
|
104
|
+
> Embrace the Walrus!
|
|
105
|
+
|
|
106
|
+
## Credits
|
|
107
|
+
|
|
108
|
+
Logo by [lion_space](https://www.fiverr.com/lion_space)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
auto_walrus.py,sha256=VI_WBQuwOu7OUY6fkul4wqRKedOWF9tt8U8GV8ZJiwk,12579
|
|
2
|
+
auto_walrus-0.3.0.dist-info/METADATA,sha256=y_wh-WMBR4Siy5ciZNrQIXXj3WjV2VhOVvqVzozSe_4,2936
|
|
3
|
+
auto_walrus-0.3.0.dist-info/WHEEL,sha256=as-1oFTWSeWBgyzh0O_qF439xqBe6AbBgt4MfYe5zwY,87
|
|
4
|
+
auto_walrus-0.3.0.dist-info/licenses/LICENSE,sha256=YhJBcZzQmTfUwcrA-3FDZB3hiSIawDPD-VJSugAZXLY,1071
|
|
5
|
+
auto_walrus-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022, Marco Gorelli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
auto_walrus.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import ast
|
|
5
|
+
import os
|
|
6
|
+
import pathlib
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Any
|
|
10
|
+
from typing import Iterable
|
|
11
|
+
from typing import Sequence
|
|
12
|
+
from typing import Tuple
|
|
13
|
+
|
|
14
|
+
if sys.version_info >= (3, 11): # pragma: no cover
|
|
15
|
+
import tomllib
|
|
16
|
+
else: # pragma: no cover
|
|
17
|
+
import tomli as tomllib
|
|
18
|
+
|
|
19
|
+
SEP_SYMBOLS = frozenset(("(", ")", ",", ":"))
|
|
20
|
+
# name, lineno, col_offset, end_lineno, end_col_offset
|
|
21
|
+
Token = Tuple[str, int, int, int, int]
|
|
22
|
+
SIMPLE_NODE = (ast.Name, ast.Constant)
|
|
23
|
+
ENDS_WITH_COMMENT = re.compile(r"#.*$")
|
|
24
|
+
EXCLUDES = (
|
|
25
|
+
r"/("
|
|
26
|
+
r"\.direnv|\.eggs|\.git|\.hg|\.ipynb_checkpoints|\.mypy_cache|\.nox|\.svn|"
|
|
27
|
+
r"\.tox|\.venv|"
|
|
28
|
+
r"_build|buck-out|build|dist|venv"
|
|
29
|
+
r")/"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def name_lineno_coloffset_iterable(
|
|
34
|
+
tokens: Iterable[Token],
|
|
35
|
+
) -> list[tuple[str, int, int]]:
|
|
36
|
+
return [(i[0], i[1], i[2]) for i in tokens]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def name_lineno_coloffset(tokens: Token) -> tuple[str, int, int]:
|
|
40
|
+
return (tokens[0], tokens[1], tokens[2])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_simple_test(node: ast.AST) -> bool:
|
|
44
|
+
return isinstance(node, SIMPLE_NODE) or (
|
|
45
|
+
isinstance(node, ast.Compare)
|
|
46
|
+
and isinstance(node.left, SIMPLE_NODE)
|
|
47
|
+
and (all(isinstance(_node, SIMPLE_NODE) for _node in node.comparators))
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def record_name_lineno_coloffset(
|
|
52
|
+
node: ast.Name,
|
|
53
|
+
end_lineno: int | None = None,
|
|
54
|
+
end_col_offset: int | None = None,
|
|
55
|
+
) -> Token:
|
|
56
|
+
if end_lineno is None:
|
|
57
|
+
assert node.end_lineno is not None
|
|
58
|
+
_end_lineno = node.end_lineno
|
|
59
|
+
else:
|
|
60
|
+
_end_lineno = end_lineno
|
|
61
|
+
if end_col_offset is None:
|
|
62
|
+
assert node.end_col_offset is not None
|
|
63
|
+
_end_col_offset = node.end_col_offset
|
|
64
|
+
else:
|
|
65
|
+
_end_col_offset = end_col_offset
|
|
66
|
+
return (
|
|
67
|
+
node.id,
|
|
68
|
+
node.lineno,
|
|
69
|
+
node.col_offset,
|
|
70
|
+
_end_lineno,
|
|
71
|
+
_end_col_offset,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def find_names(
|
|
76
|
+
node: ast.AST,
|
|
77
|
+
end_lineno: int | None = None,
|
|
78
|
+
end_col_offset: int | None = None,
|
|
79
|
+
) -> set[Token]:
|
|
80
|
+
names = set()
|
|
81
|
+
for _node in ast.walk(node):
|
|
82
|
+
if isinstance(_node, ast.Name):
|
|
83
|
+
names.add(
|
|
84
|
+
record_name_lineno_coloffset(
|
|
85
|
+
_node,
|
|
86
|
+
end_lineno,
|
|
87
|
+
end_col_offset,
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
return names
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def process_if(
|
|
94
|
+
node: ast.If,
|
|
95
|
+
in_body_vars: dict[Token, set[Token]],
|
|
96
|
+
) -> set[Token]:
|
|
97
|
+
_names = find_names(node.test)
|
|
98
|
+
_body_names = {_name for _body in node.body for _name in find_names(_body)}
|
|
99
|
+
for _name in _names:
|
|
100
|
+
in_body_vars[_name] = _body_names
|
|
101
|
+
return _names
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def process_assign(
|
|
105
|
+
node: ast.Assign,
|
|
106
|
+
assignments: set[Token],
|
|
107
|
+
related_vars: dict[str, list[Token]],
|
|
108
|
+
) -> None:
|
|
109
|
+
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
|
110
|
+
target = node.targets[0]
|
|
111
|
+
assignments.add(
|
|
112
|
+
record_name_lineno_coloffset(
|
|
113
|
+
target,
|
|
114
|
+
node.end_lineno,
|
|
115
|
+
node.end_col_offset,
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
related_vars[target.id] = list(find_names(node.value))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def is_walrussable(
|
|
122
|
+
_assignment: Token,
|
|
123
|
+
_if_statement: Token,
|
|
124
|
+
sorted_names: list[Token],
|
|
125
|
+
assignment_idx: int,
|
|
126
|
+
if_statement_idx: int,
|
|
127
|
+
_other_assignments: list[Token],
|
|
128
|
+
_other_usages: list[Token],
|
|
129
|
+
names: set[Token],
|
|
130
|
+
in_body_vars: dict[Token, set[Token]],
|
|
131
|
+
) -> bool:
|
|
132
|
+
return (
|
|
133
|
+
# check name doesn't appear between assignment and if statement
|
|
134
|
+
_assignment[0]
|
|
135
|
+
not in [sorted_names[i][0] for i in range(assignment_idx + 1, if_statement_idx)]
|
|
136
|
+
# check it's the variable's only assignment
|
|
137
|
+
and (len(_other_assignments) == 1)
|
|
138
|
+
# check this is the first usage of this name
|
|
139
|
+
and (
|
|
140
|
+
name_lineno_coloffset(
|
|
141
|
+
_other_usages[0],
|
|
142
|
+
)
|
|
143
|
+
== name_lineno_coloffset(_assignment)
|
|
144
|
+
)
|
|
145
|
+
# check it's used at least somewhere else
|
|
146
|
+
and (len(_other_usages) > 2)
|
|
147
|
+
# check it doesn't appear anywhere else
|
|
148
|
+
and not [
|
|
149
|
+
i
|
|
150
|
+
for i in names
|
|
151
|
+
if (
|
|
152
|
+
name_lineno_coloffset(i)
|
|
153
|
+
not in name_lineno_coloffset_iterable(in_body_vars[_if_statement])
|
|
154
|
+
)
|
|
155
|
+
and (
|
|
156
|
+
name_lineno_coloffset(
|
|
157
|
+
i,
|
|
158
|
+
)
|
|
159
|
+
!= name_lineno_coloffset(_assignment)
|
|
160
|
+
)
|
|
161
|
+
and (
|
|
162
|
+
name_lineno_coloffset(
|
|
163
|
+
i,
|
|
164
|
+
)
|
|
165
|
+
!= name_lineno_coloffset(_if_statement)
|
|
166
|
+
)
|
|
167
|
+
and i[0] == _assignment[0]
|
|
168
|
+
]
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def related_vars_are_unused(
|
|
173
|
+
related_vars: dict[str, list[Token]],
|
|
174
|
+
name: str,
|
|
175
|
+
sorted_names: list[Token],
|
|
176
|
+
assignment_idx: int,
|
|
177
|
+
if_statement_idx: int,
|
|
178
|
+
) -> bool:
|
|
179
|
+
# Check that names which appear in right hand side of
|
|
180
|
+
# assignment aren't used between assignment and if-statement.
|
|
181
|
+
related = related_vars[name]
|
|
182
|
+
should_break = False
|
|
183
|
+
for rel in related:
|
|
184
|
+
usages = [i for i in sorted_names if i[0] == rel[0] if i != rel]
|
|
185
|
+
for usage in usages:
|
|
186
|
+
rel_used_idx = name_lineno_coloffset_iterable(
|
|
187
|
+
sorted_names,
|
|
188
|
+
).index(name_lineno_coloffset(usage))
|
|
189
|
+
if assignment_idx < rel_used_idx < if_statement_idx:
|
|
190
|
+
should_break = True
|
|
191
|
+
return not should_break
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def visit_function_def(
|
|
195
|
+
node: ast.FunctionDef,
|
|
196
|
+
) -> list[tuple[Token, Token]]:
|
|
197
|
+
names = set()
|
|
198
|
+
assignments: set[Token] = set()
|
|
199
|
+
ifs = set()
|
|
200
|
+
for _node in ast.walk(node):
|
|
201
|
+
if isinstance(_node, ast.Name):
|
|
202
|
+
names.add(record_name_lineno_coloffset(_node))
|
|
203
|
+
|
|
204
|
+
related_vars: dict[str, list[Token]] = {}
|
|
205
|
+
in_body_vars: dict[Token, set[Token]] = {}
|
|
206
|
+
|
|
207
|
+
for _node in node.body:
|
|
208
|
+
if isinstance(_node, ast.Assign):
|
|
209
|
+
process_assign(_node, assignments, related_vars)
|
|
210
|
+
elif isinstance(_node, ast.If):
|
|
211
|
+
if is_simple_test(_node.test):
|
|
212
|
+
ifs.update(process_if(_node, in_body_vars))
|
|
213
|
+
for __node in _node.orelse:
|
|
214
|
+
if isinstance(__node, ast.If) and is_simple_test(__node.test):
|
|
215
|
+
ifs.update(process_if(__node, in_body_vars))
|
|
216
|
+
|
|
217
|
+
sorted_names = sorted(names, key=lambda x: (x[1], x[2]))
|
|
218
|
+
sorted_assignments = sorted(assignments, key=lambda x: (x[1], x[2]))
|
|
219
|
+
sorted_ifs = sorted(ifs, key=lambda x: (x[1], x[2]))
|
|
220
|
+
walrus = []
|
|
221
|
+
|
|
222
|
+
for _assignment in sorted_assignments:
|
|
223
|
+
_if_statements = [i for i in sorted_ifs if i[0] == _assignment[0]]
|
|
224
|
+
if len(_if_statements) != 1:
|
|
225
|
+
continue
|
|
226
|
+
_if_statement = _if_statements[0]
|
|
227
|
+
assignment_idx = name_lineno_coloffset_iterable(
|
|
228
|
+
sorted_names,
|
|
229
|
+
).index(name_lineno_coloffset(_assignment))
|
|
230
|
+
if_statement_idx = name_lineno_coloffset_iterable(
|
|
231
|
+
sorted_names,
|
|
232
|
+
).index(name_lineno_coloffset(_if_statement))
|
|
233
|
+
_other_assignments = [i for i in sorted_assignments if i[0] == _assignment[0]]
|
|
234
|
+
_other_usages = [i for i in sorted_names if i[0] == _assignment[0]]
|
|
235
|
+
if is_walrussable(
|
|
236
|
+
_assignment,
|
|
237
|
+
_if_statement,
|
|
238
|
+
sorted_names,
|
|
239
|
+
assignment_idx,
|
|
240
|
+
if_statement_idx,
|
|
241
|
+
_other_assignments,
|
|
242
|
+
_other_usages,
|
|
243
|
+
names,
|
|
244
|
+
in_body_vars,
|
|
245
|
+
) and related_vars_are_unused(
|
|
246
|
+
related_vars,
|
|
247
|
+
_assignment[0],
|
|
248
|
+
sorted_names,
|
|
249
|
+
assignment_idx,
|
|
250
|
+
if_statement_idx,
|
|
251
|
+
):
|
|
252
|
+
walrus.append((_assignment, _if_statement))
|
|
253
|
+
return walrus
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def auto_walrus(
|
|
257
|
+
content: str,
|
|
258
|
+
line_length: int,
|
|
259
|
+
) -> str | None:
|
|
260
|
+
lines = content.splitlines()
|
|
261
|
+
try:
|
|
262
|
+
tree = ast.parse(content)
|
|
263
|
+
except SyntaxError: # pragma: no cover
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
walruses = []
|
|
267
|
+
for node in ast.walk(tree):
|
|
268
|
+
if isinstance(node, ast.FunctionDef):
|
|
269
|
+
walruses.extend(visit_function_def(node))
|
|
270
|
+
lines_to_remove = []
|
|
271
|
+
walruses = sorted(walruses, key=lambda x: (-x[1][1], -x[1][2]))
|
|
272
|
+
|
|
273
|
+
if not walruses:
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
for _assignment, _if_statement in walruses:
|
|
277
|
+
if _assignment[1] != _assignment[3]:
|
|
278
|
+
continue
|
|
279
|
+
txt = lines[_assignment[1] - 1][_assignment[2] : _assignment[4]]
|
|
280
|
+
if txt.count("=") > 1:
|
|
281
|
+
continue
|
|
282
|
+
line = lines[_if_statement[1] - 1]
|
|
283
|
+
left_bit = line[: _if_statement[2]]
|
|
284
|
+
right_bit = line[_if_statement[4] :]
|
|
285
|
+
no_paren = any(left_bit.endswith(i) for i in SEP_SYMBOLS) and any(
|
|
286
|
+
right_bit.startswith(i) for i in SEP_SYMBOLS
|
|
287
|
+
)
|
|
288
|
+
replace = txt.replace("=", ":=")
|
|
289
|
+
if no_paren:
|
|
290
|
+
line_with_walrus = left_bit + replace + right_bit
|
|
291
|
+
else:
|
|
292
|
+
line_with_walrus = left_bit + "(" + replace + ")" + right_bit
|
|
293
|
+
if len(line_with_walrus) > line_length:
|
|
294
|
+
# don't rewrite if it would split over multiple lines
|
|
295
|
+
continue
|
|
296
|
+
# replace assignment
|
|
297
|
+
line_without_assignment = (
|
|
298
|
+
f"{lines[_assignment[1]-1][:_assignment[2]]}"
|
|
299
|
+
f"{lines[_assignment[1]-1][_assignment[4]:]}"
|
|
300
|
+
)
|
|
301
|
+
if (ENDS_WITH_COMMENT.search(lines[_assignment[1] - 1]) is not None) or (
|
|
302
|
+
ENDS_WITH_COMMENT.search(lines[_if_statement[1] - 1]) is not None
|
|
303
|
+
):
|
|
304
|
+
continue
|
|
305
|
+
lines[_assignment[1] - 1] = line_without_assignment
|
|
306
|
+
# add walrus
|
|
307
|
+
lines[_if_statement[1] - 1] = line_with_walrus
|
|
308
|
+
# remove empty line
|
|
309
|
+
if not lines[_assignment[1] - 1].strip():
|
|
310
|
+
lines_to_remove.append(_assignment[1] - 1)
|
|
311
|
+
|
|
312
|
+
newlines = [
|
|
313
|
+
line
|
|
314
|
+
for i, line in enumerate(
|
|
315
|
+
lines,
|
|
316
|
+
)
|
|
317
|
+
if i not in lines_to_remove
|
|
318
|
+
]
|
|
319
|
+
newcontent = "\n".join(newlines)
|
|
320
|
+
if newcontent and content.endswith("\n"):
|
|
321
|
+
newcontent += "\n"
|
|
322
|
+
if newcontent != content:
|
|
323
|
+
return newcontent
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _get_config(paths: list[pathlib.Path]) -> dict[str, Any]:
|
|
328
|
+
"""Get the configuration from a config file.
|
|
329
|
+
|
|
330
|
+
Search for a pyproject.toml in common parent directories
|
|
331
|
+
of the given list of paths.
|
|
332
|
+
"""
|
|
333
|
+
root = pathlib.Path(os.path.commonpath(paths))
|
|
334
|
+
root = root.parent if root.is_file() else root
|
|
335
|
+
|
|
336
|
+
while root != root.parent:
|
|
337
|
+
config_file = root / "pyproject.toml"
|
|
338
|
+
if config_file.is_file():
|
|
339
|
+
config = tomllib.loads(config_file.read_text())
|
|
340
|
+
config = config.get("tool", {}).get("auto-walrus", {})
|
|
341
|
+
if config:
|
|
342
|
+
return config
|
|
343
|
+
|
|
344
|
+
root = root.parent
|
|
345
|
+
|
|
346
|
+
return {}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def main(argv: Sequence[str] | None = None) -> int: # pragma: no cover
|
|
350
|
+
parser = argparse.ArgumentParser()
|
|
351
|
+
parser.add_argument("paths", nargs="*")
|
|
352
|
+
parser.add_argument(
|
|
353
|
+
"--files",
|
|
354
|
+
help="Regex pattern with which to match files to include",
|
|
355
|
+
required=False,
|
|
356
|
+
default=r"",
|
|
357
|
+
)
|
|
358
|
+
parser.add_argument(
|
|
359
|
+
"--exclude",
|
|
360
|
+
help="Regex pattern with which to match files to exclude",
|
|
361
|
+
required=False,
|
|
362
|
+
default=r"^$",
|
|
363
|
+
)
|
|
364
|
+
# black formatter's default
|
|
365
|
+
parser.add_argument("--line-length", type=int, default=88)
|
|
366
|
+
args = parser.parse_args(argv)
|
|
367
|
+
paths = [pathlib.Path(path).resolve() for path in args.paths]
|
|
368
|
+
|
|
369
|
+
# Update defaults from pyproject.toml if present
|
|
370
|
+
config = {k.replace("-", "_"): v for k, v in _get_config(paths).items()}
|
|
371
|
+
parser.set_defaults(**config)
|
|
372
|
+
args = parser.parse_args(argv)
|
|
373
|
+
|
|
374
|
+
ret = 0
|
|
375
|
+
|
|
376
|
+
for path in paths:
|
|
377
|
+
if path.is_file():
|
|
378
|
+
filepaths = iter((path,))
|
|
379
|
+
else:
|
|
380
|
+
filepaths = (
|
|
381
|
+
p
|
|
382
|
+
for p in path.rglob("*")
|
|
383
|
+
if re.search(args.files, p.as_posix(), re.VERBOSE)
|
|
384
|
+
and not re.search(args.exclude, p.as_posix(), re.VERBOSE)
|
|
385
|
+
and not re.search(EXCLUDES, p.as_posix())
|
|
386
|
+
and p.suffix == ".py"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
for filepath in filepaths:
|
|
390
|
+
try:
|
|
391
|
+
with open(filepath, encoding="utf-8") as fd:
|
|
392
|
+
content = fd.read()
|
|
393
|
+
except UnicodeDecodeError:
|
|
394
|
+
continue
|
|
395
|
+
new_content = auto_walrus(
|
|
396
|
+
content,
|
|
397
|
+
line_length=args.line_length,
|
|
398
|
+
)
|
|
399
|
+
if new_content is not None and content != new_content:
|
|
400
|
+
sys.stdout.write(f"Rewriting {filepath}\n")
|
|
401
|
+
with open(filepath, "w", encoding="utf-8") as fd:
|
|
402
|
+
fd.write(new_content)
|
|
403
|
+
ret = 1
|
|
404
|
+
return ret
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
if __name__ == "__main__":
|
|
408
|
+
sys.exit(main())
|