auto-walrus 0.3.4__tar.gz → 0.4.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.

Potentially problematic release.


This version of auto-walrus might be problematic. Click here for more details.

@@ -8,9 +8,9 @@ jobs:
8
8
  runs-on: ubuntu-latest
9
9
 
10
10
  steps:
11
- - uses: actions/checkout@v4
11
+ - uses: actions/checkout@v6
12
12
  - name: Set up Python
13
- uses: actions/setup-python@v5
13
+ uses: actions/setup-python@v6
14
14
  with:
15
15
  python-version: "3.x"
16
16
  - name: Install pypa/build
@@ -22,7 +22,7 @@ jobs:
22
22
  - name: Build a binary wheel and a source tarball
23
23
  run: python3 -m build
24
24
  - name: Store the distribution packages
25
- uses: actions/upload-artifact@v4
25
+ uses: actions/upload-artifact@v6
26
26
  with:
27
27
  name: python-package-distributions
28
28
  path: dist/
@@ -42,7 +42,7 @@ jobs:
42
42
 
43
43
  steps:
44
44
  - name: Download all the dists
45
- uses: actions/download-artifact@v4
45
+ uses: actions/download-artifact@v7
46
46
  with:
47
47
  name: python-package-distributions
48
48
  path: dist/
@@ -63,12 +63,12 @@ jobs:
63
63
 
64
64
  steps:
65
65
  - name: Download all the dists
66
- uses: actions/download-artifact@v4
66
+ uses: actions/download-artifact@v7
67
67
  with:
68
68
  name: python-package-distributions
69
69
  path: dist/
70
70
  - name: Sign the dists with Sigstore
71
- uses: sigstore/gh-action-sigstore-python@v2.1.1
71
+ uses: sigstore/gh-action-sigstore-python@v3.2.0
72
72
  with:
73
73
  inputs: >-
