python-constricter 0.3.1__tar.gz → 0.3.3__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_constricter-0.3.1 → python_constricter-0.3.3}/.gitignore +27 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/PKG-INFO +18 -8
- {python_constricter-0.3.1 → python_constricter-0.3.3}/README.md +17 -7
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/__init__.py +1 -1
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/command.py +55 -15
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/config.py +66 -12
- python_constricter-0.3.3/constricter/cli/edits.py +188 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/explain.py +2 -1
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/hints.py +122 -29
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/options.py +88 -5
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/protocol.py +6 -1
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/report.py +1 -1
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/runs.py +1 -1
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/schedule.py +41 -9
- python_constricter-0.3.3/constricter/cli/traced.py +289 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/workers.py +2 -2
- python_constricter-0.3.3/constricter/fix/core/__init__.py +2 -0
- python_constricter-0.3.3/constricter/fix/core/asked.py +56 -0
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/core}/fixes.py +38 -22
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/core}/imports.py +153 -11
- python_constricter-0.3.3/constricter/fix/core/inherited.py +210 -0
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/core}/known.py +235 -28
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/core}/signatures.py +14 -9
- python_constricter-0.3.3/constricter/fix/index/__init__.py +2 -0
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/atoms.py +11 -8
- python_constricter-0.3.3/constricter/fix/index/beyond.py +90 -0
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/callers.py +4 -4
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/classnames.py +5 -5
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/declared.py +17 -1
- python_constricter-0.3.3/constricter/fix/index/decorated.py +119 -0
- python_constricter-0.3.3/constricter/fix/index/fixtures.py +176 -0
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/installed.py +36 -15
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/modules.py +196 -12
- python_constricter-0.3.3/constricter/fix/index/offers.py +135 -0
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/order.py +6 -3
- python_constricter-0.3.3/constricter/fix/index/own_overloads.py +57 -0
- python_constricter-0.3.3/constricter/fix/index/plain.py +123 -0
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/project.py +339 -94
- python_constricter-0.3.3/constricter/fix/index/read.py +70 -0
- python_constricter-0.3.3/constricter/fix/index/sides.py +65 -0
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/index}/stubbed.py +58 -87
- python_constricter-0.3.3/constricter/fix/index/tuples.py +86 -0
- python_constricter-0.3.3/constricter/fix/libraries/__init__.py +2 -0
- python_constricter-0.3.3/constricter/fix/libraries/library.py +286 -0
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/libraries}/opened.py +35 -3
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/libraries}/overloads.py +128 -13
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/libraries}/stdlib.py +303 -18
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/attributes.json +23 -0
- python_constricter-0.3.3/constricter/fix/tables/awaited.json +69 -0
- python_constricter-0.3.3/constricter/fix/tables/awaited_overloads.json +14 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/classes.json +2 -1
- python_constricter-0.3.3/constricter/fix/tables/elements.json +156 -0
- python_constricter-0.3.3/constricter/fix/tables/functions.json +43 -0
- python_constricter-0.3.3/constricter/fix/tables/generic_attributes.json +252 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/method_overloads.json +165 -65
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/method_signatures.json +230 -19
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/methods.json +184 -88
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/overloads.json +70 -33
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/returns.json +34 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/scalars.json +3 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/source.json +5 -1
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/subscriptable.json +1 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/type_parameters.json +2 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/variables.json +21 -0
- python_constricter-0.3.3/constricter/fix/values/__init__.py +2 -0
- python_constricter-0.3.3/constricter/fix/values/aliased.py +182 -0
- python_constricter-0.3.3/constricter/fix/values/called.py +108 -0
- python_constricter-0.3.3/constricter/fix/values/classvars.py +438 -0
- python_constricter-0.3.3/constricter/fix/values/decided.py +376 -0
- python_constricter-0.3.3/constricter/fix/values/displays.py +87 -0
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/doubts.py +222 -27
- python_constricter-0.3.3/constricter/fix/values/entered.py +173 -0
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/fills.py +93 -34
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/guesses.py +104 -44
- python_constricter-0.3.3/constricter/fix/values/hinted.py +645 -0
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/inference.py +377 -180
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/members.py +129 -18
- python_constricter-0.3.3/constricter/fix/values/operated.py +199 -0
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/returned.py +202 -33
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/returns.py +2 -1
- python_constricter-0.3.3/constricter/fix/values/shapes.py +394 -0
- python_constricter-0.3.3/constricter/fix/values/targets.py +356 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/notebook.py +1 -1
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/offences.py +30 -3
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/plugins/flake8.py +11 -1
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/plugins/pylint.py +11 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/annotations.py +319 -76
- python_constricter-0.3.3/constricter/rules/binding.py +454 -0
- python_constricter-0.3.3/constricter/rules/calls.py +409 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/checker.py +256 -116
- python_constricter-0.3.3/constricter/rules/decorators.py +221 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/flow.py +18 -3
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/late.py +61 -26
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/parsed.py +44 -5
- python_constricter-0.3.3/constricter/rules/quoted.py +88 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/rebinding.py +5 -3
- python_constricter-0.3.3/constricter/rules/recorded.py +163 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/scope.py +276 -87
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/syntax.py +123 -5
- python_constricter-0.3.3/constricter/rules/tables.py +155 -0
- python_constricter-0.3.3/constricter/trace.py +385 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/pyproject.toml +2 -2
- python_constricter-0.3.3/stdlib_tables/elements.py +77 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/generate.py +238 -27
- {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/overloads.py +123 -18
- {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/partial.json +25 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/reading.py +78 -10
- {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/stubs.py +11 -4
- {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/templates.py +42 -7
- python_constricter-0.3.1/constricter/fix/hinted.py +0 -242
- python_constricter-0.3.1/constricter/fix/library.py +0 -164
- python_constricter-0.3.1/constricter/fix/tables/generic_attributes.json +0 -55
- python_constricter-0.3.1/constricter/fix/targets.py +0 -184
- python_constricter-0.3.1/constricter/rules/binding.py +0 -251
- python_constricter-0.3.1/constricter/rules/calls.py +0 -211
- {python_constricter-0.3.1 → python_constricter-0.3.3}/LICENSE.md +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/__main__.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/__init__.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/baseline.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/collecting.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/guard.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/cli/paths.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/__init__.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/aliases.json +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/bases.json +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/fix/tables/scalar_members.json +0 -0
- {python_constricter-0.3.1/constricter/fix → python_constricter-0.3.3/constricter/fix/values}/narrowed.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/jsonc.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/noqa.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/plugins/__init__.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/py.typed +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/__init__.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/narrowing.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/redundant.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/constricter/rules/walked.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/hatch_build.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/__init__.py +0 -0
- {python_constricter-0.3.1 → python_constricter-0.3.3}/stdlib_tables/__main__.py +0 -0
|
@@ -17,5 +17,32 @@ stdlib_tables/partial.json
|
|
|
17
17
|
# Claude Code's agent worktrees: other checkouts of this repository.
|
|
18
18
|
.claude/worktrees/
|
|
19
19
|
|
|
20
|
+
# Claude Code's sandbox bind-mounts /dev/null over these denied paths while a
|
|
21
|
+
# session runs; none is a file of this repo
|
|
22
|
+
/.bash_profile
|
|
23
|
+
/.bashrc
|
|
24
|
+
/.gitconfig
|
|
25
|
+
/.gitmodules
|
|
26
|
+
/.idea
|
|
27
|
+
/.mcp.json
|
|
28
|
+
/.profile
|
|
29
|
+
/.ripgreprc
|
|
30
|
+
/.zprofile
|
|
31
|
+
/.zshrc
|
|
32
|
+
/.claude/agents
|
|
33
|
+
/.claude/commands
|
|
34
|
+
/.claude/hooks
|
|
35
|
+
/.claude/launch.json
|
|
36
|
+
/.claude/loop.md
|
|
37
|
+
/.claude/output-styles
|
|
38
|
+
/.claude/routines
|
|
39
|
+
/.claude/scheduled_tasks.json
|
|
40
|
+
/.claude/settings.json
|
|
41
|
+
/.claude/settings.local.json
|
|
42
|
+
/.claude/skills
|
|
43
|
+
/.claude/workflows
|
|
44
|
+
/.claude/.cc-writes
|
|
45
|
+
/.vscode
|
|
46
|
+
|
|
20
47
|
# Backups merge tools leave next to a conflicted file.
|
|
21
48
|
*.orig
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-constricter
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Import-Name: constricter
|
|
5
5
|
Summary: Lint rules: every local variable is typed where it's first bound. A flake8 plugin, a pylint plugin and a standalone CLI.
|
|
6
6
|
Author: Ivy Duggan
|
|
@@ -97,7 +97,7 @@ class bodies too. Statements are read in source order, and only a name's first b
|
|
|
97
97
|
| `LVA002` | an untyped `for` target or `match` capture | `name: T` first (or a type comment) |
|
|
98
98
|
| `LVA003` | a `for` target typed only by `# type: T` | `name: T` first |
|
|
99
99
|
| `LVA004` | with `all-scopes`: the same as `LVA001`, in a module or class body | `name: T = ...` (`ClassVar[T]` in a dataclass) |
|
|
100
|
-
| `LVA005` | an annotation with `Any`, `object` or a generic without its parameters
|
|
100
|
+
| `LVA005` | an annotation with `Any`, `object` or a generic without its parameters, past `vague` | name the real type |
|
|
101
101
|
| `LVA006` | an annotation nested `nesting` deep (3 by default) | a `type` alias for a part of it |
|
|
102
102
|
| `LVA007` | a name annotated again with the type it already has, in the same block | drop the second annotation |
|
|
103
103
|
| `LVA008` | with every value the name ever holds known: an annotation that could narrow to them | narrow it (`total: int`) |
|
|
@@ -109,7 +109,10 @@ class bodies too. Statements are read in source order, and only a name's first b
|
|
|
109
109
|
Exempt: comprehensions, `except ... as`, imports, `def`/`class`, `type` aliases, parameters,
|
|
110
110
|
`global`/`nonlocal`, and `_`; in module and class bodies, dunder names (`__all__`, `__slots__`) and
|
|
111
111
|
enum members (a base imported from `enum`, however it's aliased, or else whose name ends in `Enum`
|
|
112
|
-
or `Flag`).
|
|
112
|
+
or `Flag`). And what makes a type, not a value, which an annotation would turn into a variable to a
|
|
113
|
+
type checker: `T = TypeVar("T")`, a `ParamSpec`, a `TypeVarTuple`, a `NewType`, and a functional
|
|
114
|
+
`NamedTuple`, `TypedDict`, `Enum` or `collections.namedtuple`, however `typing`,
|
|
115
|
+
`typing_extensions`, `enum` or `collections` is imported.
|
|
113
116
|
|
|
114
117
|
A `# type:` comment (`x = 1 # type: int`, `with f() as x: # type: T`) counts as an annotation with
|
|
115
118
|
`type-comments`, or automatically in a module written to run on Python 2: one that imports
|
|
@@ -174,16 +177,20 @@ Options:
|
|
|
174
177
|
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------- | --------------------------------- |
|
|
175
178
|
| level | `--level` | `level` | `--constricter-level` | `constricter-level` |
|
|
176
179
|
| strictest | `--max`: `--level=suffocate` for every path, `--all-scopes`, and the opt-in `LVA012` | — | — | — |
|
|
177
|
-
| every fix | `--max-fix`: `--max`, `--fix --unsafe-fixes`, and `--infer-with` each of basedpyright and
|
|
180
|
+
| every fix | `--max-fix`: `--max`, `--fix --unsafe-fixes`, and `--infer-with` each of basedpyright, ty and pyrefly that's installed and runs | — | — | — |
|
|
178
181
|
| type comments | `--type-comments` | `type-comments` | `--constricter-type-comments` | `constricter-type-comments = yes` |
|
|
179
182
|
| all scopes | `--all-scopes` | `all-scopes` | `--constricter-all-scopes` | `constricter-all-scopes = yes` |
|
|
180
183
|
| nesting | `--nesting N` | `nesting` | `--constricter-nesting` | `constricter-nesting` |
|
|
181
184
|
| max length | `--max-length N` (LVA011) | `max-length` | `--constricter-max-length` | `constricter-max-length` |
|
|
185
|
+
| vague | `--vague LEVEL` (LVA005, and the fixes offered): -1 none (the default), 0 one `Any` inside a type that says the rest (`tuple[str, Any]`), N from 1 N + 1, or one alone | `vague` | `--constricter-vague` | `constricter-vague` |
|
|
186
|
+
| min python | `--min-python VERSION` (the oldest Python the code runs on, whose syntax `--fix` writes; default: `requires-python`'s) | `min-python` | - | - |
|
|
182
187
|
| type hierarchy | - | `narrower` (a table) | `--constricter-narrower` | `constricter-narrower` |
|
|
183
188
|
| fix | `--fix` (`--unsafe-fixes` for guesses), `--diff` to preview | - | - | - |
|
|
184
|
-
| infer with | `--infer-with CHECKERS` (`basedpyright`, `ty`,
|
|
189
|
+
| infer with | `--infer-with CHECKERS` (`basedpyright`, `ty`, `pyrefly`, several: inferred types, as guesses) | `infer-with` | - | - |
|
|
190
|
+
| infer from | `--infer-from FILE` (the types `python -m constricter.trace -m pytest` recorded from a run, as guesses) | `infer-from` | - | - |
|
|
185
191
|
| show fixes | `--show-fixes` (each fix and how it was decided, text) | - | - | - |
|
|
186
192
|
| fix levels | `--fix-select`, `--fix-ignore`, `--unsafe-fix-select` (mechanisms: [docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md#fix-levels)) | `fix-select`, `fix-ignore`, `unsafe-fix-select` | - | - |
|
|
193
|
+
| fix plain bases | `--fix-plain-bases BASES` (frameworks' classes whose subclasses' bodies `--fix` annotates; django's are built in) | `fix-plain-bases` | - | - |
|
|
187
194
|
| select | `--select CODES` (codes or prefixes) | `select` | flake8's own `select` | pylint's own `enable` |
|
|
188
195
|
| extend select | `--extend-select CODES` (also report these; an opt-in code by its full code) | `extend-select` | flake8's own `extend-select` | pylint's own `enable` |
|
|
189
196
|
| ignore | `--ignore CODES` | `ignore` | flake8's own `extend-ignore` | pylint's own `disable` |
|
|
@@ -212,6 +219,8 @@ type-comments = false
|
|
|
212
219
|
all-scopes = true
|
|
213
220
|
nesting = 3
|
|
214
221
|
max-length = 4
|
|
222
|
+
vague = -1 # the default: no `Any`; 0 lets `dict[str, Any]` through, 1 `Any` itself
|
|
223
|
+
min-python = "3.11" # the default: your `requires-python`'s lower bound
|
|
215
224
|
jobs = 0
|
|
216
225
|
baseline = "constricter-baseline.json" # the default; relative to this pyproject.toml
|
|
217
226
|
|
|
@@ -237,8 +246,9 @@ return type or whose `return`s agree (in another checked file too), a copy, subs
|
|
|
237
246
|
method call of a local whose type is known, and values computed from those; a loop's target or an
|
|
238
247
|
unpacking's names get a declaration on the line before. `--unsafe-fixes` adds guesses, and
|
|
239
248
|
`--show-fixes` lists each fix and how its value decided it. `--infer-with basedpyright` (or `ty`, or
|
|
240
|
-
`basedpyright,ty
|
|
241
|
-
as guesses
|
|
249
|
+
`pyrefly`, or several: `basedpyright,ty`; each installed alongside) asks those type checkers for
|
|
250
|
+
what `--fix` can't type itself, as guesses; `--infer-from FILE` takes the types a run bound each
|
|
251
|
+
name to, recorded by `python -m constricter.trace -m pytest`, the same way. The full list is in
|
|
242
252
|
[docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md).
|
|
243
253
|
|
|
244
254
|
### Installing and running
|
|
@@ -264,7 +274,7 @@ pre-commit, after ruff's hooks (or `constricter-fix`, which runs `--fix` first):
|
|
|
264
274
|
|
|
265
275
|
```yaml
|
|
266
276
|
- repo: https://github.com/ivylikethevine/python-constricter
|
|
267
|
-
rev: v0.3.
|
|
277
|
+
rev: v0.3.3
|
|
268
278
|
hooks:
|
|
269
279
|
- id: constricter
|
|
270
280
|
```
|
|
@@ -70,7 +70,7 @@ class bodies too. Statements are read in source order, and only a name's first b
|
|
|
70
70
|
| `LVA002` | an untyped `for` target or `match` capture | `name: T` first (or a type comment) |
|
|
71
71
|
| `LVA003` | a `for` target typed only by `# type: T` | `name: T` first |
|
|
72
72
|
| `LVA004` | with `all-scopes`: the same as `LVA001`, in a module or class body | `name: T = ...` (`ClassVar[T]` in a dataclass) |
|
|
73
|
-
| `LVA005` | an annotation with `Any`, `object` or a generic without its parameters
|
|
73
|
+
| `LVA005` | an annotation with `Any`, `object` or a generic without its parameters, past `vague` | name the real type |
|
|
74
74
|
| `LVA006` | an annotation nested `nesting` deep (3 by default) | a `type` alias for a part of it |
|
|
75
75
|
| `LVA007` | a name annotated again with the type it already has, in the same block | drop the second annotation |
|
|
76
76
|
| `LVA008` | with every value the name ever holds known: an annotation that could narrow to them | narrow it (`total: int`) |
|
|
@@ -82,7 +82,10 @@ class bodies too. Statements are read in source order, and only a name's first b
|
|
|
82
82
|
Exempt: comprehensions, `except ... as`, imports, `def`/`class`, `type` aliases, parameters,
|
|
83
83
|
`global`/`nonlocal`, and `_`; in module and class bodies, dunder names (`__all__`, `__slots__`) and
|
|
84
84
|
enum members (a base imported from `enum`, however it's aliased, or else whose name ends in `Enum`
|
|
85
|
-
or `Flag`).
|
|
85
|
+
or `Flag`). And what makes a type, not a value, which an annotation would turn into a variable to a
|
|
86
|
+
type checker: `T = TypeVar("T")`, a `ParamSpec`, a `TypeVarTuple`, a `NewType`, and a functional
|
|
87
|
+
`NamedTuple`, `TypedDict`, `Enum` or `collections.namedtuple`, however `typing`,
|
|
88
|
+
`typing_extensions`, `enum` or `collections` is imported.
|
|
86
89
|
|
|
87
90
|
A `# type:` comment (`x = 1 # type: int`, `with f() as x: # type: T`) counts as an annotation with
|
|
88
91
|
`type-comments`, or automatically in a module written to run on Python 2: one that imports
|
|
@@ -147,16 +150,20 @@ Options:
|
|
|
147
150
|
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------- | --------------------------------- |
|
|
148
151
|
| level | `--level` | `level` | `--constricter-level` | `constricter-level` |
|
|
149
152
|
| strictest | `--max`: `--level=suffocate` for every path, `--all-scopes`, and the opt-in `LVA012` | — | — | — |
|
|
150
|
-
| every fix | `--max-fix`: `--max`, `--fix --unsafe-fixes`, and `--infer-with` each of basedpyright and
|
|
153
|
+
| every fix | `--max-fix`: `--max`, `--fix --unsafe-fixes`, and `--infer-with` each of basedpyright, ty and pyrefly that's installed and runs | — | — | — |
|
|
151
154
|
| type comments | `--type-comments` | `type-comments` | `--constricter-type-comments` | `constricter-type-comments = yes` |
|
|
152
155
|
| all scopes | `--all-scopes` | `all-scopes` | `--constricter-all-scopes` | `constricter-all-scopes = yes` |
|
|
153
156
|
| nesting | `--nesting N` | `nesting` | `--constricter-nesting` | `constricter-nesting` |
|
|
154
157
|
| max length | `--max-length N` (LVA011) | `max-length` | `--constricter-max-length` | `constricter-max-length` |
|
|
158
|
+
| vague | `--vague LEVEL` (LVA005, and the fixes offered): -1 none (the default), 0 one `Any` inside a type that says the rest (`tuple[str, Any]`), N from 1 N + 1, or one alone | `vague` | `--constricter-vague` | `constricter-vague` |
|
|
159
|
+
| min python | `--min-python VERSION` (the oldest Python the code runs on, whose syntax `--fix` writes; default: `requires-python`'s) | `min-python` | - | - |
|
|
155
160
|
| type hierarchy | - | `narrower` (a table) | `--constricter-narrower` | `constricter-narrower` |
|
|
156
161
|
| fix | `--fix` (`--unsafe-fixes` for guesses), `--diff` to preview | - | - | - |
|
|
157
|
-
| infer with | `--infer-with CHECKERS` (`basedpyright`, `ty`,
|
|
162
|
+
| infer with | `--infer-with CHECKERS` (`basedpyright`, `ty`, `pyrefly`, several: inferred types, as guesses) | `infer-with` | - | - |
|
|
163
|
+
| infer from | `--infer-from FILE` (the types `python -m constricter.trace -m pytest` recorded from a run, as guesses) | `infer-from` | - | - |
|
|
158
164
|
| show fixes | `--show-fixes` (each fix and how it was decided, text) | - | - | - |
|
|
159
165
|
| fix levels | `--fix-select`, `--fix-ignore`, `--unsafe-fix-select` (mechanisms: [docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md#fix-levels)) | `fix-select`, `fix-ignore`, `unsafe-fix-select` | - | - |
|
|
166
|
+
| fix plain bases | `--fix-plain-bases BASES` (frameworks' classes whose subclasses' bodies `--fix` annotates; django's are built in) | `fix-plain-bases` | - | - |
|
|
160
167
|
| select | `--select CODES` (codes or prefixes) | `select` | flake8's own `select` | pylint's own `enable` |
|
|
161
168
|
| extend select | `--extend-select CODES` (also report these; an opt-in code by its full code) | `extend-select` | flake8's own `extend-select` | pylint's own `enable` |
|
|
162
169
|
| ignore | `--ignore CODES` | `ignore` | flake8's own `extend-ignore` | pylint's own `disable` |
|
|
@@ -185,6 +192,8 @@ type-comments = false
|
|
|
185
192
|
all-scopes = true
|
|
186
193
|
nesting = 3
|
|
187
194
|
max-length = 4
|
|
195
|
+
vague = -1 # the default: no `Any`; 0 lets `dict[str, Any]` through, 1 `Any` itself
|
|
196
|
+
min-python = "3.11" # the default: your `requires-python`'s lower bound
|
|
188
197
|
jobs = 0
|
|
189
198
|
baseline = "constricter-baseline.json" # the default; relative to this pyproject.toml
|
|
190
199
|
|
|
@@ -210,8 +219,9 @@ return type or whose `return`s agree (in another checked file too), a copy, subs
|
|
|
210
219
|
method call of a local whose type is known, and values computed from those; a loop's target or an
|
|
211
220
|
unpacking's names get a declaration on the line before. `--unsafe-fixes` adds guesses, and
|
|
212
221
|
`--show-fixes` lists each fix and how its value decided it. `--infer-with basedpyright` (or `ty`, or
|
|
213
|
-
`basedpyright,ty
|
|
214
|
-
as guesses
|
|
222
|
+
`pyrefly`, or several: `basedpyright,ty`; each installed alongside) asks those type checkers for
|
|
223
|
+
what `--fix` can't type itself, as guesses; `--infer-from FILE` takes the types a run bound each
|
|
224
|
+
name to, recorded by `python -m constricter.trace -m pytest`, the same way. The full list is in
|
|
215
225
|
[docs/FIXES.md](https://github.com/ivylikethevine/python-constricter/blob/main/docs/FIXES.md).
|
|
216
226
|
|
|
217
227
|
### Installing and running
|
|
@@ -237,7 +247,7 @@ pre-commit, after ruff's hooks (or `constricter-fix`, which runs `--fix` first):
|
|
|
237
247
|
|
|
238
248
|
```yaml
|
|
239
249
|
- repo: https://github.com/ivylikethevine/python-constricter
|
|
240
|
-
rev: v0.3.
|
|
250
|
+
rev: v0.3.3
|
|
241
251
|
hooks:
|
|
242
252
|
- id: constricter
|
|
243
253
|
```
|
|
@@ -16,15 +16,16 @@ from pathlib import Path
|
|
|
16
16
|
from typing import TYPE_CHECKING, Final, NamedTuple, TextIO, TypeAlias, cast
|
|
17
17
|
|
|
18
18
|
from constricter import notebook
|
|
19
|
-
from constricter.cli import baseline, collecting, schedule
|
|
19
|
+
from constricter.cli import baseline, collecting, schedule, traced
|
|
20
20
|
from constricter.cli.options import Mode, Options, Output
|
|
21
21
|
from constricter.cli.paths import STDIN, python_files
|
|
22
22
|
from constricter.cli.protocol import HintError
|
|
23
23
|
from constricter.cli.report import Format, Result, fix_reasons, render, statistics
|
|
24
24
|
from constricter.cli.runs import BaselineRun, CheckRun, CoverageRun, FileRun
|
|
25
25
|
from constricter.cli.workers import Workers
|
|
26
|
-
from constricter.fix import
|
|
27
|
-
from constricter.fix.known import Callee, Hints, Outside, Returns
|
|
26
|
+
from constricter.fix.core import fixes
|
|
27
|
+
from constricter.fix.core.known import Callee, Hints, Outside, Returns
|
|
28
|
+
from constricter.fix.index import callers, decorated, installed, plain, project
|
|
28
29
|
from constricter.noqa import lines, unsuppressed
|
|
29
30
|
from constricter.offences import (
|
|
30
31
|
DEFAULT_CHECKS,
|
|
@@ -32,6 +33,7 @@ from constricter.offences import (
|
|
|
32
33
|
Edit,
|
|
33
34
|
Offence,
|
|
34
35
|
)
|
|
36
|
+
from constricter.rules import parsed
|
|
35
37
|
from constricter.rules.checker import Checked, Coverage, annotation_coverage, checked_source
|
|
36
38
|
|
|
37
39
|
if TYPE_CHECKING: # slow to import, and only needed for `--infer-with`
|
|
@@ -296,13 +298,17 @@ def _handled(path: Path, text: tuple[str, Path], offences: list[Offence], option
|
|
|
296
298
|
left: list[Result] = [r for r in results if r.offence not in fixing]
|
|
297
299
|
if path == STDIN: # the fixed source goes to stdout
|
|
298
300
|
return CheckRun(left, baselined, len(fixing), _fixed(raw, name, fixing).text)
|
|
301
|
+
fixed: int
|
|
299
302
|
try:
|
|
300
|
-
|
|
303
|
+
fixed = fix_file(path, fixing)
|
|
301
304
|
except UnicodeEncodeError as failure: # the file is left as it was, its offences unfixed
|
|
302
305
|
# Its canonical name: PyPy reports `latin1` where CPython says `latin-1`.
|
|
303
306
|
encoding: str = codecs.lookup(failure.encoding).name
|
|
304
307
|
message: str = f"an annotation can't be written in its encoding, {encoding}; left as it was"
|
|
305
308
|
return CheckRun(results, baselined, error=f"{name}: error: {message}")
|
|
309
|
+
if fixed:
|
|
310
|
+
_ = parsed.take(raw) # the tree of the text it had: no check will ask for it again
|
|
311
|
+
return CheckRun(left, baselined, fixed)
|
|
306
312
|
|
|
307
313
|
|
|
308
314
|
def _baseline_path(path: Path, outside: Outside, options: Options) -> BaselineRun:
|
|
@@ -397,6 +403,11 @@ def _check_all(options: Options) -> tuple[list[Path], list[FileRun]]:
|
|
|
397
403
|
for index, run in zip(again, redone, strict=True):
|
|
398
404
|
runs[index] = _merged(runs[index], run)
|
|
399
405
|
again = [index for index, run in zip(again, redone, strict=True) if cast("CheckRun", run).fixed]
|
|
406
|
+
checker: str
|
|
407
|
+
path: Path
|
|
408
|
+
for checker, path in session.abandoned:
|
|
409
|
+
name: Path = options.input.name(path)
|
|
410
|
+
_ = sys.stderr.write(f"constricter: warning: {checker} hung on {name} twice: no hints for it\n")
|
|
400
411
|
return names, runs
|
|
401
412
|
|
|
402
413
|
|
|
@@ -430,16 +441,40 @@ def _called_again(
|
|
|
430
441
|
if not (again := [at for at, name in enumerate(names) if name in found and names.count(name) == 1]):
|
|
431
442
|
return runs, modules
|
|
432
443
|
calling: list[int] = _calling(runs, found, again)
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
at
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
444
|
+
before: project.Index = callers.with_parameters(modules, found)
|
|
445
|
+
modules = _checked_more(paths, runs, again, (check, options, session), before)
|
|
446
|
+
# A caller is checked again only if a function it imports now returns something else.
|
|
447
|
+
calling = [
|
|
448
|
+
at
|
|
449
|
+
for at in calling
|
|
450
|
+
if project.returned(modules, paths[at], {}) != project.returned(before, paths[at], {})
|
|
451
|
+
]
|
|
452
|
+
return runs, _checked_more(paths, runs, calling, (check, options, session), modules)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _checked_more(
|
|
456
|
+
paths: Sequence[Path],
|
|
457
|
+
runs: list[FileRun],
|
|
458
|
+
which: Sequence[int],
|
|
459
|
+
how: "tuple[Callable[[Path, Outside], FileRun], Options, hints.Session | None]",
|
|
460
|
+
modules: project.Index,
|
|
461
|
+
) -> project.Index:
|
|
462
|
+
"""Check the files of `paths` at `which` again, knowing `modules`, into `runs`.
|
|
463
|
+
|
|
464
|
+
`how`: the check, the options and the type checkers' session. With `--fix` a file's rounds are
|
|
465
|
+
joined; otherwise the later one stands.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
The index, with what their functions now return.
|
|
469
|
+
|
|
470
|
+
"""
|
|
471
|
+
redone: list[FileRun]
|
|
472
|
+
redone, modules = _checked_all([paths[at] for at in which], how[0], how[1], how[2], modules)
|
|
473
|
+
at: int
|
|
474
|
+
run: FileRun
|
|
475
|
+
for at, run in zip(which, redone, strict=True):
|
|
476
|
+
runs[at] = _merged(runs[at], run) if how[1].mode is Mode.FIX else run
|
|
477
|
+
return modules
|
|
443
478
|
|
|
444
479
|
|
|
445
480
|
def _calling(
|
|
@@ -475,6 +510,8 @@ def _checked_all(
|
|
|
475
510
|
|
|
476
511
|
"""
|
|
477
512
|
hinted: dict[Path, tuple[Hints, ...]] = {} if session is None else session.hints(_texts(paths))
|
|
513
|
+
if options.input.trace is not None:
|
|
514
|
+
hinted = traced.merged(hinted, traced.load(options.input.trace), paths)
|
|
478
515
|
coverage: bool = options.mode is Mode.COVERAGE # needs nothing from the other files
|
|
479
516
|
stack: contextlib.ExitStack
|
|
480
517
|
with contextlib.ExitStack() as stack:
|
|
@@ -487,7 +524,10 @@ def _checked_all(
|
|
|
487
524
|
modules = project.Index({}, []) if coverage else project.index(paths)
|
|
488
525
|
collecting.indexed()
|
|
489
526
|
if not coverage:
|
|
490
|
-
modules =
|
|
527
|
+
modules = plain.settled(
|
|
528
|
+
decorated.passed(installed.with_installed(modules, installed.search_path())),
|
|
529
|
+
options.checks.plain_bases,
|
|
530
|
+
)
|
|
491
531
|
return schedule.checked(
|
|
492
532
|
paths,
|
|
493
533
|
check,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
|
2
2
|
"""The command's defaults, from the nearest `pyproject.toml`'s `[tool.constricter]` table."""
|
|
3
3
|
|
|
4
|
+
import re
|
|
4
5
|
import tomllib
|
|
5
6
|
from collections.abc import Callable, Sequence
|
|
6
7
|
from functools import partial
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING, Final, TypeAlias
|
|
9
|
+
from typing import TYPE_CHECKING, Final, TypeAlias, cast
|
|
9
10
|
|
|
10
11
|
from constricter.cli.protocol import SERVERS
|
|
11
12
|
from constricter.jsonc import is_int
|
|
@@ -30,6 +31,10 @@ def unknown_codes(codes: Sequence[str]) -> list[str]:
|
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
DEFAULT_BASELINE: Final = "constricter-baseline.json"
|
|
34
|
+
_MIN_PYTHON: Final = "min_python"
|
|
35
|
+
_VERSION: Final = re.compile(r"\d+\.\d+") # `min-python`'s: `3.11`
|
|
36
|
+
# A lower bound among `requires-python`'s specifiers: `>=3.11`, `~=3.11`, `==3.11.*`, `>3.10`.
|
|
37
|
+
_LOWER_BOUND: Final = re.compile(r"(?:>=|~=|==|>)\s*(\d+)(?:\.(\d+))?")
|
|
33
38
|
|
|
34
39
|
|
|
35
40
|
def project_root(start: Path) -> Path:
|
|
@@ -58,36 +63,65 @@ def _pyproject(start: Path) -> Path | None:
|
|
|
58
63
|
return None
|
|
59
64
|
|
|
60
65
|
|
|
61
|
-
def
|
|
62
|
-
"""
|
|
66
|
+
def _document(path: Path) -> dict[str, _Toml]:
|
|
67
|
+
"""Read `path`, a `pyproject.toml`.
|
|
63
68
|
|
|
64
69
|
Returns:
|
|
65
|
-
|
|
70
|
+
Its tables, as TOML parsed them.
|
|
66
71
|
|
|
67
72
|
Raises:
|
|
68
|
-
ValueError: The file isn't TOML
|
|
73
|
+
ValueError: The file isn't TOML.
|
|
69
74
|
|
|
70
75
|
"""
|
|
71
76
|
file: BufferedReader
|
|
72
|
-
document: dict[str, _Toml]
|
|
73
|
-
message: str
|
|
74
77
|
with path.open("rb") as file:
|
|
75
78
|
try:
|
|
76
|
-
|
|
79
|
+
return tomllib.load(file)
|
|
77
80
|
except tomllib.TOMLDecodeError as error:
|
|
78
|
-
message = f"{path}: {error}"
|
|
81
|
+
message: str = f"{path}: {error}"
|
|
79
82
|
raise ValueError(message) from error
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _table(path: Path, document: dict[str, _Toml]) -> dict[str, _Toml]:
|
|
86
|
+
"""Return the `[tool.constricter]` table of `document` (`path`'s), or an empty one.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The table's keys and values, as TOML parsed them.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValueError: `tool.constricter` isn't a table.
|
|
93
|
+
|
|
94
|
+
"""
|
|
80
95
|
table: dict[str, _Toml]
|
|
81
96
|
match document.get("tool"):
|
|
82
97
|
case {"constricter": dict() as table}:
|
|
83
98
|
return table
|
|
84
99
|
case {"constricter": _}:
|
|
85
|
-
message = f"{path}: [tool.constricter] isn't a table"
|
|
100
|
+
message: str = f"{path}: [tool.constricter] isn't a table"
|
|
86
101
|
raise ValueError(message)
|
|
87
102
|
case _:
|
|
88
103
|
return {}
|
|
89
104
|
|
|
90
105
|
|
|
106
|
+
def _required(document: dict[str, _Toml]) -> str | None:
|
|
107
|
+
"""Read the oldest Python a project's `requires-python` allows: its highest lower bound.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
It (`3.11`, for `>=3.11,<4`), or `None` if the project doesn't say.
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
required: str
|
|
114
|
+
match document.get("project"):
|
|
115
|
+
case {"requires-python": str() as required}:
|
|
116
|
+
pass
|
|
117
|
+
case _:
|
|
118
|
+
return None
|
|
119
|
+
found: list[tuple[str, str]] = cast("list[tuple[str, str]]", _LOWER_BOUND.findall(required))
|
|
120
|
+
bounds: list[tuple[int, int]] = [(int(major), int(minor or 0)) for major, minor in found]
|
|
121
|
+
oldest: tuple[int, int] | None = max(bounds, default=None)
|
|
122
|
+
return None if oldest is None else f"{oldest[0]}.{oldest[1]}"
|
|
123
|
+
|
|
124
|
+
|
|
91
125
|
def _level(value: _Toml) -> str | None:
|
|
92
126
|
"""Read a level's name or number as the options take it.
|
|
93
127
|
|
|
@@ -135,6 +169,16 @@ def _flag(value: _Toml) -> bool | None:
|
|
|
135
169
|
return value if isinstance(value, bool) else None
|
|
136
170
|
|
|
137
171
|
|
|
172
|
+
def _version(value: _Toml) -> str | None:
|
|
173
|
+
"""Read `min-python`: a Python version, as text (`"3.11"`).
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
It, or `None` for anything else.
|
|
177
|
+
|
|
178
|
+
"""
|
|
179
|
+
return value if isinstance(value, str) and _VERSION.fullmatch(value) else None
|
|
180
|
+
|
|
181
|
+
|
|
138
182
|
def _gigabytes(value: _Toml) -> float | None:
|
|
139
183
|
"""Read `infer-memory`: a positive number of gigabytes.
|
|
140
184
|
|
|
@@ -179,7 +223,7 @@ def _lists(value: _Toml, read: Callable[[_Toml], list[str] | None]) -> dict[str,
|
|
|
179
223
|
|
|
180
224
|
|
|
181
225
|
def _baseline(value: _Toml, root: Path) -> str | None:
|
|
182
|
-
"""Read a baseline path, relative to `root` (the pyproject.toml that names it).
|
|
226
|
+
"""Read a baseline's or a trace's path, relative to `root` (the pyproject.toml that names it).
|
|
183
227
|
|
|
184
228
|
Returns:
|
|
185
229
|
The resolved path, or `None` if `value` isn't a non-empty string.
|
|
@@ -193,6 +237,7 @@ _READERS: dict[str, Callable[[_Toml], Default | None]] = {
|
|
|
193
237
|
"level": _level,
|
|
194
238
|
"nesting": partial(_whole, minimum=1),
|
|
195
239
|
"max-length": partial(_whole, minimum=1),
|
|
240
|
+
"vague": partial(_whole, minimum=-1),
|
|
196
241
|
"jobs": partial(_whole, minimum=0),
|
|
197
242
|
"exclude": _strings,
|
|
198
243
|
"select": _codes,
|
|
@@ -201,6 +246,7 @@ _READERS: dict[str, Callable[[_Toml], Default | None]] = {
|
|
|
201
246
|
"fix-select": _fix_kinds,
|
|
202
247
|
"fix-ignore": _fix_kinds,
|
|
203
248
|
"unsafe-fix-select": _fix_kinds,
|
|
249
|
+
"fix-plain-bases": _strings,
|
|
204
250
|
"type-comments": _flag,
|
|
205
251
|
"all-scopes": _flag,
|
|
206
252
|
"per-path-levels": _levels,
|
|
@@ -208,12 +254,15 @@ _READERS: dict[str, Callable[[_Toml], Default | None]] = {
|
|
|
208
254
|
"narrower": partial(_lists, read=_strings), # a type hierarchy: each type, and those it's narrower than
|
|
209
255
|
"infer-with": _checkers,
|
|
210
256
|
"infer-memory": _gigabytes,
|
|
257
|
+
"min-python": _version,
|
|
211
258
|
}
|
|
212
259
|
|
|
213
260
|
|
|
214
261
|
def config_defaults(start: Path) -> dict[str, Default]:
|
|
215
262
|
"""Return the option defaults in the nearest `pyproject.toml`'s `[tool.constricter]`.
|
|
216
263
|
|
|
264
|
+
And `min-python`'s from the project's `requires-python`, where the table doesn't set it.
|
|
265
|
+
|
|
217
266
|
Returns:
|
|
218
267
|
Each option's default, keyed by its `argparse` name (`type_comments`, not `type-comments`).
|
|
219
268
|
|
|
@@ -227,14 +276,19 @@ def config_defaults(start: Path) -> dict[str, Default]:
|
|
|
227
276
|
readers: dict[str, Callable[[_Toml], Default | None]] = {
|
|
228
277
|
**_READERS,
|
|
229
278
|
"baseline": partial(_baseline, root=path.parent),
|
|
279
|
+
"infer-from": partial(_baseline, root=path.parent),
|
|
230
280
|
}
|
|
231
281
|
defaults: dict[str, Default] = {}
|
|
232
282
|
key: str
|
|
233
283
|
value: _Toml
|
|
234
|
-
|
|
284
|
+
document: dict[str, _Toml] = _document(path)
|
|
285
|
+
for key, value in _table(path, document).items():
|
|
235
286
|
default: Default | None
|
|
236
287
|
if key not in readers or (default := readers[key](value)) is None:
|
|
237
288
|
message: str = f"{path}: [tool.constricter] has an invalid {key} = {value!r}"
|
|
238
289
|
raise ValueError(message)
|
|
239
290
|
defaults[key.replace("-", "_")] = default
|
|
291
|
+
required: str | None
|
|
292
|
+
if _MIN_PYTHON not in defaults and (required := _required(document)) is not None:
|
|
293
|
+
defaults[_MIN_PYTHON] = required
|
|
240
294
|
return defaults
|