auto-walrus 0.2.2__tar.gz → 0.3.1__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.

@@ -0,0 +1,93 @@
1
+ name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI
2
+
3
+ on: push
4
+
5
+ jobs:
6
+ build:
7
+ name: Build distribution 📦
8
+ runs-on: ubuntu-latest
9
+
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - name: Set up Python
13
+ uses: actions/setup-python@v4
14
+ with:
15
+ python-version: "3.x"
16
+ - name: Install pypa/build
17
+ run: >-
18
+ python3 -m
19
+ pip install
20
+ build
21
+ --user
22
+ - name: Build a binary wheel and a source tarball
23
+ run: python3 -m build
24
+ - name: Store the distribution packages
25
+ uses: actions/upload-artifact@v3
26
+ with:
27
+ name: python-package-distributions
28
+ path: dist/
29
+
30
+ publish-to-pypi:
31
+ name: >-
32
+ Publish Python 🐍 distribution 📦 to PyPI
33
+ if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
34
+ needs:
35
+ - build
36
+ runs-on: ubuntu-latest
37
+ environment:
38
+ name: pypi
39
+ url: https://pypi.org/p/auto-walrus # Replace <package-name> with your PyPI project name
40
+ permissions:
41
+ id-token: write # IMPORTANT: mandatory for trusted publishing
42
+
43
+ steps:
44
+ - name: Download all the dists
45
+ uses: actions/download-artifact@v3
46
+ with:
47
+ name: python-package-distributions
48
+ path: dist/
49
+ - name: Publish distribution 📦 to PyPI
50
+ uses: pypa/gh-action-pypi-publish@release/v1
51
+
52
+ github-release:
53
+ name: >-
54
+ Sign the Python 🐍 distribution 📦 with Sigstore
55
+ and upload them to GitHub Release
56
+ needs:
57
+ - publish-to-pypi
58
+ runs-on: ubuntu-latest
59
+
60
+ permissions:
61
+ contents: write # IMPORTANT: mandatory for making GitHub Releases
62
+ id-token: write # IMPORTANT: mandatory for sigstore
63
+
64
+ steps:
65
+ - name: Download all the dists
66
+ uses: actions/download-artifact@v3
67
+ with:
68
+ name: python-package-distributions
69
+ path: dist/
70
+ - name: Sign the dists with Sigstore
71
+ uses: sigstore/gh-action-sigstore-python@v1.2.3
72
+ with:
73
+ inputs: >-
74
+ ./dist/*.tar.gz
75
+ ./dist/*.whl
76
+ - name: Create GitHub Release
77
+ env:
78
+ GITHUB_TOKEN: ${{ github.token }}
79
+ run: >-
80
+ gh release create
81
+ '${{ github.ref_name }}'
82
+ --repo '${{ github.repository }}'
83
+ --notes ""
84
+ - name: Upload artifact signatures to GitHub Release
85
+ env:
86
+ GITHUB_TOKEN: ${{ github.token }}
87
+ # Upload to GitHub Release using the `gh` CLI.
88
+ # `dist/` contains the built packages, and the
89
+ # sigstore-produced signatures and certificates.
90
+ run: >-
91
+ gh release upload
92
+ '${{ github.ref_name }}' dist/**
93
+ --repo '${{ github.repository }}'
@@ -0,0 +1,32 @@
1
+ name: tox
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ jobs:
9
+ tox:
10
+ strategy:
11
+ matrix:
12
+ python-version: ["3.8", "3.9", "3.10", "3.11"]
13
+ os: [windows-latest, ubuntu-latest]
14
+
15
+ runs-on: ${{ matrix.os }}
16
+ steps:
17
+ - uses: actions/checkout@v3
18
+ - uses: actions/setup-python@v4
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+ - name: Cache multiple paths
22
+ uses: actions/cache@v3
23
+ with:
24
+ path: |
25
+ ~/.cache/pip
26
+ $RUNNER_TOOL_CACHE/Python/*
27
+ ~\AppData\Local\pip\Cache
28
+ key: ${{ runner.os }}-build-${{ matrix.python-version }}
29
+ - name: install-tox
30
+ run: python -m pip install --upgrade tox virtualenv setuptools pip
31
+ - name: run-tox
32
+ run: tox -e py
@@ -0,0 +1,9 @@
1
+ .coverage
2
+ venv
3
+ *.pyc
4
+ .tox
5
+ coverage.xml
6
+ *.egg-info/
7
+ build/
8
+ dist/
9
+ todo
@@ -0,0 +1,23 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ # Ruff version.
4
+ rev: 'v0.3.5'
5
+ hooks:
6
+ # Run the formatter.
7
+ - id: ruff-format
8
+ # Run the linter.
9
+ - id: ruff
10
+ args: [--fix]
11
+ - repo: https://github.com/pre-commit/mirrors-mypy
12
+ rev: 'v1.9.0'
13
+ hooks:
14
+ - id: mypy
15
+ additional_dependencies: [pytest]
16
+ exclude: utils
17
+ - repo: https://github.com/codespell-project/codespell
18
+ rev: 'v2.2.6'
19
+ hooks:
20
+ - id: codespell
21
+ files: \.(py|rst|md)$
22
+ args: [--ignore-words-list=ser]
23
+
@@ -0,0 +1,6 @@
1
+ - id: auto-walrus
2
+ name: auto-walrus
3
+ description: Automatically use the walrus operator!
4
+ entry: auto-walrus
5
+ language: python
6
+ types: [python]
@@ -1,18 +1,16 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: auto-walrus
3
- Version: 0.2.2
3
+ Version: 0.3.1
4
4
  Summary: Automatically apply the awesome walrus operator
5
- Home-page: https://github.com/MarcoGorelli/auto-walrus
6
- Author: Marco Gorelli
7
- License: MIT
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
8
9
  Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
9
11
  Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3 :: Only
11
- Classifier: Programming Language :: Python :: Implementation :: CPython
12
- Classifier: Programming Language :: Python :: Implementation :: PyPy
13
12
  Requires-Python: >=3.8
14
13
  Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
14
 
17
15
  <h1 align="center">
18
16
  auto-walrus
@@ -83,6 +81,7 @@ To my great surprise, this is being used by:
83
81
  - https://github.com/python-graphblas/python-graphblas
84
82
  - https://github.com/Remi-Gau/bids2cite
85
83
  - https://github.com/TheAlgorithms/Python
84
+ - https://github.com/apache/superset
86
85
 
87
86
  Anyone else? Please let me know, or you can open a pull request to add yourself.
88
87
 
@@ -67,6 +67,7 @@ To my great surprise, this is being used by:
67
67
  - https://github.com/python-graphblas/python-graphblas
68
68
  - https://github.com/Remi-Gau/bids2cite
69
69
  - https://github.com/TheAlgorithms/Python
70
+ - https://github.com/apache/superset
70
71
 
71
72
  Anyone else? Please let me know, or you can open a pull request to add yourself.
72
73
 
@@ -2,17 +2,32 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import ast
5
+ import os
6
+ import pathlib
5
7
  import re
6
8
  import sys
9
+ from typing import Any
7
10
  from typing import Iterable
8
11
  from typing import Sequence
9
12
  from typing import Tuple
10
13
 
11
- SEP_SYMBOLS = frozenset(('(', ')', ',', ':'))
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(("(", ")", ",", ":"))
12
20
  # name, lineno, col_offset, end_lineno, end_col_offset
13
21
  Token = Tuple[str, int, int, int, int]
14
22
  SIMPLE_NODE = (ast.Name, ast.Constant)
15
- ENDS_WITH_COMMENT = re.compile(r'#.*$')
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
+ )
16
31
 
17
32
 
18
33
  def name_lineno_coloffset_iterable(
@@ -26,18 +41,10 @@ def name_lineno_coloffset(tokens: Token) -> tuple[str, int, int]:
26
41
 
27
42
 
28
43
  def is_simple_test(node: ast.AST) -> bool:
29
- return (
30
- isinstance(node, SIMPLE_NODE)
31
- or (
32
- isinstance(node, ast.Compare)
33
- and isinstance(node.left, SIMPLE_NODE)
34
- and (
35
- all(
36
- isinstance(_node, SIMPLE_NODE)
37
- for _node in node.comparators
38
- )
39
- )
40
- )
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))
41
48
  )
42
49
 
43
50
 
@@ -75,7 +82,9 @@ def find_names(
75
82
  if isinstance(_node, ast.Name):
76
83
  names.add(
77
84
  record_name_lineno_coloffset(
78
- _node, end_lineno, end_col_offset,
85
+ _node,
86
+ end_lineno,
87
+ end_col_offset,
79
88
  ),
80
89
  )
81
90
  return names
@@ -97,14 +106,13 @@ def process_assign(
97
106
  assignments: set[Token],
98
107
  related_vars: dict[str, list[Token]],
99
108
  ) -> None:
100
- if (
101
- len(node.targets) == 1
102
- and isinstance(node.targets[0], ast.Name)
103
- ):
109
+ if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
104
110
  target = node.targets[0]
105
111
  assignments.add(
106
112
  record_name_lineno_coloffset(
107
- target, node.end_lineno, node.end_col_offset,
113
+ target,
114
+ node.end_lineno,
115
+ node.end_col_offset,
108
116
  ),
109
117
  )
110
118
  related_vars[target.id] = list(find_names(node.value))
@@ -123,36 +131,38 @@ def is_walrussable(
123
131
  ) -> bool:
124
132
  return (
125
133
  # check name doesn't appear between assignment and if statement
126
- _assignment[0] not in [
127
- sorted_names[i][0]
128
- for i in range(assignment_idx+1, if_statement_idx)
129
- ]
134
+ _assignment[0]
135
+ not in [sorted_names[i][0] for i in range(assignment_idx + 1, if_statement_idx)]
130
136
  # check it's the variable's only assignment
131
137
  and (len(_other_assignments) == 1)
132
138
  # check this is the first usage of this name
133
139
  and (
134
140
  name_lineno_coloffset(
135
141
  _other_usages[0],
136
- ) == name_lineno_coloffset(_assignment)
142
+ )
143
+ == name_lineno_coloffset(_assignment)
137
144
  )
138
145
  # check it's used at least somewhere else
139
146
  and (len(_other_usages) > 2)
140
147
  # check it doesn't appear anywhere else
141
148
  and not [
142
- i for i in names
149
+ i
150
+ for i in names
143
151
  if (
144
- name_lineno_coloffset(i) not in
145
- name_lineno_coloffset_iterable(in_body_vars[_if_statement])
152
+ name_lineno_coloffset(i)
153
+ not in name_lineno_coloffset_iterable(in_body_vars[_if_statement])
146
154
  )
147
155
  and (
148
156
  name_lineno_coloffset(
149
157
  i,
150
- ) != name_lineno_coloffset(_assignment)
158
+ )
159
+ != name_lineno_coloffset(_assignment)
151
160
  )
152
161
  and (
153
162
  name_lineno_coloffset(
154
163
  i,
155
- ) != name_lineno_coloffset(_if_statement)
164
+ )
165
+ != name_lineno_coloffset(_if_statement)
156
166
  )
157
167
  and i[0] == _assignment[0]
158
168
  ]
@@ -171,10 +181,7 @@ def related_vars_are_unused(
171
181
  related = related_vars[name]
172
182
  should_break = False
173
183
  for rel in related:
174
- usages = [
175
- i for i in sorted_names if i[0]
176
- == rel[0] if i != rel
177
- ]
184
+ usages = [i for i in sorted_names if i[0] == rel[0] if i != rel]
178
185
  for usage in usages:
179
186
  rel_used_idx = name_lineno_coloffset_iterable(
180
187
  sorted_names,
@@ -186,7 +193,6 @@ def related_vars_are_unused(
186
193
 
187
194
  def visit_function_def(
188
195
  node: ast.FunctionDef,
189
- path: str,
190
196
  ) -> list[tuple[Token, Token]]:
191
197
  names = set()
192
198
  assignments: set[Token] = set()
@@ -224,13 +230,8 @@ def visit_function_def(
224
230
  if_statement_idx = name_lineno_coloffset_iterable(
225
231
  sorted_names,
226
232
  ).index(name_lineno_coloffset(_if_statement))
227
- _other_assignments = [
228
- i
229
- for i in sorted_assignments if i[0] == _assignment[0]
230
- ]
231
- _other_usages = [
232
- i for i in sorted_names if i[0] == _assignment[0]
233
- ]
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]]
234
235
  if is_walrussable(
235
236
  _assignment,
236
237
  _if_statement,
@@ -241,19 +242,21 @@ def visit_function_def(
241
242
  _other_usages,
242
243
  names,
243
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,
244
251
  ):
245
- if 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))
252
+ walrus.append((_assignment, _if_statement))
253
253
  return walrus
254
254
 
255
255
 
256
- def auto_walrus(content: str, path: str, line_length: int) -> str | None:
256
+ def auto_walrus(
257
+ content: str,
258
+ line_length: int,
259
+ ) -> str | None:
257
260
  lines = content.splitlines()
258
261
  try:
259
262
  tree = ast.parse(content)
@@ -263,7 +266,7 @@ def auto_walrus(content: str, path: str, line_length: int) -> str | None:
263
266
  walruses = []
264
267
  for node in ast.walk(tree):
265
268
  if isinstance(node, ast.FunctionDef):
266
- walruses.extend(visit_function_def(node, path))
269
+ walruses.extend(visit_function_def(node))
267
270
  lines_to_remove = []
268
271
  walruses = sorted(walruses, key=lambda x: (-x[1][1], -x[1][2]))
269
272
 
@@ -273,75 +276,133 @@ def auto_walrus(content: str, path: str, line_length: int) -> str | None:
273
276
  for _assignment, _if_statement in walruses:
274
277
  if _assignment[1] != _assignment[3]:
275
278
  continue
276
- txt = lines[_assignment[1]-1][_assignment[2]:_assignment[4]]
277
- if txt.count('=') > 1:
279
+ txt = lines[_assignment[1] - 1][_assignment[2] : _assignment[4]]
280
+ if txt.count("=") > 1:
278
281
  continue
279
- line = lines[_if_statement[1]-1]
280
- left_bit = line[:_if_statement[2]]
281
- right_bit = line[_if_statement[4]:]
282
+ line = lines[_if_statement[1] - 1]
283
+ left_bit = line[: _if_statement[2]]
284
+ right_bit = line[_if_statement[4] :]
282
285
  no_paren = any(left_bit.endswith(i) for i in SEP_SYMBOLS) and any(
283
286
  right_bit.startswith(i) for i in SEP_SYMBOLS
284
287
  )
285
- replace = txt.replace('=', ':=')
288
+ replace = txt.replace("=", ":=")
286
289
  if no_paren:
287
290
  line_with_walrus = left_bit + replace + right_bit
288
291
  else:
289
- line_with_walrus = left_bit + '(' + replace + ')' + right_bit
292
+ line_with_walrus = left_bit + "(" + replace + ")" + right_bit
290
293
  if len(line_with_walrus) > line_length:
291
294
  # don't rewrite if it would split over multiple lines
292
295
  continue
293
296
  # replace assignment
294
297
  line_without_assignment = (
295
- f'{lines[_assignment[1]-1][:_assignment[2]]}'
296
- f'{lines[_assignment[1]-1][_assignment[4]:]}'
298
+ f"{lines[_assignment[1]-1][:_assignment[2]]}"
299
+ f"{lines[_assignment[1]-1][_assignment[4]:]}"
297
300
  )
298
- if (
299
- ENDS_WITH_COMMENT.search(lines[_assignment[1]-1]) is not None
300
- ) or (
301
- ENDS_WITH_COMMENT.search(lines[_if_statement[1]-1]) is not None
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
302
303
  ):
303
304
  continue
304
305
  lines[_assignment[1] - 1] = line_without_assignment
305
306
  # add walrus
306
- lines[_if_statement[1]-1] = line_with_walrus
307
+ lines[_if_statement[1] - 1] = line_with_walrus
307
308
  # remove empty line
308
- if not lines[_assignment[1]-1].strip():
309
- lines_to_remove.append(_assignment[1]-1)
309
+ if not lines[_assignment[1] - 1].strip():
310
+ lines_to_remove.append(_assignment[1] - 1)
310
311
 
311
312
  newlines = [
312
- line for i, line in enumerate(
313
+ line
314
+ for i, line in enumerate(
313
315
  lines,
314
- ) if i not in lines_to_remove
316
+ )
317
+ if i not in lines_to_remove
315
318
  ]
316
- newcontent = '\n'.join(newlines)
317
- if newcontent and content.endswith('\n'):
318
- newcontent += '\n'
319
+ newcontent = "\n".join(newlines)
320
+ if newcontent and content.endswith("\n"):
321
+ newcontent += "\n"
319
322
  if newcontent != content:
320
323
  return newcontent
321
324
  return None
322
325
 
323
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
+
324
349
  def main(argv: Sequence[str] | None = None) -> int: # pragma: no cover
325
350
  parser = argparse.ArgumentParser()
326
- parser.add_argument('paths', nargs='*')
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
+ )
327
364
  # black formatter's default
328
- parser.add_argument('--line-length', type=int, default=88)
365
+ parser.add_argument("--line-length", type=int, default=88)
329
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
+
330
374
  ret = 0
331
- for path in args.paths:
332
- try:
333
- with open(path, encoding='utf-8') as fd:
334
- content = fd.read()
335
- except UnicodeDecodeError:
336
- continue
337
- new_content = auto_walrus(content, path, line_length=args.line_length)
338
- if new_content is not None and content != new_content:
339
- sys.stdout.write(f'Rewriting {path}\n')
340
- with open(path, 'w', encoding='utf-8') as fd:
341
- fd.write(new_content)
342
- ret = 1
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
343
404
  return ret
344
405
 
345
406
 
346
- if __name__ == '__main__':
407
+ if __name__ == "__main__":
347
408
  sys.exit(main())
@@ -0,0 +1,90 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "auto-walrus"
7
+ version = "0.3.1"
8
+ authors = [
9
+ { name="Marco Gorelli", email="33491632+MarcoGorelli@users.noreply.github.com" },
10
+ ]
11
+ description = "Automatically apply the awesome walrus operator"
12
+ readme = "README.md"
13
+ requires-python = ">=3.8"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+
20
+ [project.urls]
21
+ "Homepage" = "https://github.com/MarcoGorelli/auto-walrus"
22
+ "Bug Tracker" = "https://github.com/MarcoGorelli/auto-walrus"
23
+
24
+ [tool.ruff]
25
+ line-length = 90
26
+ fix = true
27
+ target-version = "py38"
28
+
29
+ lint.select = [
30
+ "ALL",
31
+ ]
32
+ lint.ignore = [
33
+ 'A001',
34
+ 'A003',
35
+ 'ANN101',
36
+ 'ANN401',
37
+ 'ARG002', # todo: enable
38
+ 'ARG003', # todo: enable
39
+ 'C901',
40
+ 'COM812',
41
+ 'D',
42
+ 'DTZ',
43
+ 'E501',
44
+ 'EM101', # todo: enable
45
+ 'ERA001', # todo: enable
46
+ 'FBT003', # todo: enable
47
+ 'FIX',
48
+ 'ICN001',
49
+ 'ISC001',
50
+ 'PD',
51
+ 'PLR0911',
52
+ 'PLR0912',
53
+ 'PLR5501',
54
+ 'PLR2004',
55
+ 'PT011',
56
+ 'PTH',
57
+ 'RET505',
58
+ 'S',
59
+ 'SLF001',
60
+ 'TD',
61
+ 'TRY003', # todo: enable
62
+ 'TRY004'
63
+ ]
64
+
65
+ [tool.ruff.lint.isort]
66
+ force-single-line = true
67
+
68
+ [tool.black]
69
+ line-length = 90
70
+
71
+ [tool.pytest.ini_options]
72
+ filterwarnings = [
73
+ "error",
74
+ 'ignore:distutils Version classes are deprecated:DeprecationWarning',
75
+ ]
76
+ xfail_strict = true
77
+ markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]
78
+
79
+ [tool.coverage.run]
80
+ plugins = ["covdefaults"]
81
+
82
+ [tool.coverage.report]
83
+ exclude_also = [
84
+ "> POLARS_VERSION",
85
+ "if sys.version_info() <",
86
+ ]
87
+
88
+ [tool.mypy]
89
+ strict = true
90
+
@@ -0,0 +1,4 @@
1
+ covdefaults
2
+ pytest
3
+ pytest-cov
4
+ pytest-randomly
File without changes
@@ -0,0 +1,212 @@
1
+ from __future__ import annotations
2
+
3
+ import pathlib
4
+ from typing import Any
5
+ from typing import List
6
+ from typing import Tuple
7
+
8
+ import pytest
9
+
10
+ from auto_walrus import auto_walrus
11
+ from auto_walrus import main
12
+
13
+
14
+ @pytest.mark.parametrize(
15
+ ("src", "expected"),
16
+ [
17
+ (
18
+ "def foo():\n" " a = 0\n" " if a:\n" " print(a)\n",
19
+ "def foo():\n" " if (a := 0):\n" " print(a)\n",
20
+ ),
21
+ (
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",
24
+ ),
25
+ (
26
+ "def foo():\n"
27
+ " a = 0\n"
28
+ " if a:\n"
29
+ " print(a)\n"
30
+ " else:\n"
31
+ " pass\n",
32
+ "def foo():\n"
33
+ " if (a := 0):\n"
34
+ " print(a)\n"
35
+ " else:\n"
36
+ " pass\n",
37
+ ),
38
+ (
39
+ "def foo():\n"
40
+ " a = 0\n"
41
+ " if True:\n"
42
+ " print(1)\n"
43
+ " elif a:\n"
44
+ " print(a)\n",
45
+ "def foo():\n"
46
+ " if True:\n"
47
+ " print(1)\n"
48
+ " elif (a := 0):\n"
49
+ " print(a)\n",
50
+ ),
51
+ (
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",
58
+ ),
59
+ (
60
+ "def foo():\n" " a = 0\n" " if (a):\n" " print(a)\n",
61
+ "def foo():\n" " if (a := 0):\n" " print(a)\n",
62
+ ),
63
+ (
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",
66
+ ),
67
+ (
68
+ "def foo():\n" " a = 0\n" " if a:\n" " print(a)",
69
+ "def foo():\n" " if (a := 0):\n" " print(a)",
70
+ ),
71
+ (
72
+ "def foo():\n"
73
+ " a = 0\n"
74
+ " if a:\n"
75
+ " print(a)\n"
76
+ " if (b := 3) > 0:\n"
77
+ " print(b)\n",
78
+ "def foo():\n"
79
+ " if (a := 0):\n"
80
+ " print(a)\n"
81
+ " if (b := 3) > 0:\n"
82
+ " print(b)\n",
83
+ ),
84
+ (
85
+ "def foo():\n"
86
+ " a = 0\n"
87
+ " if np.sin(b) + np.cos(b) < np.tan(b):\n"
88
+ " pass\n"
89
+ " elif a:\n"
90
+ " print(a)\n",
91
+ "def foo():\n"
92
+ " if np.sin(b) + np.cos(b) < np.tan(b):\n"
93
+ " pass\n"
94
+ " elif (a := 0):\n"
95
+ " print(a)\n",
96
+ ),
97
+ ],
98
+ )
99
+ def test_rewrite(src: str, expected: str) -> None:
100
+ ret = auto_walrus(src, 88)
101
+ assert ret == expected
102
+
103
+
104
+ @pytest.mark.parametrize(
105
+ "src",
106
+ [
107
+ "def foo():\n"
108
+ " b = [0]\n"
109
+ " a = b[0]\n"
110
+ " b[0] = 1\n"
111
+ " if a:\n"
112
+ " 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",
116
+ "def foo():\n"
117
+ " a = thequickbrownfoxjumpsoverthelazydog\n"
118
+ " if a:\n"
119
+ " 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",
125
+ "def foo():\n"
126
+ " n = 10\n"
127
+ " if True:\n"
128
+ " pass\n"
129
+ " elif foo(a := n+1):\n"
130
+ " 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",
136
+ ],
137
+ )
138
+ def test_noop(src: str) -> None:
139
+ ret = auto_walrus(src, 40)
140
+ assert ret is None
141
+
142
+
143
+ ProjectDirT = Tuple[pathlib.Path, List[pathlib.Path]]
144
+
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"
147
+
148
+
149
+ @pytest.fixture()
150
+ def project_dir(request: Any, tmp_path: pathlib.Path) -> ProjectDirT:
151
+ # tmp_path will be the root of the project, e.g.:
152
+ # tmp_path
153
+ # ├── submodule1/
154
+ # | ├── submodule2/
155
+ # | | └── a.py
156
+ # | └── b.py
157
+ # ├── submodule3/
158
+ # | └── c.py
159
+ # └── pyproject.toml
160
+
161
+ config_content = request.node.get_closest_marker("config_content")
162
+ if config_content:
163
+ (tmp_path / "pyproject.toml").write_text(config_content.args[0])
164
+
165
+ python_files = [
166
+ tmp_path / "submodule1" / "submodule2" / "a.py",
167
+ tmp_path / "submodule1" / "b.py",
168
+ tmp_path / "submodule3" / "c.py",
169
+ ]
170
+ for file_path in python_files:
171
+ file_path.parent.mkdir(parents=True, exist_ok=True)
172
+ file_path.write_text(SRC_ORIG)
173
+
174
+ return tmp_path, python_files
175
+
176
+
177
+ PROJECT_CONFIG_EXCLUDE_A = "[tool.auto-walrus]\n" 'exclude = "/a"\n'
178
+
179
+
180
+ @pytest.mark.config_content(PROJECT_CONFIG_EXCLUDE_A)
181
+ def test_config_file_respected(project_dir: ProjectDirT) -> None:
182
+ project_root, files = project_dir
183
+ main([str(project_root)])
184
+ for file in files:
185
+ expected = SRC_ORIG if file.name == "a.py" else SRC_CHANGED
186
+ assert file.read_text() == expected, f"Unexpected result for {file}"
187
+
188
+
189
+ @pytest.mark.config_content(PROJECT_CONFIG_EXCLUDE_A)
190
+ def test_config_file_overridden_by_cmdline(
191
+ project_dir: ProjectDirT,
192
+ ) -> None:
193
+ project_root, files = project_dir
194
+ main(["--exclude", "/b", str(project_root)])
195
+ for file in files:
196
+ expected = SRC_ORIG if file.name == "b.py" else SRC_CHANGED
197
+ assert file.read_text() == expected, f"Unexpected result for {file}"
198
+
199
+
200
+ @pytest.mark.config_content("\n")
201
+ def test_config_file_no_auto_walrus(project_dir: ProjectDirT) -> None:
202
+ project_root, files = project_dir
203
+ main([str(project_root)])
204
+ for file in files:
205
+ assert file.read_text() == SRC_CHANGED, f"Unexpected result for {file}"
206
+
207
+
208
+ def test_config_file_missing(project_dir: ProjectDirT) -> None:
209
+ project_root, files = project_dir
210
+ main([str(project_root)])
211
+ for file in files:
212
+ assert file.read_text() == SRC_CHANGED, f"Unexpected result for {file}"
@@ -0,0 +1,11 @@
1
+ [tox]
2
+ envlist = py{38,39,310,311}
3
+
4
+ [testenv]
5
+ deps =
6
+ -rrequirements-dev.txt
7
+ commands =
8
+ coverage erase
9
+ coverage run -m pytest {posargs:tests -vv -W error}
10
+ coverage xml
11
+ coverage report --show-missing
@@ -0,0 +1,25 @@
1
+ # mypy: ignore
2
+ # ruff: noqa
3
+ import re
4
+ import subprocess
5
+ import sys
6
+
7
+ how = sys.argv[1]
8
+
9
+ with open("pyproject.toml", encoding="utf-8") as f:
10
+ content = f.read()
11
+ old_version = re.search(r'version = "(.*)"', content).group(1)
12
+ version = old_version.split(".")
13
+ if how == "patch":
14
+ version = ".".join(version[:-1] + [str(int(version[-1]) + 1)])
15
+ elif how == "minor":
16
+ version = ".".join(version[:-2] + [str(int(version[-2]) + 1), "0"])
17
+ elif how == "major":
18
+ version = ".".join([str(int(version[0]) + 1), "0", "0"])
19
+ content = content.replace(f'version = "{old_version}"', f'version = "{version}"')
20
+ with open("pyproject.toml", "w", encoding="utf-8") as f:
21
+ f.write(content)
22
+
23
+ subprocess.run(["git", "commit", "-a", "-m", f"Bump version to {version}"])
24
+ subprocess.run(["git", "tag", "-a", version, "-m", version])
25
+ subprocess.run(["git", "push", "--follow-tags"])
@@ -1,109 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: auto_walrus
3
- Version: 0.2.2
4
- Summary: Automatically apply the awesome walrus operator
5
- Home-page: https://github.com/MarcoGorelli/auto-walrus
6
- Author: Marco Gorelli
7
- License: MIT
8
- Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3 :: Only
11
- Classifier: Programming Language :: Python :: Implementation :: CPython
12
- Classifier: Programming Language :: Python :: Implementation :: PyPy
13
- Requires-Python: >=3.8
14
- Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
-
17
- <h1 align="center">
18
- auto-walrus
19
- </h1>
20
-
21
- <p align="center">
22
- <img width="458" alt="auto-walrus" src="https://user-images.githubusercontent.com/33491632/195613331-f7442140-09da-4376-90aa-2ac4aaa242fa.png">
23
- </p>
24
-
25
- auto-walrus
26
- ===========
27
- [![Build Status](https://github.com/MarcoGorelli/auto-walrus/workflows/tox/badge.svg)](https://github.com/MarcoGorelli/auto-walrus/actions?workflow=tox)
28
- [![Coverage](https://codecov.io/gh/MarcoGorelli/auto-walrus/branch/main/graph/badge.svg)](https://codecov.io/gh/MarcoGorelli/auto-walrus)
29
- [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/MarcoGorelli/auto-walrus/main.svg)](https://results.pre-commit.ci/latest/github/MarcoGorelli/auto-walrus/main)
30
-
31
-
32
- A tool and pre-commit hook to automatically apply the awesome walrus operator.
33
-
34
-
35
- ## Installation
36
-
37
- ```console
38
- pip install auto-walrus
39
- ```
40
-
41
- ## Usage as a pre-commit hook
42
-
43
- See [pre-commit](https://github.com/pre-commit/pre-commit) for instructions
44
-
45
- Sample `.pre-commit-config.yaml`:
46
-
47
- ```yaml
48
- - repo: https://github.com/MarcoGorelli/auto-walrus
49
- rev: v0.2.2
50
- hooks:
51
- - id: auto-walrus
52
- ```
53
-
54
- ## Command-line example
55
-
56
- ```console
57
- auto-walrus myfile.py
58
- ```
59
-
60
- ```diff
61
- - n = 10
62
- - if n > 3:
63
- + if (n := 10) > 3:
64
- print(n)
65
- ```
66
-
67
- ## Configuration
68
-
69
- Using the walrus operator can result in longer lines. Lines longer than what you
70
- pass to ``--line-length`` won't be rewritten to use walrus operators.
71
-
72
- E.g.
73
- ```
74
- auto-walrus myfile_1.py myfile_2.py --line-length 89
75
- ```
76
-
77
- Lines with comments won't be rewritten.
78
-
79
- ## Used by
80
-
81
- To my great surprise, this is being used by:
82
-
83
- - https://github.com/python-graphblas/python-graphblas
84
- - https://github.com/Remi-Gau/bids2cite
85
- - https://github.com/TheAlgorithms/Python
86
-
87
- Anyone else? Please let me know, or you can open a pull request to add yourself.
88
-
89
- ## Testimonials
90
-
91
- **Christopher Redwine**, [Senior Software Engineer at TechnologyAdvice](https://github.com/chrisRedwine)
92
-
93
- > hmm, i dunno about this one chief
94
-
95
- **Michael Kennedy & Brian Okken**, [hosts of the Python Bytes podcast](https://pythonbytes.fm/):
96
-
97
- > I kind of like this being separate from other tools
98
-
99
- **Someone on Discord**
100
-
101
- > you're a monster
102
-
103
- **Will McGugan**, [CEO / Founder of http://Textualize.io](https://www.willmcgugan.com/):
104
-
105
- > Embrace the Walrus!
106
-
107
- ## Credits
108
-
109
- Logo by [lion_space](https://www.fiverr.com/lion_space)
@@ -1,10 +0,0 @@
1
- LICENSE
2
- README.md
3
- auto_walrus.py
4
- setup.cfg
5
- setup.py
6
- auto_walrus.egg-info/PKG-INFO
7
- auto_walrus.egg-info/SOURCES.txt
8
- auto_walrus.egg-info/dependency_links.txt
9
- auto_walrus.egg-info/entry_points.txt
10
- auto_walrus.egg-info/top_level.txt
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- auto-walrus = auto_walrus:main
@@ -1 +0,0 @@
1
- auto_walrus
@@ -1,48 +0,0 @@
1
- [metadata]
2
- name = auto_walrus
3
- version = 0.2.2
4
- description = Automatically apply the awesome walrus operator
5
- long_description = file: README.md
6
- long_description_content_type = text/markdown
7
- url = https://github.com/MarcoGorelli/auto-walrus
8
- author = Marco Gorelli
9
- license = MIT
10
- license_file = LICENSE
11
- classifiers =
12
- License :: OSI Approved :: MIT License
13
- Programming Language :: Python :: 3
14
- Programming Language :: Python :: 3 :: Only
15
- Programming Language :: Python :: Implementation :: CPython
16
- Programming Language :: Python :: Implementation :: PyPy
17
-
18
- [options]
19
- py_modules = auto_walrus
20
- python_requires = >=3.8
21
-
22
- [options.entry_points]
23
- console_scripts =
24
- auto-walrus = auto_walrus:main
25
-
26
- [bdist_wheel]
27
- universal = True
28
-
29
- [coverage:run]
30
- plugins = covdefaults
31
-
32
- [mypy]
33
- check_untyped_defs = true
34
- disallow_any_generics = true
35
- disallow_incomplete_defs = true
36
- disallow_untyped_defs = true
37
- no_implicit_optional = true
38
-
39
- [mypy-testing.*]
40
- disallow_untyped_defs = false
41
-
42
- [mypy-tests.*]
43
- disallow_untyped_defs = false
44
-
45
- [egg_info]
46
- tag_build =
47
- tag_date = 0
48
-
@@ -1,2 +0,0 @@
1
- from setuptools import setup
2
- setup()
File without changes