74
74
  ./dist/*.tar.gz
@@ -9,18 +9,18 @@ jobs:
9
9
  tox:
10
10
  strategy:
11
11
  matrix:
12
- python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
12
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
13
13
  os: [windows-latest, ubuntu-latest]
14
14
 
15
15
  runs-on: ${{ matrix.os }}
16
16
  steps:
17
- - uses: actions/checkout@v4
18
- - uses: actions/setup-python@v5
17
+ - uses: actions/checkout@v6
18
+ - uses: actions/setup-python@v6
19
19
  with:
20
20
  python-version: ${{ matrix.python-version }}
21
21
  allow-prereleases: true
22
22
  - name: Cache multiple paths
23
- uses: actions/cache@v4
23
+ uses: actions/cache@v5
24
24
  with:
25
25
  path: |
26
26
  ~/.cache/pip
@@ -31,3 +31,4 @@ jobs:
31
31
  run: python -m pip install --upgrade tox virtualenv setuptools pip
32
32
  - name: run-tox
33
33
  run: tox -e py
34
+ - run: pipx run auto-walrus -h
@@ -1,7 +1,7 @@
1
1
  repos:
2
2
  - repo: https://github.com/astral-sh/ruff-pre-commit
3
3
  # Ruff version.
4
- rev: 'v0.4.1'
4
+ rev: 'v0.9.2'
5
5
  hooks:
6
6
  # Run the formatter.
7
7
  - id: ruff-format
@@ -9,13 +9,13 @@ repos:
9
9
  - id: ruff
10
10
  args: [--fix]
11
11
  - repo: https://github.com/pre-commit/mirrors-mypy
12
- rev: 'v1.9.0'
12
+ rev: 'v1.14.1'
13
13
  hooks:
14
14
  - id: mypy
15
15
  additional_dependencies: [pytest]
16
16
  exclude: utils
17
17
  - repo: https://github.com/codespell-project/codespell
18
- rev: 'v2.2.6'
18
+ rev: 'v2.3.0'
19
19
  hooks:
20
20
  - id: codespell
21
21
  files: \.(py|rst|md)$
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: auto-walrus
3
- Version: 0.3.4
3
+ Version: 0.4.0
4
4
  Summary: Automatically apply the awesome walrus operator
5
5
  Project-URL: Homepage, https://github.com/MarcoGorelli/auto-walrus
6
6
  Project-URL: Bug Tracker, https://github.com/MarcoGorelli/auto-walrus
@@ -45,7 +45,7 @@ Sample `.pre-commit-config.yaml`:
45
45
 
46
46
  ```yaml
47
47
  - repo: https://github.com/MarcoGorelli/auto-walrus
48
- rev: 0.3.4
48
+ rev: 0.4.0
49
49
  hooks:
50
50
  - id: auto-walrus
51
51
  ```
@@ -30,7 +30,7 @@ Sample `.pre-commit-config.yaml`:
30
30
 
31
31
  ```yaml
32
32
  - repo: https://github.com/MarcoGorelli/auto-walrus
33
- rev: 0.3.4
33
+ rev: 0.4.0
34
34
  hooks:
35
35
  - id: auto-walrus
36
36
  ```
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import ast
5
+ import dataclasses
5
6
  import os
6
7
  import pathlib
7
8
  import re
@@ -30,6 +31,12 @@ EXCLUDES = (
30
31
  )
31
32
 
32
33
 
34
+ @dataclasses.dataclass
35
+ class Config:
36
+ line_length: int
37
+ unsafe: bool = False
38
+
39
+
33
40
  def name_lineno_coloffset_iterable(
34
41
  tokens: Iterable[Token],
35
42
  ) -> list[tuple[str, int, int]]:
@@ -193,6 +200,7 @@ def related_vars_are_unused(
193
200
 
194
201
  def visit_function_def(
195
202
  node: ast.FunctionDef,
203
+ config: Config,
196
204
  ) -> list[tuple[Token, Token]]:
197
205
  names = set()
198
206
  assignments: set[Token] = set()
@@ -204,7 +212,7 @@ def visit_function_def(
204
212
  related_vars: dict[str, list[Token]] = {}
205
213
  in_body_vars: dict[Token, set[Token]] = {}
206
214
 
207
- for _node in node.body:
215
+ for _node in ast.walk(node) if config.unsafe else node.body:
208
216
  if isinstance(_node, ast.Assign):
209
217
  process_assign(_node, assignments, related_vars)
210
218
  elif isinstance(_node, ast.If):
@@ -255,7 +263,7 @@ def visit_function_def(
255
263
 
256
264
  def auto_walrus(
257
265
  content: str,
258
- line_length: int,
266
+ config: Config,
259
267
  ) -> str | None:
260
268
  lines = content.splitlines()
261
269
  try:
@@ -266,7 +274,7 @@ def auto_walrus(
266
274
  walruses = []
267
275
  for node in ast.walk(tree):
268
276
  if isinstance(node, ast.FunctionDef):
269
- walruses.extend(visit_function_def(node))
277
+ walruses.extend(visit_function_def(node, config))
270
278
  lines_to_remove = []
271
279
  walruses = sorted(walruses, key=lambda x: (-x[1][1], -x[1][2]))
272
280
 
@@ -290,13 +298,13 @@ def auto_walrus(
290
298
  line_with_walrus = left_bit + replace + right_bit
291
299
  else:
292
300
  line_with_walrus = left_bit + "(" + replace + ")" + right_bit
293
- if len(line_with_walrus) > line_length:
301
+ if len(line_with_walrus) > config.line_length:
294
302
  # don't rewrite if it would split over multiple lines
295
303
  continue
296
304
  # replace assignment
297
305
  line_without_assignment = (
298
- f"{lines[_assignment[1]-1][:_assignment[2]]}"
299
- f"{lines[_assignment[1]-1][_assignment[4]:]}"
306
+ f"{lines[_assignment[1] - 1][: _assignment[2]]}"
307
+ f"{lines[_assignment[1] - 1][_assignment[4] :]}"
300
308
  )
301
309
  if (ENDS_WITH_COMMENT.search(lines[_assignment[1] - 1]) is not None) or (
302
310
  ENDS_WITH_COMMENT.search(lines[_if_statement[1] - 1]) is not None
@@ -348,7 +356,7 @@ def _get_config(paths: list[pathlib.Path]) -> dict[str, Any]:
348
356
 
349
357
  def main(argv: Sequence[str] | None = None) -> int: # pragma: no cover
350
358
  parser = argparse.ArgumentParser()
351
- parser.add_argument("paths", nargs="*")
359
+ parser.add_argument("paths", nargs="+", metavar="path")
352
360
  parser.add_argument(
353
361
  "--files",
354
362
  help="Regex pattern with which to match files to include",
@@ -361,18 +369,24 @@ def main(argv: Sequence[str] | None = None) -> int: # pragma: no cover
361
369
  required=False,
362
370
  default=r"^$",
363
371
  )
372
+ parser.add_argument(
373
+ "--unsafe",
374
+ action="store_true",
375
+ help="Also process if statements inside other blocks (like for loops)",
376
+ )
364
377
  # black formatter's default
365
378
  parser.add_argument("--line-length", type=int, default=88)
366
379
  args = parser.parse_args(argv)
367
380
  paths = [pathlib.Path(path).resolve() for path in args.paths]
368
381
 
369
382
  # 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)
383
+ defaults = {k.replace("-", "_"): v for k, v in _get_config(paths).items()}
384
+ parser.set_defaults(**defaults)
372
385
  args = parser.parse_args(argv)
373
386
 
374
387
  ret = 0
375
388
 
389
+ config = Config(line_length=args.line_length, unsafe=args.unsafe)
376
390
  for path in paths:
377
391
  if path.is_file():
378
392
  filepaths = iter((path,))
@@ -382,7 +396,7 @@ def main(argv: Sequence[str] | None = None) -> int: # pragma: no cover
382
396
  for p in path.rglob("*")
383
397
  if re.search(args.files, p.as_posix(), re.VERBOSE)
384
398
  and not re.search(args.exclude, p.as_posix(), re.VERBOSE)
385
- and not re.search(EXCLUDES, p.as_posix())
399
+ and not re.search(EXCLUDES, p.relative_to(path).as_posix())
386
400
  and p.suffix == ".py"
387
401
  )
388
402
 
@@ -392,10 +406,7 @@ def main(argv: Sequence[str] | None = None) -> int: # pragma: no cover
392
406
  content = fd.read()
393
407
  except UnicodeDecodeError:
394
408
  continue
395
- new_content = auto_walrus(
396
- content,
397
- line_length=args.line_length,
398
- )
409
+ new_content = auto_walrus(content, config)
399
410
  if new_content is not None and content != new_content:
400
411
  sys.stdout.write(f"Rewriting {filepath}\n")
401
412
  with open(filepath, "w", encoding="utf-8") as fd:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "auto-walrus"
7
- version = "0.3.4"
7
+ version = "0.4.0"
8
8
  authors = [
9
9
  { name="Marco Gorelli", email="33491632+MarcoGorelli@users.noreply.github.com" },
10
10
  ]
@@ -80,7 +80,7 @@ filterwarnings = [
80
80
  'ignore:distutils Version classes are deprecated:DeprecationWarning',
81
81
  ]
82
82
  xfail_strict = true
83
- markers = ["config_content"]
83
+ markers = ["config_content", "project_dir_name"]
84
84
 
85
85
  [tool.coverage.run]
86
86
  plugins = ["covdefaults"]
@@ -7,6 +7,7 @@ from typing import Tuple
7
7
 
8
8
  import pytest
9
9
 
10
+ from auto_walrus import Config
10
11
  from auto_walrus import auto_walrus
11
12
  from auto_walrus import main
12
13
 
@@ -15,12 +16,12 @@ from auto_walrus import main
15
16
  ("src", "expected"),
16
17
  [
17
18
  (
18
- "def foo():\n" " a = 0\n" " if a:\n" " print(a)\n",
19
- "def foo():\n" " if (a := 0):\n" " print(a)\n",
19
+ "def foo():\n a = 0\n if a:\n print(a)\n",
20
+ "def foo():\n if (a := 0):\n print(a)\n",
20
21
  ),
21
22
  (
22
- "def foo():\n" " a = 0\n" " if a > 3:\n" " print(a)\n",
23
- "def foo():\n" " if (a := 0) > 3:\n" " print(a)\n",
23
+ "def foo():\n a = 0\n if a > 3:\n print(a)\n",
24
+ "def foo():\n if (a := 0) > 3:\n print(a)\n",
24
25
  ),
25
26
  (
26
27
  "def foo():\n"
@@ -29,11 +30,7 @@ from auto_walrus import main
29
30
  " print(a)\n"
30
31
  " else:\n"
31
32
  " pass\n",
32
- "def foo():\n"
33
- " if (a := 0):\n"
34
- " print(a)\n"
35
- " else:\n"
36
- " pass\n",
33
+ "def foo():\n if (a := 0):\n print(a)\n else:\n pass\n",
37
34
  ),
38
35
  (
39
36
  "def foo():\n"
@@ -49,24 +46,20 @@ from auto_walrus import main
49
46
  " print(a)\n",
50
47
  ),
51
48
  (
52
- "def foo():\n"
53
- " a = 0\n"
54
- " print(0)\n"
55
- " if a:\n"
56
- " print(a)\n",
57
- "def foo():\n" " print(0)\n" " if (a := 0):\n" " print(a)\n",
49
+ "def foo():\n a = 0\n print(0)\n if a:\n print(a)\n",
50
+ "def foo():\n print(0)\n if (a := 0):\n print(a)\n",
58
51
  ),
59
52
  (
60
- "def foo():\n" " a = 0\n" " if (a):\n" " print(a)\n",
61
- "def foo():\n" " if (a := 0):\n" " print(a)\n",
53
+ "def foo():\n a = 0\n if (a):\n print(a)\n",
54
+ "def foo():\n if (a := 0):\n print(a)\n",
62
55
  ),
63
56
  (
64
- "def foo():\n" " b = 0; a = 0\n" " if a:\n" " print(a)\n",
65
- "def foo():\n" " b = 0; \n" " if (a := 0):\n" " print(a)\n",
57
+ "def foo():\n b = 0; a = 0\n if a:\n print(a)\n",
58
+ "def foo():\n b = 0; \n if (a := 0):\n print(a)\n",
66
59
  ),
67
60
  (
68
- "def foo():\n" " a = 0\n" " if a:\n" " print(a)",
69
- "def foo():\n" " if (a := 0):\n" " print(a)",
61
+ "def foo():\n a = 0\n if a:\n print(a)",
62
+ "def foo():\n if (a := 0):\n print(a)",
70
63
  ),
71
64
  (
72
65
  "def foo():\n"
@@ -97,7 +90,7 @@ from auto_walrus import main
97
90
  ],
98
91
  )
99
92
  def test_rewrite(src: str, expected: str) -> None:
100
- ret = auto_walrus(src, 88)
93
+ ret = auto_walrus(src, Config(line_length=88))
101
94
  assert ret == expected
102
95
 
103
96
 
@@ -110,43 +103,54 @@ def test_rewrite(src: str, expected: str) -> None:
110
103
  " b[0] = 1\n"
111
104
  " if a:\n"
112
105
  " print(a)\n",
113
- "def foo():\n" " a = 1\n" " a = 2\n" " if a:\n" " print(a)\n",
114
- "def foo():\n" " a = (\n" " 0,)\n" " if a:\n" " print(a)\n",
115
- "def foo():\n" " a = (b==True)\n" " if a:\n" " print(a)\n",
106
+ "def foo():\n a = 1\n a = 2\n if a:\n print(a)\n",
107
+ "def foo():\n a = (\n 0,)\n if a:\n print(a)\n",
108
+ "def foo():\n a = (b==True)\n if a:\n print(a)\n",
116
109
  "def foo():\n"
117
110
  " a = thequickbrownfoxjumpsoverthelazydog\n"
118
111
  " if a:\n"
119
112
  " print(a)\n",
120
- "def foo():\n" " a = 0 # no-walrus\n" " if a:\n" " print(a)\n",
121
- "def foo():\n" " a = 0\n" " if a: # no-walrus\n" " print(a)\n",
122
- "n = 10\n" "if foo(a := n+1):\n" " print(n)\n",
123
- "a = 0\n" "if False and a:\n" " print(a)\n" "else:\n" " print(a)\n",
124
- "def foo():\n" " a = 1\n" " if a:\n" " print(a)\n" " a = 2\n",
113
+ "def foo():\n a = 0 # no-walrus\n if a:\n print(a)\n",
114
+ "def foo():\n a = 0\n if a: # no-walrus\n print(a)\n",
115
+ "n = 10\nif foo(a := n+1):\n print(n)\n",
116
+ "a = 0\nif False and a:\n print(a)\nelse:\n print(a)\n",
117
+ "def foo():\n a = 1\n if a:\n print(a)\n a = 2\n",
125
118
  "def foo():\n"
126
119
  " n = 10\n"
127
120
  " if True:\n"
128
121
  " pass\n"
129
122
  " elif foo(a := n+1):\n"
130
123
  " print(n)\n",
131
- "def foo():\n"
132
- " n = 10\n"
133
- " if n > np.sin(foo.bar.quox):\n"
134
- " print(n)\n",
135
- "def foo():\n" " n = 10\n" " if True or n > 3:\n" " print(n)\n",
124
+ "def foo():\n n = 10\n if n > np.sin(foo.bar.quox):\n print(n)\n",
125
+ "def foo():\n n = 10\n if True or n > 3:\n print(n)\n",
136
126
  ],
137
127
  )
138
128
  def test_noop(src: str) -> None:
139
- ret = auto_walrus(src, 40)
129
+ ret = auto_walrus(src, Config(line_length=40))
140
130
  assert ret is None
141
131
 
142
132
 
133
+ @pytest.mark.parametrize(
134
+ ("src", "expected"),
135
+ [
136
+ (
137
+ 'def foo(data):\n if True:\n foo = data.get("blah")\n if foo:\n return foo\n return data',
138
+ 'def foo(data):\n if True:\n if (foo := data.get("blah")):\n return foo\n return data',
139
+ ),
140
+ ],
141
+ )
142
+ def test_rewrite_unsafe(src: str, expected: str) -> None:
143
+ ret = auto_walrus(src, Config(line_length=88, unsafe=True))
144
+ assert ret == expected
145
+
146
+
143
147
  ProjectDirT = Tuple[pathlib.Path, List[pathlib.Path]]
144
148
 
145
- SRC_ORIG = "def foo():\n" " a = 0\n" " if a:\n" " print(a)\n"
146
- SRC_CHANGED = "def foo():\n" " if (a := 0):\n" " print(a)\n"
149
+ SRC_ORIG = "def foo():\n a = 0\n if a:\n print(a)\n"
150
+ SRC_CHANGED = "def foo():\n if (a := 0):\n print(a)\n"
147
151
 
148
152
 
149
- @pytest.fixture()
153
+ @pytest.fixture
150
154
  def project_dir(request: Any, tmp_path: pathlib.Path) -> ProjectDirT:
151
155
  # tmp_path will be the root of the project, e.g.:
152
156
  # tmp_path
@@ -158,6 +162,10 @@ def project_dir(request: Any, tmp_path: pathlib.Path) -> ProjectDirT:
158
162
  # | └── c.py
159
163
  # └── pyproject.toml
160
164
 
165
+ if project_dir_name := request.node.get_closest_marker("project_dir_name"):
166
+ tmp_path = tmp_path / project_dir_name.args[0]
167
+ tmp_path.mkdir(parents=True)
168
+
161
169
  if config_content := request.node.get_closest_marker("config_content"):
162
170
  (tmp_path / "pyproject.toml").write_text(config_content.args[0])
163
171
 
@@ -173,7 +181,7 @@ def project_dir(request: Any, tmp_path: pathlib.Path) -> ProjectDirT:
173
181
  return tmp_path, python_files
174
182
 
175
183
 
176
- PROJECT_CONFIG_EXCLUDE_A = "[tool.auto-walrus]\n" 'exclude = "/a"\n'
184
+ PROJECT_CONFIG_EXCLUDE_A = '[tool.auto-walrus]\nexclude = "/a"\n'
177
185
 
178
186
 
179
187
  @pytest.mark.config_content(PROJECT_CONFIG_EXCLUDE_A)
@@ -209,3 +217,18 @@ def test_config_file_missing(project_dir: ProjectDirT) -> None:
209
217
  main([str(project_root)])
210
218
  for file in files:
211
219
  assert file.read_text() == SRC_CHANGED, f"Unexpected result for {file}"
220
+
221
+
222
+ @pytest.mark.project_dir_name("build/zoop")
223
+ def test_project_in_subdirectory_that_would_be_ignored(project_dir: ProjectDirT) -> None:
224
+ project_root, files = project_dir
225
+ main([str(project_root)])
226
+ for file in files:
227
+ assert file.read_text() == SRC_CHANGED, f"Unexpected result for {file}"
228
+
229
+
230
+ def test_complains_when_no_paths(capsys: pytest.CaptureFixture[str]) -> None:
231
+ with pytest.raises(SystemExit) as ei:
232
+ main([])
233
+ assert ei.value.code == 2
234
+ assert "the following arguments are required" in capsys.readouterr().err
@@ -1,5 +1,5 @@
1
1
  [tox]
2
- envlist = py{38,39,310,311,312}
2
+ envlist = py{38,39,310,311,312,313,314}
3
3
 
4
4
  [testenv]
5
5
  deps =
File without changes
File without changes