django-spec-cli 0.5.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/CHANGELOG.md +33 -1
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/CLAUDE.md +1 -1
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/PKG-INFO +3 -3
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/README.md +1 -1
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/checks.py +10 -3
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/spec_command.py +72 -6
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/version.py +1 -1
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/docs/index.md +17 -8
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/pyproject.toml +5 -3
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/test_checks.py +78 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/test_spec_command.py +227 -9
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/uv.lock +56 -56
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/.github/CODEOWNERS +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/.github/SECURITY.md +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/.github/dependabot.yml +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/.github/workflows/release.yml +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/.github/workflows/tests.yml +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/.github/workflows/upstream-drift.yml +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/.gitignore +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/.pre-commit-config.yaml +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/LICENSE +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/Makefile +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/apps.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/argv/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/argv/add_spec_arguments.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/argv/describe_parameters.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/argv/parameter_descriptor.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/argv/parsed_arguments.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/argv/utils.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/exit_code_for.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/py.typed +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/render/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/render/render_json_lines.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/render/render_result.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/render/render_table.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/render/utils.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/render/wants_table.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/report/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/report/unguarded_message.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/report/utils.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/settings.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/types/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/types/cli_transport.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/django_spec_cli/types/exit_code.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/docs/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/docs/examples/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/docs/examples/commands.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/docs/reference.md +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/mkdocs.yml +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/scripts/check-changelog +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/scripts/release-publish.sh +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/argv/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/argv/test_add_spec_arguments.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/argv/test_describe_parameters.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/argv/test_parameter_descriptor.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/argv/test_parsed_arguments.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/argv/test_utils.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/argv/utils.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/commands_app/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/commands_app/apps.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/commands_app/management/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/commands_app/management/commands/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/commands_app/management/commands/notes.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/commands_app/management/commands/unguarded_bad_parser.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/commands_app/management/commands/unguarded_broken_import.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/commands_app/management/commands/unguarded_broken_init.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/commands_app/management/commands/unguarded_empty.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/commands_app/management/commands/unguarded_no_registry.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/commands_app/management/commands/unguarded_plain.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/commands_app/management/commands/unguarded_report.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/commands_app/unguarded_registries.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/conftest.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/conftest_settings.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/dispatch_app/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/dispatch_app/apps.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/dispatch_app/models.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/docs/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/docs/test_commands_examples.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/docs/test_reference.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/render/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/render/test_render_json_lines.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/render/test_render_result.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/render/test_render_table.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/render/test_utils.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/render/test_wants_table.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/render/utils.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/report/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/report/test_unguarded_message.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/report/test_utils.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/test_apps.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/test_exit_code_for.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/test_init.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/test_settings.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/types/__init__.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/types/test_exit_code.py +0 -0
- {django_spec_cli-0.5.0 → django_spec_cli-0.6.0}/tests/utils.py +0 -0
|
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] — 2026-11-09
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **A field a Validator declares only under `cli` is an option.** The command
|
|
14
|
+
asked the kernel's `spec_caller_input_schema` for the options to offer with no
|
|
15
|
+
`call_context`, so the schema was the declaration made to no transport at all,
|
|
16
|
+
and a field the Validator declares only when
|
|
17
|
+
`call_transport(call_context) == "cli"` got no option. If it was required,
|
|
18
|
+
every run was refused for it. The schema is now read under the same stamped
|
|
19
|
+
dict `--help` already hands the Validator, so such a field is offered, marked
|
|
20
|
+
`(required)` where it is, and a run leaving it out is refused with the shape
|
|
21
|
+
check's own message. A field declared to another transport is still no option.
|
|
22
|
+
The command line page no longer carries the caveat that asked for such a field
|
|
23
|
+
to be declared to every transport.
|
|
24
|
+
|
|
25
|
+
- **A field a Validator declares only under `cli` that dispatch seeds itself is
|
|
26
|
+
refused for its spec alone.** The read `--help` makes under the stamped dict
|
|
27
|
+
raised `ImproperlyConfigured` for such a field, for example `user`, while the
|
|
28
|
+
parser was built, so every spec of the command, `--help` and `--explain` died
|
|
29
|
+
with a traceback, and `django_spec_cli.W004` said nothing because
|
|
30
|
+
`check_seeds` takes no `call_context`. The parser now takes that refusal for
|
|
31
|
+
the one spec: its subcommand takes no option and its `--help` prints the
|
|
32
|
+
kernel's reason, a run of it raises the same sentence as `ImproperlyConfigured`
|
|
33
|
+
before `--as-user` is looked up, whatever was sent, and `W004` names it. The
|
|
34
|
+
command's other specs are unaffected.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- **The floor on `django-service-specs` rises from 0.29.0 to 0.30.0**, the
|
|
38
|
+
release that lets `spec_caller_input_schema` take a `call_context`. A project
|
|
39
|
+
on an older kernel stays on 0.5.x.
|
|
40
|
+
|
|
10
41
|
## [0.5.0] — 2026-11-08
|
|
11
42
|
|
|
12
43
|
### Added
|
|
@@ -155,7 +186,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
155
186
|
`django-service-specs>=0.10.0`, the first kernel release with every name the
|
|
156
187
|
command reads at its root.
|
|
157
188
|
|
|
158
|
-
[Unreleased]: https://github.com/Artui/django-spec-cli/compare/v0.
|
|
189
|
+
[Unreleased]: https://github.com/Artui/django-spec-cli/compare/v0.6.0...HEAD
|
|
190
|
+
[0.6.0]: https://github.com/Artui/django-spec-cli/compare/v0.5.0...v0.6.0
|
|
159
191
|
[0.5.0]: https://github.com/Artui/django-spec-cli/compare/v0.4.1...v0.5.0
|
|
160
192
|
[0.4.1]: https://github.com/Artui/django-spec-cli/compare/v0.4.0...v0.4.1
|
|
161
193
|
[0.4.0]: https://github.com/Artui/django-spec-cli/compare/v0.3.1...v0.4.0
|
|
@@ -139,7 +139,7 @@ configured and enforced. isort order is stdlib, third party, first party.
|
|
|
139
139
|
| --- | --- | --- |
|
|
140
140
|
| Python | 3.10 | 3.10 through 3.14 |
|
|
141
141
|
| Django | 4.2 | 4.2, 5.0, 5.1, 5.2, 6.0, 6.1 |
|
|
142
|
-
| django-service-specs | 0.
|
|
142
|
+
| django-service-specs | 0.30.0 | the newest release, and the floor in the `floor` job |
|
|
143
143
|
|
|
144
144
|
The Python and Django floors are the kernel's, and cannot sit below them.
|
|
145
145
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: django-spec-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Run a django-service-specs registry from manage.py: one subcommand per spec, options generated from its parameters, exit codes for every refusal.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Artui/django-spec-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/Artui/django-spec-cli
|
|
@@ -30,7 +30,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
30
30
|
Classifier: Programming Language :: Python :: 3.14
|
|
31
31
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
32
32
|
Requires-Python: >=3.10
|
|
33
|
-
Requires-Dist: django-service-specs>=0.
|
|
33
|
+
Requires-Dist: django-service-specs>=0.30.0
|
|
34
34
|
Requires-Dist: django>=4.2
|
|
35
35
|
Description-Content-Type: text/markdown
|
|
36
36
|
|
|
@@ -61,7 +61,7 @@ the import path, the system check ids and the setting key are this package's.
|
|
|
61
61
|
pip install django-spec-cli
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Python 3.10 or later, Django 4.2 or later, and `django-service-specs` 0.
|
|
64
|
+
Python 3.10 or later, Django 4.2 or later, and `django-service-specs` 0.30.0 or
|
|
65
65
|
later, which it installs.
|
|
66
66
|
|
|
67
67
|
## Quickstart
|
|
@@ -25,7 +25,7 @@ the import path, the system check ids and the setting key are this package's.
|
|
|
25
25
|
pip install django-spec-cli
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Python 3.10 or later, Django 4.2 or later, and `django-service-specs` 0.
|
|
28
|
+
Python 3.10 or later, Django 4.2 or later, and `django-service-specs` 0.30.0 or
|
|
29
29
|
later, which it installs.
|
|
30
30
|
|
|
31
31
|
## Quickstart
|
|
@@ -108,10 +108,13 @@ def check_unguarded_specs(
|
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
def _refused(command: SpecCommand) -> Iterator[tuple[str, ImproperlyConfigured]]:
|
|
111
|
-
"""Each spec of ``command``
|
|
111
|
+
"""Each spec of ``command`` a run refuses as declared, with its refusal, in registry order.
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
same ``pool_seeds``,
|
|
113
|
+
Two questions, as ``SpecCommand.handle`` asks them and in its order:
|
|
114
|
+
``check_seeds`` under the same ``pool_seeds``, and the read the parser
|
|
115
|
+
made under the cli-stamped dict, which ``check_seeds`` never sees. The
|
|
116
|
+
parser has been built by now (the walk builds it), and it kept the
|
|
117
|
+
second's refusals. A spec both refuse is reported once, for the first.
|
|
115
118
|
Outside the walk's ``try``: a refusal names one spec, and the command
|
|
116
119
|
loaded, so its other specs are still reported and it is no W003.
|
|
117
120
|
"""
|
|
@@ -120,6 +123,10 @@ def _refused(command: SpecCommand) -> Iterator[tuple[str, ImproperlyConfigured]]
|
|
|
120
123
|
check_seeds(entry.spec, pool_seeds=command.pool_seeds)
|
|
121
124
|
except ImproperlyConfigured as error:
|
|
122
125
|
yield entry.name, error
|
|
126
|
+
continue
|
|
127
|
+
declared = command._refused_by_declaration.get(entry.name)
|
|
128
|
+
if declared is not None:
|
|
129
|
+
yield entry.name, declared
|
|
123
130
|
|
|
124
131
|
|
|
125
132
|
def _spec_commands(
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
import inspect
|
|
7
|
-
from collections.abc import Mapping
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
8
|
from dataclasses import replace
|
|
9
9
|
from typing import Any, TextIO, cast
|
|
10
10
|
|
|
@@ -78,7 +78,7 @@ class SpecCommand(BaseCommand):
|
|
|
78
78
|
[`add_spec_arguments`][django_spec_cli.argv.add_spec_arguments.add_spec_arguments]
|
|
79
79
|
spells them, as the input schema a transport advertises asks for them
|
|
80
80
|
([`spec_caller_input_schema`][django_service_specs.schema.spec_caller_input_schema.spec_caller_input_schema]
|
|
81
|
-
under ``pool_seeds``): less any key the server fills, and required only
|
|
81
|
+
under ``pool_seeds`` and the stamped ``call_context``): less any key the server fills, and required only
|
|
82
82
|
where dispatch refuses leaving it out.
|
|
83
83
|
No option is matched by a prefix: an abbreviation is refused rather
|
|
84
84
|
than read as whichever option it begins.
|
|
@@ -99,7 +99,14 @@ class SpecCommand(BaseCommand):
|
|
|
99
99
|
and its refusal is raised as ``ImproperlyConfigured``, as a command
|
|
100
100
|
naming no registry is: a traceback and ``ExitCode.ERROR``, whatever
|
|
101
101
|
the options sent. The command's other specs still run, and the system
|
|
102
|
-
check reports the same spec as ``django_spec_cli.W004``.
|
|
102
|
+
check reports the same spec as ``django_spec_cli.W004``. The same holds for a spec
|
|
103
|
+
whose Validator declares, only to ``cli``, a parameter dispatch seeds
|
|
104
|
+
itself: ``check_seeds`` is asked outside a call and never sees it, the
|
|
105
|
+
read the parser makes under the stamped dict does, and the parser takes
|
|
106
|
+
that refusal for its spec alone. Its subcommand takes no option and its
|
|
107
|
+
``--help`` gives the reason; a run raises it as above, and W004 reports
|
|
108
|
+
it too
|
|
109
|
+
(test_a_run_of_it_is_refused_with_the_kernel_s_reason_whatever_is_sent).
|
|
103
110
|
- ``--explain`` runs no spec: it lists the ones that authorize nothing,
|
|
104
111
|
as the system check does.
|
|
105
112
|
|
|
@@ -119,10 +126,14 @@ class SpecCommand(BaseCommand):
|
|
|
119
126
|
_command_label: str
|
|
120
127
|
_parser: CommandParser
|
|
121
128
|
_entries: dict[str, RegisteredSpec]
|
|
129
|
+
_refused_by_declaration: dict[str, ImproperlyConfigured]
|
|
122
130
|
|
|
123
131
|
def create_parser(self, prog_name: str, subcommand: str, **kwargs: Any) -> CommandParser:
|
|
124
132
|
"""Django's parser, with this command's options added and no option read by its prefix."""
|
|
125
133
|
self._command_label = subcommand
|
|
134
|
+
# Before ``add_arguments``, and not in it: a subclass overriding that
|
|
135
|
+
# without calling ``super()`` still leaves the system check a dict to read.
|
|
136
|
+
self._refused_by_declaration = {}
|
|
126
137
|
parser = super().create_parser(prog_name, subcommand, **kwargs)
|
|
127
138
|
# Django's own options are on this parser, and a prefix of one is no
|
|
128
139
|
# more an option here than on a subcommand.
|
|
@@ -175,8 +186,24 @@ class SpecCommand(BaseCommand):
|
|
|
175
186
|
)
|
|
176
187
|
self._entries[name] = entry
|
|
177
188
|
spec = kernel_spec(entry.spec)
|
|
178
|
-
parameters = _sendable(spec, self.pool_seeds, _call_context())
|
|
179
189
|
summary = _summary(spec, entry.name)
|
|
190
|
+
try:
|
|
191
|
+
parameters = _sendable(spec, self.pool_seeds, _call_context())
|
|
192
|
+
except ImproperlyConfigured as error:
|
|
193
|
+
# Only the read made under the stamped dict can raise here: a
|
|
194
|
+
# declaration the kernel refuses outside a call is
|
|
195
|
+
# ``check_seeds``' and is asked in ``handle``. Raising from the
|
|
196
|
+
# parser would stop every spec the command serves, ``--help``
|
|
197
|
+
# and ``--explain`` with them, so the refusal waits for a run of
|
|
198
|
+
# this spec, which raises it, and its subcommand takes no option.
|
|
199
|
+
self._refused_by_declaration[entry.name] = error
|
|
200
|
+
_add_refused_subparser(
|
|
201
|
+
subparsers,
|
|
202
|
+
name,
|
|
203
|
+
summary,
|
|
204
|
+
refused_message(entry.name, self._command_label, error),
|
|
205
|
+
)
|
|
206
|
+
continue
|
|
180
207
|
subparser = subparsers.add_parser(
|
|
181
208
|
name,
|
|
182
209
|
help=summary.replace("%", "%%"),
|
|
@@ -253,6 +280,9 @@ class SpecCommand(BaseCommand):
|
|
|
253
280
|
# ``ExitCode.ERROR`` is published as the failure with a traceback.
|
|
254
281
|
try:
|
|
255
282
|
check_seeds(entry.spec, pool_seeds=self.pool_seeds)
|
|
283
|
+
declared = self._refused_by_declaration.get(entry.name)
|
|
284
|
+
if declared is not None:
|
|
285
|
+
raise declared
|
|
256
286
|
except ImproperlyConfigured as error:
|
|
257
287
|
raise ImproperlyConfigured(
|
|
258
288
|
refused_message(entry.name, self._command_label, error)
|
|
@@ -328,7 +358,12 @@ def _sendable(
|
|
|
328
358
|
"""What a caller may send, as the kernel's caller schema reads it: required only where dispatch demands it.
|
|
329
359
|
|
|
330
360
|
The parameters ``spec_caller_input_schema`` offers under the command's
|
|
331
|
-
``pool_seeds
|
|
361
|
+
``pool_seeds`` and the stamped ``call_context``, so a field a Validator
|
|
362
|
+
declares only to ``cli`` is an option and one declared to another
|
|
363
|
+
transport is not
|
|
364
|
+
(test_a_required_field_declared_only_under_cli_is_an_option_marked_required,
|
|
365
|
+
test_a_field_declared_under_another_transport_is_no_option), and no key the
|
|
366
|
+
server fills is an option: one a provider
|
|
332
367
|
is annotated to fill (test_a_key_the_server_fills_is_no_option), and a
|
|
333
368
|
reflected read a registered seed fills
|
|
334
369
|
(test_a_reflected_read_a_seed_fills_is_an_option_only_without_the_seed).
|
|
@@ -342,7 +377,7 @@ def _sendable(
|
|
|
342
377
|
(test_a_read_a_provider_requires_itself_is_required). Only
|
|
343
378
|
relaxed, never made required, so a declared default still answers.
|
|
344
379
|
"""
|
|
345
|
-
schema = spec_caller_input_schema(spec, pool_seeds=pool_seeds)
|
|
380
|
+
schema = spec_caller_input_schema(spec, pool_seeds=pool_seeds, call_context=call_context)
|
|
346
381
|
required = frozenset(schema.get("required", ()))
|
|
347
382
|
return Parameters.of(
|
|
348
383
|
*(
|
|
@@ -353,6 +388,37 @@ def _sendable(
|
|
|
353
388
|
)
|
|
354
389
|
|
|
355
390
|
|
|
391
|
+
def _add_refused_subparser(subparsers: Any, name: str, summary: str, message: str) -> None:
|
|
392
|
+
"""The subcommand of a spec whose declaration refuses under the stamped dict: its reason, and no option.
|
|
393
|
+
|
|
394
|
+
Whatever is sent after the name is taken and read by nothing, so a run is
|
|
395
|
+
refused with the reason whatever it carried, as a spec ``check_seeds``
|
|
396
|
+
refuses is. Only ``--help`` is read, and it prints the reason.
|
|
397
|
+
"""
|
|
398
|
+
subparser = subparsers.add_parser(
|
|
399
|
+
name,
|
|
400
|
+
help=summary.replace("%", "%%"),
|
|
401
|
+
description=summary,
|
|
402
|
+
# argparse formats an epilog only where it names ``%(prog)``, so a
|
|
403
|
+
# ``%`` is doubled there and nowhere else
|
|
404
|
+
# (test_a_percent_sign_in_the_reason_is_printed_as_it_is).
|
|
405
|
+
epilog=message.replace("%", "%%") if "%(prog)" in message else message,
|
|
406
|
+
usage="%(prog)s [arguments ...]",
|
|
407
|
+
allow_abbrev=False,
|
|
408
|
+
)
|
|
409
|
+
reads_help = subparser.parse_known_args
|
|
410
|
+
|
|
411
|
+
def take_everything(
|
|
412
|
+
args: Sequence[str], namespace: argparse.Namespace | None = None
|
|
413
|
+
) -> tuple[argparse.Namespace, list[str]]:
|
|
414
|
+
# ``_SubParsersAction`` hands a list and a ``None`` namespace.
|
|
415
|
+
if {"-h", "--help"} & set(args):
|
|
416
|
+
return reads_help(args, namespace)
|
|
417
|
+
return argparse.Namespace(), []
|
|
418
|
+
|
|
419
|
+
subparser.parse_known_args = take_everything
|
|
420
|
+
|
|
421
|
+
|
|
356
422
|
def _summary(spec: ServiceSpec | SelectorSpec, name: str) -> str:
|
|
357
423
|
"""The first paragraph of what the spec's callable says about itself, and whether it reads or writes.
|
|
358
424
|
|
|
@@ -27,7 +27,7 @@ pip install django-spec-cli
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
It installs [`django-service-specs`](https://artui.github.io/django-service-specs/), the kernel whose specs it serves,
|
|
30
|
-
from 0.
|
|
30
|
+
from 0.30.0, on Python 3.10 and Django 4.2 or later. The command line was the
|
|
31
31
|
kernel's `commands/` subpackage, and moved here so the kernel carries no
|
|
32
32
|
transport.
|
|
33
33
|
|
|
@@ -264,6 +264,17 @@ with the same sentence, raised as `ImproperlyConfigured` (exit code 1,
|
|
|
264
264
|
with a traceback) before `--as-user` is looked up, even when the options sent
|
|
265
265
|
would let it run. The command's other specs are unaffected.
|
|
266
266
|
|
|
267
|
+
The same holds for a declaration only the command's own read reaches. A
|
|
268
|
+
Validator may declare a field only when `call_transport(call_context) == "cli"`,
|
|
269
|
+
and if that field is named for a key dispatch seeds itself, such as `user`,
|
|
270
|
+
`check_seeds` (asked outside a call) never sees it, and the read `--help`
|
|
271
|
+
makes under the stamped dict raises. That refusal belongs to the one spec: its
|
|
272
|
+
subcommand takes no option, its `--help` prints the kernel's reason, a run of it
|
|
273
|
+
ends with the same sentence as `ImproperlyConfigured` whatever was sent, and
|
|
274
|
+
`django_spec_cli.W004` names it. The command's other specs, `--help` and
|
|
275
|
+
`--explain` work as before. A spec both reads refuse is reported once, with the
|
|
276
|
+
`check_seeds` reason.
|
|
277
|
+
|
|
267
278
|
A `Parameter` is optional unless it says `required=True`, so a read declared
|
|
268
279
|
plainly over a keyword with no default is one such declaration:
|
|
269
280
|
|
|
@@ -323,13 +334,11 @@ docs/examples/commands.py:transport
|
|
|
323
334
|
such an entry, as it does for a missing one, and `format` and `action` are
|
|
324
335
|
never bound by name, so a spec written for HTTP runs here as it did.
|
|
325
336
|
- **`--help` is read under a dict too.** The command builds a stamped dict of
|
|
326
|
-
its own for each spec
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
never offered, and if it is required, every run is refused for it.
|
|
332
|
-
Declare such a field for every transport, or not at all.
|
|
337
|
+
its own for each spec, and the options come from the kernel's caller schema
|
|
338
|
+
read under it, as the Validator's declaration is. A field a Validator
|
|
339
|
+
declares only when `call_transport(call_context) == "cli"` is an option,
|
|
340
|
+
marked `(required)` where it is required, and one it declares to another
|
|
341
|
+
transport is none.
|
|
333
342
|
- **Seams.** The value is not configurable: a subclass cannot change what a
|
|
334
343
|
run stamps. What a subclass can supply is `resolve_as_user`, who the run is
|
|
335
344
|
made as, and `pool_seeds`, which adds a keyword to the pool every callable
|
|
@@ -49,8 +49,10 @@ dependencies = [
|
|
|
49
49
|
# ``tests/test_exit_code_for.py`` decides by name. Below 0.11.0 the kernel
|
|
50
50
|
# also still shipped its own copy of the command and registered its own
|
|
51
51
|
# ``W001`` and ``W003``. 0.29.0 adds ``build_call_context``, which the
|
|
52
|
-
# command builds the context it stamps ``cli`` on through.
|
|
53
|
-
|
|
52
|
+
# command builds the context it stamps ``cli`` on through. 0.30.0 lets
|
|
53
|
+
# ``spec_caller_input_schema`` take that context, so the options offered are
|
|
54
|
+
# read under it.
|
|
55
|
+
"django-service-specs>=0.30.0",
|
|
54
56
|
]
|
|
55
57
|
|
|
56
58
|
[project.urls]
|
|
@@ -150,7 +152,7 @@ skip-magic-trailing-comma = false
|
|
|
150
152
|
root = ["./django_spec_cli"]
|
|
151
153
|
|
|
152
154
|
[tool.bumpversion]
|
|
153
|
-
current_version = "0.
|
|
155
|
+
current_version = "0.6.0"
|
|
154
156
|
allow_dirty = false
|
|
155
157
|
commit = false
|
|
156
158
|
tag = false
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import importlib
|
|
12
12
|
import io
|
|
13
|
+
from typing import Any
|
|
13
14
|
|
|
14
15
|
import pytest
|
|
15
16
|
from django.apps import apps
|
|
@@ -27,6 +28,9 @@ from django_service_specs import (
|
|
|
27
28
|
UnguardedReason,
|
|
28
29
|
UnguardedSpec,
|
|
29
30
|
Unrestricted,
|
|
31
|
+
ValidationContext,
|
|
32
|
+
Validator,
|
|
33
|
+
call_transport,
|
|
30
34
|
)
|
|
31
35
|
|
|
32
36
|
from django_spec_cli.checks import check_unguarded_specs
|
|
@@ -263,6 +267,80 @@ class TestADeclarationTheKernelRefuses:
|
|
|
263
267
|
assert seeded == []
|
|
264
268
|
|
|
265
269
|
|
|
270
|
+
class _SeedNamedUnderCli(Validator):
|
|
271
|
+
"""Declares ``user``, a name dispatch seeds itself, only when the transport is ``cli``."""
|
|
272
|
+
|
|
273
|
+
def parameters(self, *, call_context: Any = None) -> Parameters:
|
|
274
|
+
declared = [Parameter("n", "integer", required=False)]
|
|
275
|
+
if call_transport(call_context) == "cli":
|
|
276
|
+
declared.append(Parameter("user", "string", required=False))
|
|
277
|
+
return Parameters.of(*declared)
|
|
278
|
+
|
|
279
|
+
def validate(self, arguments: Any, context: ValidationContext) -> dict[str, Any]:
|
|
280
|
+
return dict(arguments)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class TestADeclarationOnlyTheCliStampedReadRefuses:
|
|
284
|
+
"""The read the parser makes under the stamped dict, which ``check_seeds`` never sees."""
|
|
285
|
+
|
|
286
|
+
def _walk(
|
|
287
|
+
self, monkeypatch: pytest.MonkeyPatch, registry: SpecRegistry
|
|
288
|
+
) -> tuple[type, list[CheckMessage]]:
|
|
289
|
+
command = type("Command", (SpecCommand,), {"registry": registry})
|
|
290
|
+
monkeypatch.setattr("django_spec_cli.checks.get_commands", lambda: {"seeded": APP})
|
|
291
|
+
monkeypatch.setattr(
|
|
292
|
+
"django_spec_cli.checks.load_command_class", lambda app_name, name: command()
|
|
293
|
+
)
|
|
294
|
+
return command, check_unguarded_specs(None)
|
|
295
|
+
|
|
296
|
+
def test_the_spec_is_reported_as_w004_naming_it_and_the_command_is_no_w003(
|
|
297
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
298
|
+
) -> None:
|
|
299
|
+
registry = SpecRegistry()
|
|
300
|
+
registry.register(
|
|
301
|
+
"bad",
|
|
302
|
+
ServiceSpec(service=_echo, permissions=[StaffOnly()], validator=_SeedNamedUnderCli()),
|
|
303
|
+
)
|
|
304
|
+
registry.register("good", ServiceSpec(service=_echo, permissions=[StaffOnly()]))
|
|
305
|
+
command, messages = self._walk(monkeypatch, registry)
|
|
306
|
+
assert [(m.id, m.obj) for m in messages] == [("django_spec_cli.W004", command)]
|
|
307
|
+
assert messages[0].msg.startswith(
|
|
308
|
+
"'bad' in manage.py seeded cannot run as declared, and a run of it is refused: "
|
|
309
|
+
"ServiceSpec.validator declares the parameter(s) ['user'], which dispatch seeds itself."
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def test_a_command_overriding_add_arguments_without_super_is_still_checked(
|
|
313
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
314
|
+
) -> None:
|
|
315
|
+
class Bare(SpecCommand):
|
|
316
|
+
registry = SpecRegistry()
|
|
317
|
+
|
|
318
|
+
def add_arguments(self, parser: Any) -> None: ...
|
|
319
|
+
|
|
320
|
+
registry = SpecRegistry()
|
|
321
|
+
registry.register("open", ServiceSpec(service=_echo, permissions=[Unrestricted()]))
|
|
322
|
+
Bare.registry = registry
|
|
323
|
+
monkeypatch.setattr("django_spec_cli.checks.get_commands", lambda: {"bare": APP})
|
|
324
|
+
monkeypatch.setattr("django_spec_cli.checks.load_command_class", lambda *_: Bare())
|
|
325
|
+
assert [m.id for m in check_unguarded_specs(None)] == ["django_spec_cli.W001"]
|
|
326
|
+
|
|
327
|
+
def test_a_spec_both_reads_refuse_is_reported_once(
|
|
328
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
329
|
+
) -> None:
|
|
330
|
+
both = ServiceSpec(
|
|
331
|
+
service=_echo,
|
|
332
|
+
permissions=[StaffOnly()],
|
|
333
|
+
validator=_SeedNamedUnderCli(),
|
|
334
|
+
reads=Parameters.of(Parameter("project", "integer", required=False)),
|
|
335
|
+
kwargs=_needs_project,
|
|
336
|
+
)
|
|
337
|
+
registry = SpecRegistry()
|
|
338
|
+
registry.register("both", both)
|
|
339
|
+
_, messages = self._walk(monkeypatch, registry)
|
|
340
|
+
assert [m.id for m in messages] == ["django_spec_cli.W004"]
|
|
341
|
+
assert "requires the keyword(s) ['project']" in messages[0].msg
|
|
342
|
+
|
|
343
|
+
|
|
266
344
|
class TestAppConfigs:
|
|
267
345
|
"""``manage.py check <app_label>`` checks the commands of the apps it names."""
|
|
268
346
|
|