python-devkit 0.1.1__tar.gz → 0.1.2__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.
- python_devkit-0.1.2/.gitattributes +1 -0
- {python_devkit-0.1.1 → python_devkit-0.1.2}/.github/workflows/ci.yml +3 -0
- {python_devkit-0.1.1 → python_devkit-0.1.2}/AGENTS.md +3 -0
- {python_devkit-0.1.1 → python_devkit-0.1.2}/PKG-INFO +1 -1
- {python_devkit-0.1.1 → python_devkit-0.1.2}/README.md +11 -0
- {python_devkit-0.1.1 → python_devkit-0.1.2}/pyproject.toml +3 -2
- python_devkit-0.1.2/ruff.base.toml +198 -0
- python_devkit-0.1.2/ruff.toml +1 -0
- python_devkit-0.1.2/src/python_devkit/ruff.base.toml +198 -0
- python_devkit-0.1.2/src/python_devkit/sync_ruff.py +90 -0
- python_devkit-0.1.1/ruff.toml +0 -95
- {python_devkit-0.1.1 → python_devkit-0.1.2}/.github/workflows/release.yml +0 -0
- {python_devkit-0.1.1 → python_devkit-0.1.2}/.gitignore +0 -0
- {python_devkit-0.1.1 → python_devkit-0.1.2}/.python-version +0 -0
- {python_devkit-0.1.1 → python_devkit-0.1.2}/CLAUDE.md +0 -0
- {python_devkit-0.1.1 → python_devkit-0.1.2}/LICENSE +0 -0
- {python_devkit-0.1.1 → python_devkit-0.1.2}/NOTICE +0 -0
- {python_devkit-0.1.1 → python_devkit-0.1.2}/publish-config.json +0 -0
- {python_devkit-0.1.1 → python_devkit-0.1.2}/src/python_devkit/__init__.py +0 -0
- {python_devkit-0.1.1 → python_devkit-0.1.2}/src/python_devkit/lock_python.py +0 -0
- {python_devkit-0.1.1 → python_devkit-0.1.2}/src/python_devkit/preflight_runner.py +0 -0
- {python_devkit-0.1.1 → python_devkit-0.1.2}/src/python_devkit/py.typed +0 -0
- {python_devkit-0.1.1 → python_devkit-0.1.2}/uv.lock +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruff.base.toml -text
|
|
@@ -12,9 +12,12 @@ Entry point (`[project.scripts]`): `lock-python` → `lock_python.py:app`. The c
|
|
|
12
12
|
|
|
13
13
|
`preflight_runner.py` is the labeled-check runner consumer preflights thin into: `run_checks(checks)` executes a declarative list of `CommandCheck` (label + shell command under a `ci_step` group) and `GeneratedCheck` (label + generate command + pathspec + fix command — runs the generator, then fails on any `git status --porcelain` dirt under the paths, showing the diff and the fix command). Fail-fast; each check's duration lands in the Actions step summary via `ci_step`.
|
|
14
14
|
|
|
15
|
+
`sync_ruff.py` writes the org-canonical ruff config (`src/python_devkit/ruff.base.toml`, shipped as package data) into a consuming repo's root as `ruff.base.toml`; `--check` is the drift gate (byte compare against the packaged canonical). The verb also validates the consuming repo's `ruff.toml` layer: it must set `extend = "ruff.base.toml"` and use only extend keys — plain `exclude`/`lint.select`/`lint.ignore`/`lint.per-file-ignores` in the layer replace the canonical settings under `extend` and fail the check. This repo consumes its own canonical the same way: root `ruff.base.toml` is a verb-written copy of the packaged one, root `ruff.toml` is the (currently empty) local layer.
|
|
16
|
+
|
|
15
17
|
## Constraints
|
|
16
18
|
|
|
17
19
|
- **The module surface is cross-repo Python API.** `python_devkit.lock_python.lock_python(check, root)` and `python_devkit.preflight_runner.run_checks(checks)` are contract: placeframe's preflight calls both in-process, the lock-python entry point is on CI's path, and the `[tool.python-devkit.lock-python]` table in consumer root pyprojects is its declarative config surface.
|
|
20
|
+
- **The canonical ruff body is byte-stable contract.** Every consuming repo's `ruff.base.toml` is a verbatim copy compared byte-for-byte (after newline normalization); a cosmetic edit to the canonical ripples into every consumer's diff by design. Repo-local deltas never go here — they go in the consumer's `ruff.toml` extend layer.
|
|
18
21
|
- **`group-export-dirs` is devkit-owned and `extra="forbid"`.** A typo'd key in that table fails validation loudly rather than silently dropping the redirect (the failure mode is per-group pylocks appearing in member directories).
|
|
19
22
|
- **The redirect contract assumes identical group declarations.** Dedup is first-member-wins; members declaring the same redirected group differently get whichever member enumerates first, silently.
|
|
20
23
|
- **Export commands are byte-stability load-bearing.** The exact `uv export` flag order must not churn — committed pylocks are compared byte-for-byte (after newline normalization), so a cosmetic flag change ripples into every consumer's diff.
|
|
@@ -38,6 +38,17 @@ run_checks([
|
|
|
38
38
|
|
|
39
39
|
`CommandCheck` wraps a shell command in a labeled CI step group; `GeneratedCheck` additionally guards the checked-in generator output with a git-status staleness check (diff shown, fix command named). Checks run fail-fast with per-step durations in the Actions step summary.
|
|
40
40
|
|
|
41
|
+
## ruff canonical config
|
|
42
|
+
|
|
43
|
+
`src/python_devkit/ruff.base.toml` is the org-canonical ruff configuration. Consuming repos carry it verbatim as `ruff.base.toml` at their root, with a hand-owned `ruff.toml` beside it layering repo-local deltas on top:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uvx --from python-devkit sync-ruff # write/refresh ruff.base.toml
|
|
47
|
+
uvx --from python-devkit sync-ruff --check # drift gate: exit 1 on divergence
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The consuming `ruff.toml` must set `extend = "ruff.base.toml"` and may only add via extend keys (`extend-exclude`, `lint.extend-ignore`, `lint.extend-per-file-ignores`) — plain `exclude`, `lint.select`, `lint.ignore`, and `lint.per-file-ignores` replace the canonical settings under `extend`, and `--check` fails on them. To change the canonical: edit it here, release, and run `sync-ruff` in every consuming repo.
|
|
51
|
+
|
|
41
52
|
## Development
|
|
42
53
|
|
|
43
54
|
Requires Python 3.13+ and [uv](https://docs.astral.sh/uv/).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-devkit"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "Python repo-lifecycle tooling: workspace locking, preflight checks, canonical ruff configuration"
|
|
5
5
|
requires-python = ">=3.13"
|
|
6
6
|
dependencies = [
|
|
@@ -12,6 +12,7 @@ dependencies = [
|
|
|
12
12
|
|
|
13
13
|
[project.scripts]
|
|
14
14
|
lock-python = "python_devkit.lock_python:app"
|
|
15
|
+
sync-ruff = "python_devkit.sync_ruff:app"
|
|
15
16
|
|
|
16
17
|
[dependency-groups]
|
|
17
18
|
dev = ["basedpyright>=1.39.10", "ruff>=0.14.11"]
|
|
@@ -22,7 +23,7 @@ build-backend = "hatchling.build"
|
|
|
22
23
|
|
|
23
24
|
[tool.hatch.build.targets.wheel]
|
|
24
25
|
packages = ["src/python_devkit"]
|
|
25
|
-
include = ["src/python_devkit/py.typed"]
|
|
26
|
+
include = ["src/python_devkit/py.typed", "src/python_devkit/ruff.base.toml"]
|
|
26
27
|
|
|
27
28
|
[tool.uv.sources]
|
|
28
29
|
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Org-canonical ruff configuration, hosted in python-devkit.
|
|
2
|
+
# Consuming repos carry this file verbatim as ruff.base.toml, written by the
|
|
3
|
+
# sync-ruff verb (uvx --from python-devkit sync-ruff) and drift-gated in CI.
|
|
4
|
+
# Never edit a synced copy: change this canonical, release python-devkit, and
|
|
5
|
+
# re-run sync-ruff in each consuming repo.
|
|
6
|
+
target-version = "py313"
|
|
7
|
+
line-length = 120
|
|
8
|
+
preview = true
|
|
9
|
+
required-version = ">=0.14.11"
|
|
10
|
+
|
|
11
|
+
[lint]
|
|
12
|
+
select = ["ALL"]
|
|
13
|
+
ignore = [
|
|
14
|
+
# ── Temporarily ignored (inline noqas exist for these rules but are redundant
|
|
15
|
+
# while the rules are globally ignored — RUF100 suppresses the "unused noqa" warning
|
|
16
|
+
# until we un-ignore each ticket's rules)
|
|
17
|
+
"RUF100",
|
|
18
|
+
|
|
19
|
+
# ── Permanently ignored ──────────────────────────────────────────────
|
|
20
|
+
# Type annotations — basedpyright strict mode handles this
|
|
21
|
+
"ANN",
|
|
22
|
+
# EM101 (string literal in exception) — the msg = ... indirection adds verbosity without benefit
|
|
23
|
+
"EM101",
|
|
24
|
+
# TRY003 (raise vanilla args) — requires custom exception class for every raise site;
|
|
25
|
+
# the real problem (interpolation) is caught by EM102/EM103
|
|
26
|
+
"TRY003",
|
|
27
|
+
# A003 (class attribute shadowing) — class attributes via self.x never conflict with builtins
|
|
28
|
+
"A003",
|
|
29
|
+
# Function complexity counting — C901 already covers structural complexity;
|
|
30
|
+
# these counting rules are largely redundant
|
|
31
|
+
"PLR0911",
|
|
32
|
+
"PLR0912",
|
|
33
|
+
"PLR0913",
|
|
34
|
+
"PLR0914",
|
|
35
|
+
"PLR0915",
|
|
36
|
+
"PLR0917",
|
|
37
|
+
|
|
38
|
+
# ── Temporarily ignored (will enable when conventions are established) ─
|
|
39
|
+
# Docstrings — project policy is "no docstrings" for now
|
|
40
|
+
"D",
|
|
41
|
+
# Copyright notices — not yet established
|
|
42
|
+
"CPY",
|
|
43
|
+
# Docstring formatting — blocked on D
|
|
44
|
+
"DOC",
|
|
45
|
+
# TODO/FIXME comment formatting
|
|
46
|
+
"TD",
|
|
47
|
+
"FIX",
|
|
48
|
+
|
|
49
|
+
# The "Ticket N" blocks below are the transitional cleanup slices tracked
|
|
50
|
+
# by PLE-248 (Enforce the deferred ruff rules):
|
|
51
|
+
# https://linear.app/plerionplatforms/issue/PLE-248
|
|
52
|
+
# Ticket 3 (T201, print → logging) is tracked by PLE-336:
|
|
53
|
+
# https://linear.app/plerionplatforms/issue/PLE-336
|
|
54
|
+
# Lifting a rule = remove it here in python-devkit's canonical config,
|
|
55
|
+
# release, and run sync-ruff in every consuming repo.
|
|
56
|
+
|
|
57
|
+
# ── Ticket 1: Auto-fix formatting & type modernization ───────────────
|
|
58
|
+
# Zero risk. Purely mechanical, all auto-fixable via ruff --fix.
|
|
59
|
+
"COM812", # missing trailing commas
|
|
60
|
+
"I001", # import sorting
|
|
61
|
+
"ISC003", # explicit string concatenation
|
|
62
|
+
"RUF031", # parenthesized tuple in subscript
|
|
63
|
+
"TC001", # move import into TYPE_CHECKING block
|
|
64
|
+
"TC002", # move third-party import into TYPE_CHECKING block
|
|
65
|
+
"TC003", # move stdlib import into TYPE_CHECKING block
|
|
66
|
+
"TC006", # move typing import into TYPE_CHECKING block (PEP 695)
|
|
67
|
+
"TC008", # move import into TYPE_CHECKING block (quoted annotation)
|
|
68
|
+
"UP006", # use X instead of typing.X
|
|
69
|
+
"UP007", # use X | Y instead of Union
|
|
70
|
+
"UP011", # unnecessary parentheses to functools.lru_cache
|
|
71
|
+
"UP015", # unnecessary open mode parameters
|
|
72
|
+
"UP017", # use datetime.UTC alias
|
|
73
|
+
"UP034", # extraneous parentheses
|
|
74
|
+
"UP035", # deprecated import (use collections.abc, etc.)
|
|
75
|
+
"UP040", # use PEP 695 type alias
|
|
76
|
+
"UP042", # use PEP 695 type parameters
|
|
77
|
+
"UP045", # use X | None instead of Optional
|
|
78
|
+
|
|
79
|
+
# ── Ticket 2: Auto-fix code simplifications ──────────────────────────
|
|
80
|
+
# Very low risk. Auto-fixable with trivial semantic changes.
|
|
81
|
+
"C401", # unnecessary generator — use set/dict comprehension
|
|
82
|
+
"C416", # unnecessary comprehension — use list/dict
|
|
83
|
+
"ERA001", # commented-out code
|
|
84
|
+
"FURB101", # read-text can replace open+read
|
|
85
|
+
"FURB103", # write-text can replace open+write
|
|
86
|
+
"FURB110", # use ternary instead of try/except for default
|
|
87
|
+
"FURB113", # use list.extend instead of repeated append
|
|
88
|
+
"FURB118", # use operator module instead of lambda
|
|
89
|
+
"LOG004", # use __name__ as logger name
|
|
90
|
+
"PERF401", # use list comprehension instead of manual append
|
|
91
|
+
"PIE810", # unnecessary spread of single-element starred
|
|
92
|
+
"PLC0207", # use a single __all__ definition
|
|
93
|
+
"PLC1901", # compare to empty string using falsy check
|
|
94
|
+
"PLR1711", # useless return at end of function
|
|
95
|
+
"PLR6104", # use augmented assignment
|
|
96
|
+
"PLR6201", # use a set literal for membership test
|
|
97
|
+
"PLW1510", # subprocess.run without explicit check= argument
|
|
98
|
+
"PLW1514", # open without explicit encoding
|
|
99
|
+
"PT001", # use @pytest.fixture over @pytest.fixture()
|
|
100
|
+
"PT018", # composite assertion — split into multiple asserts
|
|
101
|
+
"PTH101", # os.chmod → Path.chmod
|
|
102
|
+
"PTH111", # os.path.expanduser → Path.expanduser
|
|
103
|
+
"PTH112", # os.path.isdir → Path.is_dir
|
|
104
|
+
"PTH118", # os.path.join → Path /
|
|
105
|
+
"PTH123", # open() → Path.open()
|
|
106
|
+
"PTH201", # pathlib.Path(".")
|
|
107
|
+
"RET501", # unnecessary explicit return None
|
|
108
|
+
"RET504", # unnecessary assignment before return
|
|
109
|
+
"RET505", # unnecessary else after return
|
|
110
|
+
"RET506", # unnecessary else after raise
|
|
111
|
+
"RET507", # unnecessary else after continue
|
|
112
|
+
"RSE102", # unnecessary parentheses on raised exception
|
|
113
|
+
"RUF005", # consider spread operator instead of concatenation
|
|
114
|
+
"RUF010", # use explicit conversion flag in f-string
|
|
115
|
+
"RUF015", # prefer next() over single-element slice
|
|
116
|
+
"RUF029", # function is declared async but has no await
|
|
117
|
+
"RUF052", # local variable is assigned but never used (underscore prefix)
|
|
118
|
+
"RUF056", # unnecessary default value for keyword argument
|
|
119
|
+
"SIM102", # collapsible if statements
|
|
120
|
+
"SIM105", # use contextlib.suppress instead of try/except/pass
|
|
121
|
+
"SIM115", # use context manager for opening files
|
|
122
|
+
"SIM117", # use single with statement for multiple context managers
|
|
123
|
+
"SIM118", # use key in dict instead of key in dict.keys()
|
|
124
|
+
"TID252", # prefer absolute imports over relative
|
|
125
|
+
|
|
126
|
+
# ── Ticket 3: print → logging ────────────────────────────────────────
|
|
127
|
+
# All code should use logging, including CLI scripts.
|
|
128
|
+
"T201", # print found
|
|
129
|
+
|
|
130
|
+
# ── Ticket 4: Exception message interpolation ────────────────────────
|
|
131
|
+
# Remove f-strings from exception messages — use string literals.
|
|
132
|
+
"EM102", # f-string in exception message
|
|
133
|
+
|
|
134
|
+
# ── Ticket 5: Missing __init__.py + shebangs ─────────────────────────
|
|
135
|
+
# Add __init__.py to installable packages; remove vestigial shebangs.
|
|
136
|
+
"INP001", # implicit namespace package — add __init__.py
|
|
137
|
+
"EXE001", # shebang present but file not executable
|
|
138
|
+
|
|
139
|
+
# ── Ticket 6: Type ignore specificity ────────────────────────────────
|
|
140
|
+
# Replace blanket type: ignore with specific error codes.
|
|
141
|
+
"PGH003", # blanket type: ignore
|
|
142
|
+
|
|
143
|
+
# ── Ticket 7: Boolean trap refactoring ───────────────────────────────
|
|
144
|
+
# Change function signatures — callers must update too.
|
|
145
|
+
"FBT001", # boolean positional argument
|
|
146
|
+
"FBT002", # boolean default value in function definition
|
|
147
|
+
"FBT003", # boolean positional value in call
|
|
148
|
+
|
|
149
|
+
# ── Ticket 8: Exception handling patterns ────────────────────────────
|
|
150
|
+
# Behavioral changes to error handling.
|
|
151
|
+
"B020", # loop variable overwritten by except
|
|
152
|
+
"B903", # generator should use yield from
|
|
153
|
+
"B904", # raise without from inside except
|
|
154
|
+
"B905", # zip() without strict= parameter
|
|
155
|
+
"BLE001", # blind except Exception
|
|
156
|
+
"S101", # assert outside tests
|
|
157
|
+
"TRY002", # create custom exception class
|
|
158
|
+
"TRY300", # try/except/return — move return into else
|
|
159
|
+
|
|
160
|
+
# ── Ticket 9: Complexity, naming & refactoring ───────────────────────
|
|
161
|
+
# Structural changes requiring judgment.
|
|
162
|
+
"A001", # builtin shadowing (dir, bytes)
|
|
163
|
+
"ARG001", # unused function argument
|
|
164
|
+
"ARG002", # unused method argument
|
|
165
|
+
"C901", # function too complex
|
|
166
|
+
"E501", # line too long
|
|
167
|
+
"N802", # function name should be lowercase
|
|
168
|
+
"N806", # variable in function should be lowercase
|
|
169
|
+
"N815", # mixed-case variable in class scope
|
|
170
|
+
"N818", # exception name should end in Error
|
|
171
|
+
"PLC0415", # import not at top level
|
|
172
|
+
"PLE0704", # bare raise not inside except handler
|
|
173
|
+
"PLR1702", # too many nested blocks
|
|
174
|
+
"PLR2004", # magic value used in comparison
|
|
175
|
+
"PLR6301", # method could be function or static method
|
|
176
|
+
"PLW0603", # global statement
|
|
177
|
+
"PLW2901", # loop variable overwritten
|
|
178
|
+
"RUF001", # ambiguous unicode — replace with ASCII
|
|
179
|
+
"RUF003", # ambiguous unicode in comment
|
|
180
|
+
"S106", # possible hardcoded password
|
|
181
|
+
"S108", # probable insecure temp file/dir usage
|
|
182
|
+
"S110", # try/except/pass on broad exception
|
|
183
|
+
"S202", # use of tarfile.extractall
|
|
184
|
+
# S404/S603: prerequisite — bash.py needs env, stderr=STDOUT merge, and no-raise-with-capture
|
|
185
|
+
# before context_sha.py, deptry_check.py, test_context_sha.py, list_debug_targets.py can migrate
|
|
186
|
+
"S404", # suspicious subprocess import
|
|
187
|
+
"S603", # subprocess call with non-literal
|
|
188
|
+
"S606", # starting process without shell
|
|
189
|
+
"S607", # starting process with partial path
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
[lint.per-file-ignores]
|
|
193
|
+
# Tests use assert as the standard pytest mechanism; fixtures take arguments
|
|
194
|
+
# that pytest consumes implicitly; pytest-style test classes need no self use
|
|
195
|
+
"**/tests/**" = ["S101", "ARG001", "ARG002", "PLR6301"]
|
|
196
|
+
|
|
197
|
+
[lint.flake8-builtins]
|
|
198
|
+
ignorelist = ["id", "map"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
extend = "ruff.base.toml"
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Org-canonical ruff configuration, hosted in python-devkit.
|
|
2
|
+
# Consuming repos carry this file verbatim as ruff.base.toml, written by the
|
|
3
|
+
# sync-ruff verb (uvx --from python-devkit sync-ruff) and drift-gated in CI.
|
|
4
|
+
# Never edit a synced copy: change this canonical, release python-devkit, and
|
|
5
|
+
# re-run sync-ruff in each consuming repo.
|
|
6
|
+
target-version = "py313"
|
|
7
|
+
line-length = 120
|
|
8
|
+
preview = true
|
|
9
|
+
required-version = ">=0.14.11"
|
|
10
|
+
|
|
11
|
+
[lint]
|
|
12
|
+
select = ["ALL"]
|
|
13
|
+
ignore = [
|
|
14
|
+
# ── Temporarily ignored (inline noqas exist for these rules but are redundant
|
|
15
|
+
# while the rules are globally ignored — RUF100 suppresses the "unused noqa" warning
|
|
16
|
+
# until we un-ignore each ticket's rules)
|
|
17
|
+
"RUF100",
|
|
18
|
+
|
|
19
|
+
# ── Permanently ignored ──────────────────────────────────────────────
|
|
20
|
+
# Type annotations — basedpyright strict mode handles this
|
|
21
|
+
"ANN",
|
|
22
|
+
# EM101 (string literal in exception) — the msg = ... indirection adds verbosity without benefit
|
|
23
|
+
"EM101",
|
|
24
|
+
# TRY003 (raise vanilla args) — requires custom exception class for every raise site;
|
|
25
|
+
# the real problem (interpolation) is caught by EM102/EM103
|
|
26
|
+
"TRY003",
|
|
27
|
+
# A003 (class attribute shadowing) — class attributes via self.x never conflict with builtins
|
|
28
|
+
"A003",
|
|
29
|
+
# Function complexity counting — C901 already covers structural complexity;
|
|
30
|
+
# these counting rules are largely redundant
|
|
31
|
+
"PLR0911",
|
|
32
|
+
"PLR0912",
|
|
33
|
+
"PLR0913",
|
|
34
|
+
"PLR0914",
|
|
35
|
+
"PLR0915",
|
|
36
|
+
"PLR0917",
|
|
37
|
+
|
|
38
|
+
# ── Temporarily ignored (will enable when conventions are established) ─
|
|
39
|
+
# Docstrings — project policy is "no docstrings" for now
|
|
40
|
+
"D",
|
|
41
|
+
# Copyright notices — not yet established
|
|
42
|
+
"CPY",
|
|
43
|
+
# Docstring formatting — blocked on D
|
|
44
|
+
"DOC",
|
|
45
|
+
# TODO/FIXME comment formatting
|
|
46
|
+
"TD",
|
|
47
|
+
"FIX",
|
|
48
|
+
|
|
49
|
+
# The "Ticket N" blocks below are the transitional cleanup slices tracked
|
|
50
|
+
# by PLE-248 (Enforce the deferred ruff rules):
|
|
51
|
+
# https://linear.app/plerionplatforms/issue/PLE-248
|
|
52
|
+
# Ticket 3 (T201, print → logging) is tracked by PLE-336:
|
|
53
|
+
# https://linear.app/plerionplatforms/issue/PLE-336
|
|
54
|
+
# Lifting a rule = remove it here in python-devkit's canonical config,
|
|
55
|
+
# release, and run sync-ruff in every consuming repo.
|
|
56
|
+
|
|
57
|
+
# ── Ticket 1: Auto-fix formatting & type modernization ───────────────
|
|
58
|
+
# Zero risk. Purely mechanical, all auto-fixable via ruff --fix.
|
|
59
|
+
"COM812", # missing trailing commas
|
|
60
|
+
"I001", # import sorting
|
|
61
|
+
"ISC003", # explicit string concatenation
|
|
62
|
+
"RUF031", # parenthesized tuple in subscript
|
|
63
|
+
"TC001", # move import into TYPE_CHECKING block
|
|
64
|
+
"TC002", # move third-party import into TYPE_CHECKING block
|
|
65
|
+
"TC003", # move stdlib import into TYPE_CHECKING block
|
|
66
|
+
"TC006", # move typing import into TYPE_CHECKING block (PEP 695)
|
|
67
|
+
"TC008", # move import into TYPE_CHECKING block (quoted annotation)
|
|
68
|
+
"UP006", # use X instead of typing.X
|
|
69
|
+
"UP007", # use X | Y instead of Union
|
|
70
|
+
"UP011", # unnecessary parentheses to functools.lru_cache
|
|
71
|
+
"UP015", # unnecessary open mode parameters
|
|
72
|
+
"UP017", # use datetime.UTC alias
|
|
73
|
+
"UP034", # extraneous parentheses
|
|
74
|
+
"UP035", # deprecated import (use collections.abc, etc.)
|
|
75
|
+
"UP040", # use PEP 695 type alias
|
|
76
|
+
"UP042", # use PEP 695 type parameters
|
|
77
|
+
"UP045", # use X | None instead of Optional
|
|
78
|
+
|
|
79
|
+
# ── Ticket 2: Auto-fix code simplifications ──────────────────────────
|
|
80
|
+
# Very low risk. Auto-fixable with trivial semantic changes.
|
|
81
|
+
"C401", # unnecessary generator — use set/dict comprehension
|
|
82
|
+
"C416", # unnecessary comprehension — use list/dict
|
|
83
|
+
"ERA001", # commented-out code
|
|
84
|
+
"FURB101", # read-text can replace open+read
|
|
85
|
+
"FURB103", # write-text can replace open+write
|
|
86
|
+
"FURB110", # use ternary instead of try/except for default
|
|
87
|
+
"FURB113", # use list.extend instead of repeated append
|
|
88
|
+
"FURB118", # use operator module instead of lambda
|
|
89
|
+
"LOG004", # use __name__ as logger name
|
|
90
|
+
"PERF401", # use list comprehension instead of manual append
|
|
91
|
+
"PIE810", # unnecessary spread of single-element starred
|
|
92
|
+
"PLC0207", # use a single __all__ definition
|
|
93
|
+
"PLC1901", # compare to empty string using falsy check
|
|
94
|
+
"PLR1711", # useless return at end of function
|
|
95
|
+
"PLR6104", # use augmented assignment
|
|
96
|
+
"PLR6201", # use a set literal for membership test
|
|
97
|
+
"PLW1510", # subprocess.run without explicit check= argument
|
|
98
|
+
"PLW1514", # open without explicit encoding
|
|
99
|
+
"PT001", # use @pytest.fixture over @pytest.fixture()
|
|
100
|
+
"PT018", # composite assertion — split into multiple asserts
|
|
101
|
+
"PTH101", # os.chmod → Path.chmod
|
|
102
|
+
"PTH111", # os.path.expanduser → Path.expanduser
|
|
103
|
+
"PTH112", # os.path.isdir → Path.is_dir
|
|
104
|
+
"PTH118", # os.path.join → Path /
|
|
105
|
+
"PTH123", # open() → Path.open()
|
|
106
|
+
"PTH201", # pathlib.Path(".")
|
|
107
|
+
"RET501", # unnecessary explicit return None
|
|
108
|
+
"RET504", # unnecessary assignment before return
|
|
109
|
+
"RET505", # unnecessary else after return
|
|
110
|
+
"RET506", # unnecessary else after raise
|
|
111
|
+
"RET507", # unnecessary else after continue
|
|
112
|
+
"RSE102", # unnecessary parentheses on raised exception
|
|
113
|
+
"RUF005", # consider spread operator instead of concatenation
|
|
114
|
+
"RUF010", # use explicit conversion flag in f-string
|
|
115
|
+
"RUF015", # prefer next() over single-element slice
|
|
116
|
+
"RUF029", # function is declared async but has no await
|
|
117
|
+
"RUF052", # local variable is assigned but never used (underscore prefix)
|
|
118
|
+
"RUF056", # unnecessary default value for keyword argument
|
|
119
|
+
"SIM102", # collapsible if statements
|
|
120
|
+
"SIM105", # use contextlib.suppress instead of try/except/pass
|
|
121
|
+
"SIM115", # use context manager for opening files
|
|
122
|
+
"SIM117", # use single with statement for multiple context managers
|
|
123
|
+
"SIM118", # use key in dict instead of key in dict.keys()
|
|
124
|
+
"TID252", # prefer absolute imports over relative
|
|
125
|
+
|
|
126
|
+
# ── Ticket 3: print → logging ────────────────────────────────────────
|
|
127
|
+
# All code should use logging, including CLI scripts.
|
|
128
|
+
"T201", # print found
|
|
129
|
+
|
|
130
|
+
# ── Ticket 4: Exception message interpolation ────────────────────────
|
|
131
|
+
# Remove f-strings from exception messages — use string literals.
|
|
132
|
+
"EM102", # f-string in exception message
|
|
133
|
+
|
|
134
|
+
# ── Ticket 5: Missing __init__.py + shebangs ─────────────────────────
|
|
135
|
+
# Add __init__.py to installable packages; remove vestigial shebangs.
|
|
136
|
+
"INP001", # implicit namespace package — add __init__.py
|
|
137
|
+
"EXE001", # shebang present but file not executable
|
|
138
|
+
|
|
139
|
+
# ── Ticket 6: Type ignore specificity ────────────────────────────────
|
|
140
|
+
# Replace blanket type: ignore with specific error codes.
|
|
141
|
+
"PGH003", # blanket type: ignore
|
|
142
|
+
|
|
143
|
+
# ── Ticket 7: Boolean trap refactoring ───────────────────────────────
|
|
144
|
+
# Change function signatures — callers must update too.
|
|
145
|
+
"FBT001", # boolean positional argument
|
|
146
|
+
"FBT002", # boolean default value in function definition
|
|
147
|
+
"FBT003", # boolean positional value in call
|
|
148
|
+
|
|
149
|
+
# ── Ticket 8: Exception handling patterns ────────────────────────────
|
|
150
|
+
# Behavioral changes to error handling.
|
|
151
|
+
"B020", # loop variable overwritten by except
|
|
152
|
+
"B903", # generator should use yield from
|
|
153
|
+
"B904", # raise without from inside except
|
|
154
|
+
"B905", # zip() without strict= parameter
|
|
155
|
+
"BLE001", # blind except Exception
|
|
156
|
+
"S101", # assert outside tests
|
|
157
|
+
"TRY002", # create custom exception class
|
|
158
|
+
"TRY300", # try/except/return — move return into else
|
|
159
|
+
|
|
160
|
+
# ── Ticket 9: Complexity, naming & refactoring ───────────────────────
|
|
161
|
+
# Structural changes requiring judgment.
|
|
162
|
+
"A001", # builtin shadowing (dir, bytes)
|
|
163
|
+
"ARG001", # unused function argument
|
|
164
|
+
"ARG002", # unused method argument
|
|
165
|
+
"C901", # function too complex
|
|
166
|
+
"E501", # line too long
|
|
167
|
+
"N802", # function name should be lowercase
|
|
168
|
+
"N806", # variable in function should be lowercase
|
|
169
|
+
"N815", # mixed-case variable in class scope
|
|
170
|
+
"N818", # exception name should end in Error
|
|
171
|
+
"PLC0415", # import not at top level
|
|
172
|
+
"PLE0704", # bare raise not inside except handler
|
|
173
|
+
"PLR1702", # too many nested blocks
|
|
174
|
+
"PLR2004", # magic value used in comparison
|
|
175
|
+
"PLR6301", # method could be function or static method
|
|
176
|
+
"PLW0603", # global statement
|
|
177
|
+
"PLW2901", # loop variable overwritten
|
|
178
|
+
"RUF001", # ambiguous unicode — replace with ASCII
|
|
179
|
+
"RUF003", # ambiguous unicode in comment
|
|
180
|
+
"S106", # possible hardcoded password
|
|
181
|
+
"S108", # probable insecure temp file/dir usage
|
|
182
|
+
"S110", # try/except/pass on broad exception
|
|
183
|
+
"S202", # use of tarfile.extractall
|
|
184
|
+
# S404/S603: prerequisite — bash.py needs env, stderr=STDOUT merge, and no-raise-with-capture
|
|
185
|
+
# before context_sha.py, deptry_check.py, test_context_sha.py, list_debug_targets.py can migrate
|
|
186
|
+
"S404", # suspicious subprocess import
|
|
187
|
+
"S603", # subprocess call with non-literal
|
|
188
|
+
"S606", # starting process without shell
|
|
189
|
+
"S607", # starting process with partial path
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
[lint.per-file-ignores]
|
|
193
|
+
# Tests use assert as the standard pytest mechanism; fixtures take arguments
|
|
194
|
+
# that pytest consumes implicitly; pytest-style test classes need no self use
|
|
195
|
+
"**/tests/**" = ["S101", "ARG001", "ARG002", "PLR6301"]
|
|
196
|
+
|
|
197
|
+
[lint.flake8-builtins]
|
|
198
|
+
ignorelist = ["id", "map"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from tomllib import load
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
11
|
+
|
|
12
|
+
CANONICAL_PATH = Path(__file__).parent / "ruff.base.toml"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LayerLintConfig(BaseModel):
|
|
16
|
+
model_config = ConfigDict(extra="allow")
|
|
17
|
+
|
|
18
|
+
select: list[str] | None = None
|
|
19
|
+
ignore: list[str] | None = None
|
|
20
|
+
per_file_ignores: dict[str, list[str]] | None = Field(default=None, alias="per-file-ignores")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LayerConfig(BaseModel):
|
|
24
|
+
model_config = ConfigDict(extra="allow")
|
|
25
|
+
|
|
26
|
+
extend: str | None = None
|
|
27
|
+
exclude: list[str] | None = None
|
|
28
|
+
lint: LayerLintConfig | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command()
|
|
32
|
+
def sync_ruff(
|
|
33
|
+
check: Annotated[
|
|
34
|
+
bool, typer.Option("--check", help="Validate ruff.base.toml without writing; exit non-zero if stale.")
|
|
35
|
+
] = False,
|
|
36
|
+
root: Annotated[Path, typer.Option(help="Repository root; defaults to the current directory.")] = Path(),
|
|
37
|
+
) -> None:
|
|
38
|
+
root = root.resolve()
|
|
39
|
+
base_path = root / "ruff.base.toml"
|
|
40
|
+
canonical = CANONICAL_PATH.read_text(encoding="utf-8")
|
|
41
|
+
|
|
42
|
+
if check:
|
|
43
|
+
print("Checking ruff.base.toml...")
|
|
44
|
+
committed = base_path.read_text(encoding="utf-8") if base_path.exists() else ""
|
|
45
|
+
if _normalize_line_endings(committed) != _normalize_line_endings(canonical):
|
|
46
|
+
print(" STALE: ruff.base.toml diverges from python-devkit's canonical config.")
|
|
47
|
+
print(" Run 'uvx --from python-devkit sync-ruff' to update.")
|
|
48
|
+
raise SystemExit(1)
|
|
49
|
+
print(" OK")
|
|
50
|
+
else:
|
|
51
|
+
with base_path.open("w", encoding="utf-8", newline="\n") as file:
|
|
52
|
+
file.write(canonical)
|
|
53
|
+
print(f"Wrote {base_path}")
|
|
54
|
+
|
|
55
|
+
_check_layer(root)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _check_layer(root: Path) -> None:
|
|
59
|
+
layer_path = root / "ruff.toml"
|
|
60
|
+
if not layer_path.exists():
|
|
61
|
+
print(f"BROKEN: {layer_path} is missing; the synced base needs an extending ruff.toml beside it.")
|
|
62
|
+
raise SystemExit(1)
|
|
63
|
+
|
|
64
|
+
with layer_path.open("rb") as file:
|
|
65
|
+
layer = LayerConfig.model_validate(load(file))
|
|
66
|
+
|
|
67
|
+
if layer.extend != "ruff.base.toml":
|
|
68
|
+
print('BROKEN: ruff.toml must set extend = "ruff.base.toml".')
|
|
69
|
+
raise SystemExit(1)
|
|
70
|
+
|
|
71
|
+
offenders: list[str] = []
|
|
72
|
+
if layer.exclude is not None:
|
|
73
|
+
offenders.append("exclude -> extend-exclude")
|
|
74
|
+
if layer.lint is not None:
|
|
75
|
+
if layer.lint.select is not None:
|
|
76
|
+
offenders.append("lint.select -> lint.extend-select")
|
|
77
|
+
if layer.lint.ignore is not None:
|
|
78
|
+
offenders.append("lint.ignore -> lint.extend-ignore")
|
|
79
|
+
if layer.lint.per_file_ignores is not None:
|
|
80
|
+
offenders.append("lint.per-file-ignores -> lint.extend-per-file-ignores")
|
|
81
|
+
|
|
82
|
+
if offenders:
|
|
83
|
+
print("BROKEN: plain keys replace the canonical settings under extend; use their extend-* forms:")
|
|
84
|
+
for offender in offenders:
|
|
85
|
+
print(f" {offender}")
|
|
86
|
+
raise SystemExit(1)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _normalize_line_endings(text: str) -> str:
|
|
90
|
+
return text.replace("\r\n", "\n").replace("\r", "\n")
|
python_devkit-0.1.1/ruff.toml
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
target-version = "py313"
|
|
2
|
-
line-length = 120
|
|
3
|
-
preview = true
|
|
4
|
-
|
|
5
|
-
[lint]
|
|
6
|
-
select = ["ALL"]
|
|
7
|
-
ignore = [
|
|
8
|
-
# ── Permanent ─────────────────────────────────────────────────────────
|
|
9
|
-
# Type annotations — basedpyright strict mode handles this
|
|
10
|
-
"ANN",
|
|
11
|
-
# No docstrings, no copyright headers
|
|
12
|
-
"D",
|
|
13
|
-
"DOC",
|
|
14
|
-
"CPY",
|
|
15
|
-
# TODO/FIXME comment formatting
|
|
16
|
-
"TD",
|
|
17
|
-
"FIX",
|
|
18
|
-
# Exception message ergonomics — literal-indirection and custom-class churn without benefit
|
|
19
|
-
"raw-string-in-exception",
|
|
20
|
-
"f-string-in-exception",
|
|
21
|
-
"raise-vanilla-args",
|
|
22
|
-
# Class attribute names never conflict with builtins via self.x
|
|
23
|
-
"builtin-attribute-shadowing",
|
|
24
|
-
# Function-size counters — C901 already covers structural complexity
|
|
25
|
-
"too-many-return-statements",
|
|
26
|
-
"too-many-branches",
|
|
27
|
-
"too-many-arguments",
|
|
28
|
-
"too-many-locals",
|
|
29
|
-
"too-many-statements",
|
|
30
|
-
"too-many-positional-arguments",
|
|
31
|
-
# Typer's Option/Argument in argument defaults is typer's standard API pattern
|
|
32
|
-
"function-call-in-default-argument",
|
|
33
|
-
# Boolean parameters read fine as keyword-only flags here
|
|
34
|
-
"boolean-type-hint-positional-argument",
|
|
35
|
-
"boolean-default-value-positional-argument",
|
|
36
|
-
"boolean-positional-value-in-call",
|
|
37
|
-
# Relative imports are the intra-package convention
|
|
38
|
-
"relative-imports",
|
|
39
|
-
# Formatter-owned; enforced by `ruff format`
|
|
40
|
-
"missing-trailing-comma",
|
|
41
|
-
"unsorted-imports",
|
|
42
|
-
# Magic values in comparisons are readable in this CLI
|
|
43
|
-
"magic-value-comparison",
|
|
44
|
-
# The formatter owns wrapping; long string literals and URLs stay on one line
|
|
45
|
-
"line-too-long",
|
|
46
|
-
# src-layout tests need no __init__.py
|
|
47
|
-
"implicit-namespace-package",
|
|
48
|
-
# Runtime-used typing imports stay at module level
|
|
49
|
-
"typing-only-first-party-import",
|
|
50
|
-
"typing-only-third-party-import",
|
|
51
|
-
"typing-only-standard-library-import",
|
|
52
|
-
# Small literal tuples read fine in membership tests
|
|
53
|
-
"literal-membership",
|
|
54
|
-
|
|
55
|
-
# ── Deferred to PLE-248 — Enforce the deferred ruff rules
|
|
56
|
-
# https://linear.app/plerionplatforms/issue/PLE-248
|
|
57
|
-
# Mirrors placeframe's root ruff.toml transitional Ticket-N blocks; lifting
|
|
58
|
-
# a rule here should happen alongside lifting it from placeframe's ruff.toml.
|
|
59
|
-
"runtime-cast-value", # move import into TYPE_CHECKING block (quoted annotation) — Ticket 1
|
|
60
|
-
"redundant-open-modes", # unnecessary open mode parameters — Ticket 1
|
|
61
|
-
"read-whole-file", # read-text can replace open+read — Ticket 2
|
|
62
|
-
"write-whole-file", # write-text can replace open+write — Ticket 2
|
|
63
|
-
"repeated-append", # use list.extend instead of repeated append — Ticket 2
|
|
64
|
-
"unspecified-encoding", # open/read_text without explicit encoding — Ticket 2
|
|
65
|
-
"os-chmod", # os.chmod → Path.chmod — Ticket 2
|
|
66
|
-
"os-path-isdir", # os.path.isdir → Path.is_dir — Ticket 2
|
|
67
|
-
"os-path-join", # os.path.join → Path / — Ticket 2
|
|
68
|
-
"builtin-open", # open() → Path.open() — Ticket 2
|
|
69
|
-
"unnecessary-assign", # unnecessary assignment before return — Ticket 2
|
|
70
|
-
"superfluous-else-return", # unnecessary else after return — Ticket 2
|
|
71
|
-
"collection-literal-concatenation", # consider spread operator instead of concatenation — Ticket 2
|
|
72
|
-
"collapsible-if", # collapsible if statements — Ticket 2
|
|
73
|
-
"open-file-with-context-handler", # use context manager for opening files — Ticket 2
|
|
74
|
-
"multiple-with-statements", # use single with statement for multiple context managers — Ticket 2
|
|
75
|
-
"raise-without-from-inside-except", # raise ... from inside except — Ticket 8
|
|
76
|
-
"blind-except", # blind except Exception — Ticket 8
|
|
77
|
-
"try-consider-else", # try/except/return — move return into else — Ticket 8
|
|
78
|
-
"complex-structure", # function too complex — Ticket 9
|
|
79
|
-
"import-outside-top-level", # import not at top level — Ticket 9
|
|
80
|
-
"global-statement", # global statement — Ticket 9
|
|
81
|
-
"redefined-loop-name", # loop variable overwritten — Ticket 9
|
|
82
|
-
"path-constructor-current-directory", # do not pass "." explicitly to Path — Ticket 2
|
|
83
|
-
"bad-file-permissions", # bad-file-permissions (0o755 on executable) — Ticket 9
|
|
84
|
-
"hardcoded-temp-file", # hardcoded temp file / directory — Ticket 9
|
|
85
|
-
"suspicious-subprocess-import", # suspicious subprocess import — Ticket 9
|
|
86
|
-
"subprocess-without-shell-equals-true", # subprocess call with non-literal — Ticket 9
|
|
87
|
-
"start-process-with-partial-path", # start-process-with-partial-path — Ticket 9
|
|
88
|
-
|
|
89
|
-
# ── Deferred to PLE-336 — Replace print() with structured logging
|
|
90
|
-
# https://linear.app/plerionplatforms/issue/PLE-336
|
|
91
|
-
"print", # print found
|
|
92
|
-
]
|
|
93
|
-
|
|
94
|
-
[lint.flake8-builtins]
|
|
95
|
-
ignorelist = ["id", "map"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|