django-spec-cli 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/CHANGELOG.md +60 -1
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/CLAUDE.md +10 -8
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/PKG-INFO +8 -6
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/README.md +6 -4
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/apps.py +2 -2
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/checks.py +42 -3
- django_spec_cli-0.4.0/django_spec_cli/report/utils.py +18 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/spec_command.py +54 -16
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/version.py +1 -1
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/docs/index.md +28 -8
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/docs/reference.md +5 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/pyproject.toml +11 -9
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/docs/test_reference.py +7 -2
- django_spec_cli-0.4.0/tests/report/test_utils.py +13 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/test_checks.py +119 -2
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/test_spec_command.py +123 -4
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/uv.lock +1 -1
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/.github/CODEOWNERS +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/.github/SECURITY.md +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/.github/dependabot.yml +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/.github/workflows/release.yml +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/.github/workflows/tests.yml +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/.github/workflows/upstream-drift.yml +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/.gitignore +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/.pre-commit-config.yaml +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/LICENSE +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/Makefile +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/argv/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/argv/add_spec_arguments.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/argv/describe_parameters.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/argv/parameter_descriptor.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/argv/parsed_arguments.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/argv/utils.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/exit_code_for.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/py.typed +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/render/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/render/render_json_lines.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/render/render_result.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/render/render_table.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/render/utils.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/render/wants_table.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/report/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/report/unguarded_message.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/settings.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/types/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/types/exit_code.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/docs/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/docs/examples/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/docs/examples/commands.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/mkdocs.yml +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/scripts/check-changelog +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/scripts/release-publish.sh +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/argv/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/argv/test_add_spec_arguments.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/argv/test_describe_parameters.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/argv/test_parameter_descriptor.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/argv/test_parsed_arguments.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/argv/test_utils.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/argv/utils.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/commands_app/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/commands_app/apps.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/commands_app/management/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/commands_app/management/commands/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/commands_app/management/commands/notes.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/commands_app/management/commands/unguarded_bad_parser.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/commands_app/management/commands/unguarded_broken_import.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/commands_app/management/commands/unguarded_broken_init.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/commands_app/management/commands/unguarded_empty.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/commands_app/management/commands/unguarded_no_registry.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/commands_app/management/commands/unguarded_plain.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/commands_app/management/commands/unguarded_report.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/commands_app/unguarded_registries.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/conftest.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/conftest_settings.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/dispatch_app/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/dispatch_app/apps.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/dispatch_app/models.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/docs/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/docs/test_commands_examples.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/render/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/render/test_render_json_lines.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/render/test_render_result.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/render/test_render_table.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/render/test_utils.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/render/test_wants_table.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/render/utils.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/report/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/report/test_unguarded_message.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/test_apps.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/test_exit_code_for.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/test_init.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/test_settings.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/types/__init__.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/types/test_exit_code.py +0 -0
- {django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/utils.py +0 -0
|
@@ -7,6 +7,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] — 2026-11-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **A spec the kernel refuses as declared is reported, and never run.**
|
|
14
|
+
Every spec a `SpecCommand` serves is now put to the kernel's `check_seeds`
|
|
15
|
+
under the command's `pool_seeds`, which other transports already ask where
|
|
16
|
+
they register a spec. `manage.py check` reports each spec it refuses as
|
|
17
|
+
`django_spec_cli.W004`, a warning, naming the command, the spec and the
|
|
18
|
+
kernel's reason. A run of such a spec raises `ImproperlyConfigured` with the
|
|
19
|
+
same sentence, exit code 1 with a traceback, before `--as-user` is looked
|
|
20
|
+
up. The command's other specs, its `--help` and its `--explain` are
|
|
21
|
+
unaffected.
|
|
22
|
+
|
|
23
|
+
Three of the kernel's refusals stop runs that succeeded before, because
|
|
24
|
+
each fails only some calls. The first is a read declared without
|
|
25
|
+
`required=True`, which is `Parameter`'s default, over a keyword the
|
|
26
|
+
service, the selector or a callable such as a `kwargs` provider takes by
|
|
27
|
+
name with no default: `Parameter("title", "string")` over
|
|
28
|
+
`def rename(*, title)`, or `def needs_project(*, project: int)` as the
|
|
29
|
+
provider. A run sending the read worked, and one leaving it out was refused
|
|
30
|
+
as missing or crashed with the provider's `TypeError`; now every run is
|
|
31
|
+
refused, so declare such a read `required=True` or give the keyword a
|
|
32
|
+
default. The second is an array read declaring no `items` over a keyword
|
|
33
|
+
annotated with its element, such as `pks: list[int]`, which was handed the
|
|
34
|
+
command line's strings. The third is a `progress_reporter` requiring a
|
|
35
|
+
keyword nothing fills, which a run whose service never reports never
|
|
36
|
+
called. The others refuse a declaration on which every run that reached
|
|
37
|
+
the callable already failed, with its `TypeError` or the kernel's own
|
|
38
|
+
`ImproperlyConfigured`, which now names the spec and the command.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- **The kernel floor is `django-service-specs>=0.25.0`, the first kernel
|
|
42
|
+
release that keeps a read a provider requires itself demanded of the
|
|
43
|
+
caller.** From 0.3.1 `--help` reads its options, and which are marked
|
|
44
|
+
`(required)`, from the kernel's `spec_caller_input_schema`, so it repeats
|
|
45
|
+
what the installed kernel answers. On kernel 0.24.x a read declared
|
|
46
|
+
required that a `kwargs` provider takes by name with no default, such as
|
|
47
|
+
`def needs_project(*, project: int)`, was offered without `(required)`, and
|
|
48
|
+
a run leaving it out crashed in the provider with a `TypeError`. From 0.25.0
|
|
49
|
+
it is marked `(required)`, and that run is refused as invalid arguments,
|
|
50
|
+
exit code 3. The lowest-declared-versions job runs the suite on 0.25.0.
|
|
51
|
+
|
|
52
|
+
## [0.3.1] — 2026-11-04
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
- **`--help` offers and requires what dispatch asks a caller for.** A
|
|
56
|
+
subcommand's options, and which of them are marked `(required)`, are now
|
|
57
|
+
read from the kernel's `spec_caller_input_schema` under the command's
|
|
58
|
+
`pool_seeds`, rather than from `server_owned_keys` read without them. On any
|
|
59
|
+
kernel from 0.15.0, two parameters were shown wrongly. A read declared
|
|
60
|
+
required beside a provider that may fill it, such as a `kwargs` provider
|
|
61
|
+
whose return names no keys, was marked `(required)`, though a run leaving
|
|
62
|
+
it out was dispatched. A `reflected` read named like a seed the command's
|
|
63
|
+
`pool_seeds` registers was offered as an option and marked `(required)`,
|
|
64
|
+
and passing it was refused as an unknown argument, exit code 3. It is no
|
|
65
|
+
longer an option. No public name changes.
|
|
66
|
+
|
|
10
67
|
## [0.3.0] — 2026-11-04
|
|
11
68
|
|
|
12
69
|
### Changed
|
|
@@ -67,7 +124,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
67
124
|
`django-service-specs>=0.10.0`, the first kernel release with every name the
|
|
68
125
|
command reads at its root.
|
|
69
126
|
|
|
70
|
-
[Unreleased]: https://github.com/Artui/django-spec-cli/compare/v0.
|
|
127
|
+
[Unreleased]: https://github.com/Artui/django-spec-cli/compare/v0.4.0...HEAD
|
|
128
|
+
[0.4.0]: https://github.com/Artui/django-spec-cli/compare/v0.3.1...v0.4.0
|
|
129
|
+
[0.3.1]: https://github.com/Artui/django-spec-cli/compare/v0.3.0...v0.3.1
|
|
71
130
|
[0.3.0]: https://github.com/Artui/django-spec-cli/compare/v0.2.1...v0.3.0
|
|
72
131
|
[0.2.1]: https://github.com/Artui/django-spec-cli/compare/v0.2.0...v0.2.1
|
|
73
132
|
[0.2.0]: https://github.com/Artui/django-spec-cli/compare/v0.1.0...v0.2.0
|
|
@@ -8,8 +8,9 @@ The management-command transport for `django-service-specs`. A project
|
|
|
8
8
|
subclasses `SpecCommand`, names a `SpecRegistry`, and `manage.py <command>`
|
|
9
9
|
serves one subcommand per spec: an option for each parameter, a principal named
|
|
10
10
|
by `--as-user`, the result as a table or JSON Lines, and an exit code for every
|
|
11
|
-
refusal.
|
|
12
|
-
spec that authorizes nothing
|
|
11
|
+
refusal. Three system checks, `django_spec_cli.W001`, `W003` and `W004`,
|
|
12
|
+
report a served spec that authorizes nothing, a command that cannot be loaded,
|
|
13
|
+
and a spec the kernel's `check_seeds` refuses, whose run is refused too.
|
|
13
14
|
|
|
14
15
|
It is the kernel's `commands/` subpackage, moved out with its history so that
|
|
15
16
|
the kernel can carry no transport, and the kernel carries no copy of it from
|
|
@@ -82,11 +83,12 @@ so do the check ids and the setting key, `SPEC_CLI_OUTPUT`.
|
|
|
82
83
|
Each of these reads as an oversight and is not.
|
|
83
84
|
|
|
84
85
|
- **The checks are registered by this package's own `AppConfig`.** A project
|
|
85
|
-
lists `"django_spec_cli"` in `INSTALLED_APPS` to get `W001
|
|
86
|
-
kernel keeps the registry fact the check reads,
|
|
87
|
-
another transport can ask the same question of a
|
|
88
|
-
app stays importable and registers nothing, so
|
|
89
|
-
(`tests/test_checks.py`,
|
|
86
|
+
lists `"django_spec_cli"` in `INSTALLED_APPS` to get `W001`, `W003` and
|
|
87
|
+
`W004`. The kernel keeps the registry fact the check reads,
|
|
88
|
+
`unguarded_specs`, because another transport can ask the same question of a
|
|
89
|
+
registry. The kernel's own app stays importable and registers nothing, so
|
|
90
|
+
listing only it gets no check (`tests/test_checks.py`,
|
|
91
|
+
`test_the_kernel_s_app_registers_no_check_of_its_own`).
|
|
90
92
|
- **The test settings list no kernel app.** The package needs nothing of the
|
|
91
93
|
kernel switched on, and the suite shows it.
|
|
92
94
|
|
|
@@ -137,7 +139,7 @@ configured and enforced. isort order is stdlib, third party, first party.
|
|
|
137
139
|
| --- | --- | --- |
|
|
138
140
|
| Python | 3.10 | 3.10 through 3.14 |
|
|
139
141
|
| Django | 4.2 | 4.2, 5.0, 5.1, 5.2, 6.0, 6.1 |
|
|
140
|
-
| django-service-specs | 0.
|
|
142
|
+
| django-service-specs | 0.25.0 | the newest release, and the floor in the `floor` job |
|
|
141
143
|
|
|
142
144
|
The Python and Django floors are the kernel's, and cannot sit below them.
|
|
143
145
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: django-spec-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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.25.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.25.0 or
|
|
65
65
|
later, which it installs.
|
|
66
66
|
|
|
67
67
|
## Quickstart
|
|
@@ -92,9 +92,11 @@ authorize nothing.
|
|
|
92
92
|
## The system check
|
|
93
93
|
|
|
94
94
|
List `"django_spec_cli"` in `INSTALLED_APPS` and `manage.py check` reports
|
|
95
|
-
every open spec a `SpecCommand` serves as `django_spec_cli.W001`,
|
|
96
|
-
that could not be loaded as `django_spec_cli.W003
|
|
97
|
-
|
|
95
|
+
every open spec a `SpecCommand` serves as `django_spec_cli.W001`, a command
|
|
96
|
+
that could not be loaded as `django_spec_cli.W003`, and a spec the kernel's
|
|
97
|
+
`check_seeds` refuses under the command's `pool_seeds` as `django_spec_cli.W004`.
|
|
98
|
+
A run of a spec W004 reports is refused whether or not the app is listed. The
|
|
99
|
+
command itself needs no app listed.
|
|
98
100
|
|
|
99
101
|
The kernel keeps no check of its own: its app registers nothing, so a project
|
|
100
102
|
listing only `"django_service_specs"` gets no check for a command. List
|
|
@@ -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.25.0 or
|
|
29
29
|
later, which it installs.
|
|
30
30
|
|
|
31
31
|
## Quickstart
|
|
@@ -56,9 +56,11 @@ authorize nothing.
|
|
|
56
56
|
## The system check
|
|
57
57
|
|
|
58
58
|
List `"django_spec_cli"` in `INSTALLED_APPS` and `manage.py check` reports
|
|
59
|
-
every open spec a `SpecCommand` serves as `django_spec_cli.W001`,
|
|
60
|
-
that could not be loaded as `django_spec_cli.W003
|
|
61
|
-
|
|
59
|
+
every open spec a `SpecCommand` serves as `django_spec_cli.W001`, a command
|
|
60
|
+
that could not be loaded as `django_spec_cli.W003`, and a spec the kernel's
|
|
61
|
+
`check_seeds` refuses under the command's `pool_seeds` as `django_spec_cli.W004`.
|
|
62
|
+
A run of a spec W004 reports is refused whether or not the app is listed. The
|
|
63
|
+
command itself needs no app listed.
|
|
62
64
|
|
|
63
65
|
The kernel keeps no check of its own: its app registers nothing, so a project
|
|
64
66
|
listing only `"django_service_specs"` gets no check for a command. List
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""The app a project lists to have its open specs reported by ``manage.py check``."""
|
|
1
|
+
"""The app a project lists to have its open and unrunnable specs reported by ``manage.py check``."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -9,7 +9,7 @@ from django_spec_cli.checks import check_unguarded_specs
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class DjangoSpecCliConfig(AppConfig):
|
|
12
|
-
"""Registers the
|
|
12
|
+
"""Registers the system check over every ``SpecCommand``'s specs, and does nothing else.
|
|
13
13
|
|
|
14
14
|
Optional: the package works unlisted, and a project that does not list it
|
|
15
15
|
gets no check and no sign that it is missing. It owns no models, so it
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""The system check reporting every spec a ``SpecCommand`` serves that authorizes nothing."""
|
|
1
|
+
"""The system check reporting every spec a ``SpecCommand`` serves that authorizes nothing or cannot run."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -7,22 +7,28 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
from django.apps import AppConfig
|
|
9
9
|
from django.core.checks import CheckMessage, Warning
|
|
10
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
10
11
|
from django.core.management import get_commands, load_command_class
|
|
11
|
-
from django_service_specs import UnguardedSpec, unguarded_specs
|
|
12
|
+
from django_service_specs import UnguardedSpec, check_seeds, unguarded_specs
|
|
12
13
|
|
|
13
14
|
from django_spec_cli.report.unguarded_message import unguarded_message
|
|
15
|
+
from django_spec_cli.report.utils import refused_message
|
|
14
16
|
from django_spec_cli.spec_command import SpecCommand
|
|
15
17
|
|
|
16
18
|
_HINT = (
|
|
17
19
|
"Declare a PermissionCheck that refuses someone in the spec's permissions, or add "
|
|
18
20
|
"'django_spec_cli.W001' to SILENCED_SYSTEM_CHECKS if the spec is meant to be open."
|
|
19
21
|
)
|
|
22
|
+
_REFUSED_HINT = (
|
|
23
|
+
"Change the declaration as the message says. Until then a run of the spec is refused, "
|
|
24
|
+
"whether or not django_spec_cli.W004 is silenced."
|
|
25
|
+
)
|
|
20
26
|
|
|
21
27
|
|
|
22
28
|
def check_unguarded_specs(
|
|
23
29
|
app_configs: Sequence[AppConfig] | None, **kwargs: Any
|
|
24
30
|
) -> list[CheckMessage]:
|
|
25
|
-
"""Warn once per spec that authorizes nothing, in every ``SpecCommand`` of the project.
|
|
31
|
+
"""Warn once per spec that authorizes nothing or cannot run, in every ``SpecCommand`` of the project.
|
|
26
32
|
|
|
27
33
|
Registered by the app's ``ready()``, so a project gets it by listing
|
|
28
34
|
``django_spec_cli`` in ``INSTALLED_APPS`` and not otherwise. There is
|
|
@@ -40,6 +46,15 @@ def check_unguarded_specs(
|
|
|
40
46
|
spec is a decision or an accident the check cannot tell apart, and an
|
|
41
47
|
``Error`` would stop ``migrate`` on a project working as intended.
|
|
42
48
|
``SILENCED_SYSTEM_CHECKS`` silences it by id, so for every spec at once.
|
|
49
|
+
- ``django_spec_cli.W004``, one per spec that
|
|
50
|
+
[`check_seeds`][django_service_specs.dispatch.check_seeds.check_seeds]
|
|
51
|
+
refuses under the command's ``pool_seeds``, such as an optional read
|
|
52
|
+
that a ``kwargs`` provider takes by name with no default, with the
|
|
53
|
+
kernel's reason and the command class as ``obj``. A run of that spec
|
|
54
|
+
is refused with the same sentence, so this is the same refusal told
|
|
55
|
+
before anyone runs it. A ``Warning`` for the reason W003 is one: an
|
|
56
|
+
``Error`` would stop ``migrate`` over a spec nobody runs, while the
|
|
57
|
+
command's other specs work.
|
|
43
58
|
- ``django_spec_cli.W003``, one per command that could not be loaded or
|
|
44
59
|
whose parser could not be built, naming it and the error. Without this
|
|
45
60
|
app nothing loads that command until somebody runs it, so failing the
|
|
@@ -80,9 +95,33 @@ def check_unguarded_specs(
|
|
|
80
95
|
)
|
|
81
96
|
for spec in specs
|
|
82
97
|
)
|
|
98
|
+
messages.extend(
|
|
99
|
+
Warning(
|
|
100
|
+
refused_message(spec_name, name, error),
|
|
101
|
+
hint=_REFUSED_HINT,
|
|
102
|
+
obj=type(command),
|
|
103
|
+
id="django_spec_cli.W004",
|
|
104
|
+
)
|
|
105
|
+
for spec_name, error in _refused(command)
|
|
106
|
+
)
|
|
83
107
|
return messages
|
|
84
108
|
|
|
85
109
|
|
|
110
|
+
def _refused(command: SpecCommand) -> Iterator[tuple[str, ImproperlyConfigured]]:
|
|
111
|
+
"""Each spec of ``command`` that ``check_seeds`` refuses, with its refusal, in registry order.
|
|
112
|
+
|
|
113
|
+
The same question ``SpecCommand.handle`` asks before a run, under the
|
|
114
|
+
same ``pool_seeds``, so a spec reported here is one a run refuses.
|
|
115
|
+
Outside the walk's ``try``: a refusal names one spec, and the command
|
|
116
|
+
loaded, so its other specs are still reported and it is no W003.
|
|
117
|
+
"""
|
|
118
|
+
for entry in command.registry:
|
|
119
|
+
try:
|
|
120
|
+
check_seeds(entry.spec, pool_seeds=command.pool_seeds)
|
|
121
|
+
except ImproperlyConfigured as error:
|
|
122
|
+
yield entry.name, error
|
|
123
|
+
|
|
124
|
+
|
|
86
125
|
def _spec_commands(
|
|
87
126
|
app_configs: Sequence[AppConfig] | None,
|
|
88
127
|
) -> Iterator[tuple[str, str, tuple[SpecCommand, tuple[UnguardedSpec, ...]] | Exception]]:
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Helpers the system check and ``SpecCommand`` share: the sentence for a spec the kernel refuses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def refused_message(spec_name: str, command_label: str, error: ImproperlyConfigured) -> str:
|
|
9
|
+
"""The one-line report of ``spec_name``, which ``check_seeds`` refused with ``error``.
|
|
10
|
+
|
|
11
|
+
``command_label`` is the name an operator types after ``manage.py``. The
|
|
12
|
+
system check and a run of the spec both print this, so the warning a
|
|
13
|
+
project sees before a run reads as the refusal the run ends with.
|
|
14
|
+
"""
|
|
15
|
+
return (
|
|
16
|
+
f"{spec_name!r} in manage.py {command_label} cannot run as declared, and a run of it "
|
|
17
|
+
f"is refused: {error}"
|
|
18
|
+
)
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import argparse
|
|
6
6
|
import inspect
|
|
7
7
|
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import replace
|
|
8
9
|
from typing import Any, TextIO, cast
|
|
9
10
|
|
|
10
11
|
from django.core.exceptions import ImproperlyConfigured
|
|
@@ -25,14 +26,14 @@ from django_service_specs import (
|
|
|
25
26
|
ServiceSpec,
|
|
26
27
|
ServiceValidationError,
|
|
27
28
|
SpecRegistry,
|
|
29
|
+
check_seeds,
|
|
28
30
|
coerce_flat,
|
|
29
31
|
dispatch,
|
|
30
32
|
kernel_spec,
|
|
31
33
|
present,
|
|
32
34
|
refusal_messages,
|
|
33
|
-
relax_partial,
|
|
34
35
|
resolve_principal,
|
|
35
|
-
|
|
36
|
+
spec_caller_input_schema,
|
|
36
37
|
unguarded_specs,
|
|
37
38
|
)
|
|
38
39
|
|
|
@@ -44,6 +45,7 @@ from django_spec_cli.exit_code_for import exit_code_for
|
|
|
44
45
|
from django_spec_cli.render.render_result import render_result
|
|
45
46
|
from django_spec_cli.render.wants_table import wants_table
|
|
46
47
|
from django_spec_cli.report.unguarded_message import unguarded_message
|
|
48
|
+
from django_spec_cli.report.utils import refused_message
|
|
47
49
|
from django_spec_cli.types.exit_code import ExitCode
|
|
48
50
|
|
|
49
51
|
_OWN_OPTIONS = frozenset({"--as-user", "--json", "--table"})
|
|
@@ -72,8 +74,10 @@ class SpecCommand(BaseCommand):
|
|
|
72
74
|
- **One subcommand per registry entry**, named after the entry with each
|
|
73
75
|
``_`` shown as ``-``. Its options are the spec's parameters, as
|
|
74
76
|
[`add_spec_arguments`][django_spec_cli.argv.add_spec_arguments.add_spec_arguments]
|
|
75
|
-
spells them,
|
|
76
|
-
([`
|
|
77
|
+
spells them, as the input schema a transport advertises asks for them
|
|
78
|
+
([`spec_caller_input_schema`][django_service_specs.schema.spec_caller_input_schema.spec_caller_input_schema]
|
|
79
|
+
under ``pool_seeds``): less any key the server fills, and required only
|
|
80
|
+
where dispatch refuses leaving it out.
|
|
77
81
|
No option is matched by a prefix: an abbreviation is refused rather
|
|
78
82
|
than read as whichever option it begins.
|
|
79
83
|
- **``--as-user`` is required.** Its value goes through
|
|
@@ -87,6 +91,13 @@ class SpecCommand(BaseCommand):
|
|
|
87
91
|
terminal ([`wants_table`][django_spec_cli.render.wants_table.wants_table]).
|
|
88
92
|
- A refusal ends the run with a ``CommandError`` carrying its message and
|
|
89
93
|
an [`ExitCode`][django_spec_cli.types.exit_code.ExitCode].
|
|
94
|
+
- **A spec the kernel refuses as declared is never run.**
|
|
95
|
+
[`check_seeds`][django_service_specs.dispatch.check_seeds.check_seeds],
|
|
96
|
+
under ``pool_seeds``, is asked of the spec named before anything else,
|
|
97
|
+
and its refusal is raised as ``ImproperlyConfigured``, as a command
|
|
98
|
+
naming no registry is: a traceback and ``ExitCode.ERROR``, whatever
|
|
99
|
+
the options sent. The command's other specs still run, and the system
|
|
100
|
+
check reports the same spec as ``django_spec_cli.W004``.
|
|
90
101
|
- ``--explain`` runs no spec: it lists the ones that authorize nothing,
|
|
91
102
|
as the system check does.
|
|
92
103
|
|
|
@@ -162,7 +173,7 @@ class SpecCommand(BaseCommand):
|
|
|
162
173
|
)
|
|
163
174
|
self._entries[name] = entry
|
|
164
175
|
spec = kernel_spec(entry.spec)
|
|
165
|
-
parameters = _sendable(spec)
|
|
176
|
+
parameters = _sendable(spec, self.pool_seeds)
|
|
166
177
|
summary = _summary(spec, entry.name)
|
|
167
178
|
subparser = subparsers.add_parser(
|
|
168
179
|
name,
|
|
@@ -232,6 +243,18 @@ class SpecCommand(BaseCommand):
|
|
|
232
243
|
else "name a spec to run, or pass --explain. --help lists the specs."
|
|
233
244
|
)
|
|
234
245
|
entry = self._entries[name]
|
|
246
|
+
# Asked of the one spec named, not of every entry where the parser is
|
|
247
|
+
# built: a raise there would stop every spec the command serves, and
|
|
248
|
+
# ``--help`` and ``--explain`` with them. Before the lookup, so a
|
|
249
|
+
# project's ``resolve_as_user`` never runs for a spec that cannot.
|
|
250
|
+
# Not a ``CommandError``: the declaration is wrong, not the call, and
|
|
251
|
+
# ``ExitCode.ERROR`` is published as the failure with a traceback.
|
|
252
|
+
try:
|
|
253
|
+
check_seeds(entry.spec, pool_seeds=self.pool_seeds)
|
|
254
|
+
except ImproperlyConfigured as error:
|
|
255
|
+
raise ImproperlyConfigured(
|
|
256
|
+
refused_message(entry.name, self._command_label, error)
|
|
257
|
+
) from error
|
|
235
258
|
spec = kernel_spec(entry.spec)
|
|
236
259
|
parameters = spec.parameters()
|
|
237
260
|
raw = parsed_arguments(argparse.Namespace(**options), parameters)
|
|
@@ -281,18 +304,33 @@ class SpecCommand(BaseCommand):
|
|
|
281
304
|
self.stdout.write(unguarded_message(unguarded, label))
|
|
282
305
|
|
|
283
306
|
|
|
284
|
-
def _sendable(spec: ServiceSpec | SelectorSpec) -> Parameters:
|
|
285
|
-
"""What a caller may send, as the
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
(
|
|
290
|
-
|
|
291
|
-
(
|
|
307
|
+
def _sendable(spec: ServiceSpec | SelectorSpec, pool_seeds: PoolSeeds) -> Parameters:
|
|
308
|
+
"""What a caller may send, as the kernel's caller schema reads it: required only where dispatch demands it.
|
|
309
|
+
|
|
310
|
+
The parameters ``spec_caller_input_schema`` offers under the command's
|
|
311
|
+
``pool_seeds``, so no key the server fills is an option: one a provider
|
|
312
|
+
is annotated to fill (test_a_key_the_server_fills_is_no_option), and a
|
|
313
|
+
reflected read a registered seed fills
|
|
314
|
+
(test_a_reflected_read_a_seed_fills_is_an_option_only_without_the_seed).
|
|
315
|
+
Each is relaxed where the schema does not require it, so ``--help``
|
|
316
|
+
marks ``(required)`` exactly where an omission is refused: on a
|
|
317
|
+
``partial`` spec (test_a_partial_spec_requires_only_what_names_its_row)
|
|
318
|
+
and beside a provider that may fill it
|
|
319
|
+
(test_a_read_a_provider_may_fill_is_offered_and_not_required). A read
|
|
320
|
+
stays declared required where the provider takes it by name with no
|
|
321
|
+
default, since that provider cannot be the one to fill it
|
|
322
|
+
(test_a_read_a_provider_requires_itself_is_required). Only
|
|
323
|
+
relaxed, never made required, so a declared default still answers.
|
|
292
324
|
"""
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return Parameters.of(
|
|
325
|
+
schema = spec_caller_input_schema(spec, pool_seeds=pool_seeds)
|
|
326
|
+
required = frozenset(schema.get("required", ()))
|
|
327
|
+
return Parameters.of(
|
|
328
|
+
*(
|
|
329
|
+
param if param.name in required else replace(param, required=False)
|
|
330
|
+
for param in spec.parameters()
|
|
331
|
+
if param.name in schema["properties"]
|
|
332
|
+
)
|
|
333
|
+
)
|
|
296
334
|
|
|
297
335
|
|
|
298
336
|
def _summary(spec: ServiceSpec | SelectorSpec, name: str) -> str:
|
|
@@ -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.25.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
|
|
|
@@ -86,14 +86,22 @@ equals sign: `--tag=-x`.
|
|
|
86
86
|
of its service's or selector's docstring and whether it reads or writes, or
|
|
87
87
|
its registered name when the function says nothing. `manage.py notes
|
|
88
88
|
rename-note --help` lists its options, with `(required)` where leaving one
|
|
89
|
-
out is refused.
|
|
90
|
-
|
|
89
|
+
out is refused. Both are read from the input schema a transport advertises,
|
|
90
|
+
[`spec_caller_input_schema`][django_service_specs.schema.spec_caller_input_schema.spec_caller_input_schema],
|
|
91
|
+
under the command's `pool_seeds`, so the command asks for what dispatch asks
|
|
92
|
+
for. On a `partial` spec, `(required)` is only what names the row: the
|
|
93
|
+
Validator's fields may be left out, as dispatch allows. A parameter a provider
|
|
94
|
+
may fill, such as a read beside a `kwargs` provider whose return names no keys,
|
|
95
|
+
is an option and is not required, unless the provider takes that read by name
|
|
96
|
+
with no default: only the caller can fill it then, so a read declared required
|
|
97
|
+
stays `(required)`.
|
|
91
98
|
|
|
92
99
|
Some parameters have no option:
|
|
93
100
|
|
|
94
|
-
- **A key the server fills
|
|
95
|
-
|
|
96
|
-
since
|
|
101
|
+
- **A key the server fills**: one a `kwargs` provider is annotated to answer,
|
|
102
|
+
since a value sent for it would be dropped, and a `reflected` read named like
|
|
103
|
+
a seed `pool_seeds` registers, since the seed fills it and dispatch refuses an
|
|
104
|
+
argument of its name.
|
|
97
105
|
- **A parameter with no flat spelling**: an object, an array of objects or of
|
|
98
106
|
arrays, and a file. The subcommand's help names them. A spec that requires
|
|
99
107
|
one, such as any `many` spec, cannot be run from the command line.
|
|
@@ -245,8 +253,20 @@ sentence from `manage.py check`, as `django_spec_cli.W001`, once per
|
|
|
245
253
|
open spec of every `SpecCommand` in the project, and a command that could not
|
|
246
254
|
be loaded to be checked as `django_spec_cli.W003`. There is no
|
|
247
255
|
`W002`: a spec with an empty `permissions` is refused when it is built, so
|
|
248
|
-
none reaches a registry.
|
|
249
|
-
|
|
256
|
+
none reaches a registry.
|
|
257
|
+
|
|
258
|
+
`django_spec_cli.W004` reports, once per spec, a declaration the kernel's
|
|
259
|
+
[`check_seeds`][django_service_specs.dispatch.check_seeds.check_seeds]
|
|
260
|
+
refuses under the command's `pool_seeds`: a keyword the callable requires
|
|
261
|
+
that nothing fills, or a read declared optional that a `kwargs` provider
|
|
262
|
+
takes by name with no default, among others. A run of that spec is refused
|
|
263
|
+
with the same sentence, raised as `ImproperlyConfigured` (exit code 1,
|
|
264
|
+
with a traceback) before `--as-user` is looked up, even when the options sent
|
|
265
|
+
would let it run. The command's other specs are unaffected.
|
|
266
|
+
|
|
267
|
+
All three are warnings, so a spec nobody runs never stops `migrate`, and
|
|
268
|
+
`SILENCED_SYSTEM_CHECKS` silences one by id. Silencing `W004` hides the report
|
|
269
|
+
and does not let the spec run.
|
|
250
270
|
|
|
251
271
|
The kernel keeps no check of its own: its app registers nothing, so a project
|
|
252
272
|
listing only `"django_service_specs"` gets no check for a command. List
|
|
@@ -28,6 +28,11 @@ Registered by `DjangoSpecCliConfig`, so a project gets them by listing
|
|
|
28
28
|
open spec, worded by
|
|
29
29
|
[`unguarded_message`][django_spec_cli.report.unguarded_message.unguarded_message].
|
|
30
30
|
- **`django_spec_cli.W003`**: a command could not be loaded to be checked.
|
|
31
|
+
- **`django_spec_cli.W004`**: the kernel's
|
|
32
|
+
[`check_seeds`][django_service_specs.dispatch.check_seeds.check_seeds]
|
|
33
|
+
refuses a spec a `SpecCommand` serves, under the command's `pool_seeds`.
|
|
34
|
+
Once per refused spec. A run of it is refused with the same sentence,
|
|
35
|
+
whether or not the warning is silenced.
|
|
31
36
|
|
|
32
37
|
## The command
|
|
33
38
|
|
|
@@ -40,14 +40,16 @@ dependencies = [
|
|
|
40
40
|
# kernel's specs, so this package cannot support a Django the kernel does
|
|
41
41
|
# not.
|
|
42
42
|
"django>=4.2",
|
|
43
|
-
# The first kernel release
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
# ``
|
|
48
|
-
#
|
|
49
|
-
# ``
|
|
50
|
-
|
|
43
|
+
# The first kernel release whose ``spec_caller_input_schema`` keeps a read
|
|
44
|
+
# a provider requires itself, by name and with no default, demanded of the
|
|
45
|
+
# caller. ``--help`` repeats that schema, so on 0.24.x such a read was not
|
|
46
|
+
# marked ``(required)`` and a run leaving it out crashed in the provider
|
|
47
|
+
# with a ``TypeError``; ``tests/test_spec_command.py`` holds it. 0.24.0 put
|
|
48
|
+
# ``PermissionCheckDenied`` in ``REFUSALS``, which
|
|
49
|
+
# ``tests/test_exit_code_for.py`` decides by name. Below 0.11.0 the kernel
|
|
50
|
+
# also still shipped its own copy of the command and registered its own
|
|
51
|
+
# ``W001`` and ``W003``.
|
|
52
|
+
"django-service-specs>=0.25.0",
|
|
51
53
|
]
|
|
52
54
|
|
|
53
55
|
[project.urls]
|
|
@@ -147,7 +149,7 @@ skip-magic-trailing-comma = false
|
|
|
147
149
|
root = ["./django_spec_cli"]
|
|
148
150
|
|
|
149
151
|
[tool.bumpversion]
|
|
150
|
-
current_version = "0.
|
|
152
|
+
current_version = "0.4.0"
|
|
151
153
|
allow_dirty = false
|
|
152
154
|
commit = false
|
|
153
155
|
tag = false
|
|
@@ -27,7 +27,12 @@ def test_every_root_name_has_an_entry_for_the_root_s_own_object() -> None:
|
|
|
27
27
|
assert documented[name] is getattr(django_spec_cli, name), name
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def
|
|
30
|
+
def test_the_setting_and_every_check_id_is_documented() -> None:
|
|
31
31
|
page = PAGE.read_text()
|
|
32
|
-
for spelling in (
|
|
32
|
+
for spelling in (
|
|
33
|
+
"SPEC_CLI_OUTPUT",
|
|
34
|
+
"django_spec_cli.W001",
|
|
35
|
+
"django_spec_cli.W003",
|
|
36
|
+
"django_spec_cli.W004",
|
|
37
|
+
):
|
|
33
38
|
assert f"`{spelling}`" in page, spelling
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
4
|
+
|
|
5
|
+
from django_spec_cli.report.utils import refused_message
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_names_the_spec_and_the_command_then_the_kernel_s_reason() -> None:
|
|
9
|
+
error = ImproperlyConfigured("ServiceSpec.service requires the keyword(s) ['tenant'].")
|
|
10
|
+
assert refused_message("greet", "notes", error) == (
|
|
11
|
+
"'greet' in manage.py notes cannot run as declared, and a run of it is refused: "
|
|
12
|
+
"ServiceSpec.service requires the keyword(s) ['tenant']."
|
|
13
|
+
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
"""The
|
|
1
|
+
"""The system checks, for an unguarded spec and a spec ``check_seeds`` refuses, run over
|
|
2
|
+
this suite's own command modules.
|
|
2
3
|
|
|
3
4
|
Other tests add command modules to the same app, so every assertion here is
|
|
4
5
|
filtered to the commands this file owns - the ``unguarded_`` ones - and none
|
|
@@ -16,10 +17,22 @@ from django.core.checks import CheckMessage, Warning
|
|
|
16
17
|
from django.core.management import call_command, get_commands, load_command_class
|
|
17
18
|
from django.core.management.base import SystemCheckError
|
|
18
19
|
from django.test import override_settings
|
|
19
|
-
from django_service_specs import
|
|
20
|
+
from django_service_specs import (
|
|
21
|
+
DEFAULT_POOL_SEEDS,
|
|
22
|
+
Parameter,
|
|
23
|
+
Parameters,
|
|
24
|
+
PoolSeeds,
|
|
25
|
+
ServiceSpec,
|
|
26
|
+
SpecRegistry,
|
|
27
|
+
UnguardedReason,
|
|
28
|
+
UnguardedSpec,
|
|
29
|
+
Unrestricted,
|
|
30
|
+
)
|
|
20
31
|
|
|
21
32
|
from django_spec_cli.checks import check_unguarded_specs
|
|
22
33
|
from django_spec_cli.report.unguarded_message import unguarded_message
|
|
34
|
+
from django_spec_cli.spec_command import SpecCommand
|
|
35
|
+
from tests.commands_app.unguarded_registries import StaffOnly
|
|
23
36
|
from tests.utils import run_project
|
|
24
37
|
|
|
25
38
|
APP = "tests.commands_app"
|
|
@@ -146,6 +159,110 @@ class TestACommandThatDoesNotLoad:
|
|
|
146
159
|
assert len(_own_w001(check_unguarded_specs(None))) == 3
|
|
147
160
|
|
|
148
161
|
|
|
162
|
+
def _needs_project(*, project: int) -> dict[str, int]:
|
|
163
|
+
return {"project": project}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _echo(**pool: object) -> object:
|
|
167
|
+
return pool
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _greet(*, tenant: str) -> str:
|
|
171
|
+
return f"hello {tenant}"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _project_spec(*, required: bool) -> ServiceSpec:
|
|
175
|
+
"""A read ``project`` beside a ``kwargs`` provider that takes it by name, with no default."""
|
|
176
|
+
return ServiceSpec(
|
|
177
|
+
service=_echo,
|
|
178
|
+
permissions=[StaffOnly()],
|
|
179
|
+
reads=Parameters.of(Parameter("project", "integer", required=required)),
|
|
180
|
+
kwargs=_needs_project,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TestADeclarationTheKernelRefuses:
|
|
185
|
+
"""``check_seeds``, under the command's own ``pool_seeds``, over every spec it serves.
|
|
186
|
+
|
|
187
|
+
Each test serves one command of its own through the walk, so nothing here
|
|
188
|
+
depends on what the suite's command modules declare.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
REFUSED = (
|
|
192
|
+
"'optional_project' in manage.py seeded cannot run as declared, and a run of it is "
|
|
193
|
+
"refused: ServiceSpec.kwargs requires the keyword(s) ['project'], declared as optional "
|
|
194
|
+
"reads that nothing else fills, so a call leaving one out fails with the provider's "
|
|
195
|
+
"own TypeError. Declare the read required, or give the keyword a default."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def _walk(
|
|
199
|
+
self,
|
|
200
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
201
|
+
registry: SpecRegistry,
|
|
202
|
+
pool_seeds: PoolSeeds = DEFAULT_POOL_SEEDS,
|
|
203
|
+
) -> tuple[type, list[CheckMessage]]:
|
|
204
|
+
command = type("Command", (SpecCommand,), {"registry": registry, "pool_seeds": pool_seeds})
|
|
205
|
+
monkeypatch.setattr("django_spec_cli.checks.get_commands", lambda: {"seeded": APP})
|
|
206
|
+
monkeypatch.setattr(
|
|
207
|
+
"django_spec_cli.checks.load_command_class", lambda app_name, name: command()
|
|
208
|
+
)
|
|
209
|
+
return command, check_unguarded_specs(None)
|
|
210
|
+
|
|
211
|
+
def test_an_optional_read_a_provider_requires_is_reported(
|
|
212
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
213
|
+
) -> None:
|
|
214
|
+
registry = SpecRegistry()
|
|
215
|
+
registry.register("optional_project", _project_spec(required=False))
|
|
216
|
+
command, messages = self._walk(monkeypatch, registry)
|
|
217
|
+
assert [m for m in messages if m.id == "django_spec_cli.W004"] == [
|
|
218
|
+
Warning(
|
|
219
|
+
self.REFUSED,
|
|
220
|
+
hint=(
|
|
221
|
+
"Change the declaration as the message says. Until then a run of the spec "
|
|
222
|
+
"is refused, whether or not django_spec_cli.W004 is silenced."
|
|
223
|
+
),
|
|
224
|
+
obj=command,
|
|
225
|
+
id="django_spec_cli.W004",
|
|
226
|
+
)
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
def test_a_spec_it_accepts_is_not_reported(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
230
|
+
registry = SpecRegistry()
|
|
231
|
+
registry.register("required_project", _project_spec(required=True))
|
|
232
|
+
_, messages = self._walk(monkeypatch, registry)
|
|
233
|
+
assert messages == []
|
|
234
|
+
|
|
235
|
+
def test_one_warning_per_refused_spec_beside_the_command_s_other_reports(
|
|
236
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
237
|
+
) -> None:
|
|
238
|
+
# Refusing one spec loses neither the W001 of an open spec in the same
|
|
239
|
+
# command nor the command itself to a W003.
|
|
240
|
+
registry = SpecRegistry()
|
|
241
|
+
registry.register("optional_project", _project_spec(required=False))
|
|
242
|
+
registry.register("required_project", _project_spec(required=True))
|
|
243
|
+
registry.register("open", ServiceSpec(service=_echo, permissions=[Unrestricted()]))
|
|
244
|
+
registry.register("unseeded", ServiceSpec(service=_greet, permissions=[StaffOnly()]))
|
|
245
|
+
_, messages = self._walk(monkeypatch, registry)
|
|
246
|
+
assert [(m.id, m.msg.split(" ")[0]) for m in messages] == [
|
|
247
|
+
("django_spec_cli.W001", "'open'"),
|
|
248
|
+
("django_spec_cli.W004", "'optional_project'"),
|
|
249
|
+
("django_spec_cli.W004", "'unseeded'"),
|
|
250
|
+
]
|
|
251
|
+
assert not any(message.is_serious() for message in messages)
|
|
252
|
+
|
|
253
|
+
def test_it_reads_the_command_s_pool_seeds(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
254
|
+
# ``tenant`` is filled by nothing under the stock seeds, and by the seed
|
|
255
|
+
# the command registers.
|
|
256
|
+
registry = SpecRegistry()
|
|
257
|
+
registry.register("greet", ServiceSpec(service=_greet, permissions=[StaffOnly()]))
|
|
258
|
+
_, unseeded = self._walk(monkeypatch, registry)
|
|
259
|
+
assert [m.id for m in unseeded] == ["django_spec_cli.W004"]
|
|
260
|
+
assert "['tenant']" in unseeded[0].msg
|
|
261
|
+
seeds = DEFAULT_POOL_SEEDS.extend(tenant=lambda: "acme")
|
|
262
|
+
_, seeded = self._walk(monkeypatch, registry, pool_seeds=seeds)
|
|
263
|
+
assert seeded == []
|
|
264
|
+
|
|
265
|
+
|
|
149
266
|
class TestAppConfigs:
|
|
150
267
|
"""``manage.py check <app_label>`` checks the commands of the apps it names."""
|
|
151
268
|
|
|
@@ -297,15 +297,102 @@ class TestSeeds:
|
|
|
297
297
|
command = command_over(self._registry(), pool_seeds=seeds)
|
|
298
298
|
assert run("greet", "--as-user", str(owner.pk), command=command) == '"hello acme"\n'
|
|
299
299
|
|
|
300
|
-
def
|
|
301
|
-
self, owner: Any
|
|
300
|
+
def test_a_reflected_read_a_seed_fills_is_an_option_only_without_the_seed(
|
|
301
|
+
self, capsys: pytest.CaptureFixture[str], owner: Any
|
|
302
302
|
) -> None:
|
|
303
|
+
# Offered under the command's seeds, it would be refused when passed,
|
|
304
|
+
# as an argument dispatch does not take.
|
|
305
|
+
def greet(*, tenant: str) -> str:
|
|
306
|
+
return f"hello {tenant}"
|
|
307
|
+
|
|
308
|
+
reads = Parameters.of(Parameter("tenant", "string", required=True, reflected=True))
|
|
309
|
+
spec = ServiceSpec(service=greet, permissions=[Unrestricted()], reads=reads)
|
|
310
|
+
registry = registry_of(greet=spec)
|
|
311
|
+
seeded = command_over(registry, pool_seeds=DEFAULT_POOL_SEEDS.extend(tenant=lambda: "acme"))
|
|
312
|
+
assert "--tenant" not in help_of(capsys, "greet", command=seeded)
|
|
313
|
+
assert run("greet", "--as-user", str(owner.pk), command=seeded) == '"hello acme"\n'
|
|
314
|
+
unseeded = " ".join(help_of(capsys, "greet", command=command_over(registry)).split())
|
|
315
|
+
assert "--tenant STRING (required)" in unseeded
|
|
316
|
+
|
|
317
|
+
def test_with_no_seeds_named_a_callable_reading_one_is_refused(self, owner: Any) -> None:
|
|
303
318
|
# The default is the stock seeds, as ``dispatch``'s: a seed nobody
|
|
304
|
-
# registered is the
|
|
305
|
-
|
|
319
|
+
# registered is no quiet default, and the kernel's ``check_seeds``
|
|
320
|
+
# refuses the spec before it reaches the callable's own ``TypeError``.
|
|
321
|
+
with pytest.raises(ImproperlyConfigured, match=r"\['tenant'\], which nothing fills"):
|
|
306
322
|
run("greet", "--as-user", str(owner.pk), command=command_over(self._registry()))
|
|
307
323
|
|
|
308
324
|
|
|
325
|
+
class TestADeclarationTheKernelRefuses:
|
|
326
|
+
"""A spec ``check_seeds`` refuses under the command's seeds never reaches dispatch.
|
|
327
|
+
|
|
328
|
+
``call_command`` handed an instance names it after this module, so the
|
|
329
|
+
sentence reads ``manage.py test_spec_command``.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
REFUSAL = (
|
|
333
|
+
"'echo' in manage.py test_spec_command cannot run as declared, and a run of it is refused: "
|
|
334
|
+
"ServiceSpec.kwargs requires the keyword(s) ['project'], declared as optional reads "
|
|
335
|
+
"that nothing else fills, so a call leaving one out fails with the provider's own "
|
|
336
|
+
"TypeError. Declare the read required, or give the keyword a default."
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def _command(self, **attributes: Any) -> SpecCommand:
|
|
340
|
+
def needs_project(*, project: int) -> dict[str, Any]:
|
|
341
|
+
return {"project": project}
|
|
342
|
+
|
|
343
|
+
spec = echo_spec(Parameter("project", "integer", required=False), kwargs=needs_project)
|
|
344
|
+
return command_over(registry_of(echo=spec, ping=open_spec(lambda: "pong")), **attributes)
|
|
345
|
+
|
|
346
|
+
@pytest.mark.parametrize("sent", [[], ["--project", "3"]], ids=["read-left-out", "read-sent"])
|
|
347
|
+
def test_a_run_of_it_is_refused_with_the_kernel_s_reason(
|
|
348
|
+
self, owner: Any, sent: list[str]
|
|
349
|
+
) -> None:
|
|
350
|
+
# Sending the read would run: refused anyway, because the declaration
|
|
351
|
+
# says it is optional and every run leaving it out crashes.
|
|
352
|
+
with pytest.raises(ImproperlyConfigured) as caught:
|
|
353
|
+
run("echo", "--as-user", str(owner.pk), *sent, command=self._command())
|
|
354
|
+
assert str(caught.value) == self.REFUSAL
|
|
355
|
+
|
|
356
|
+
def test_it_is_refused_before_the_user_is_looked_up(self) -> None:
|
|
357
|
+
looked_up: list[str] = []
|
|
358
|
+
|
|
359
|
+
def resolve_as_user(self: SpecCommand, value: str) -> Any:
|
|
360
|
+
looked_up.append(value)
|
|
361
|
+
raise PrincipalUnavailable("nobody")
|
|
362
|
+
|
|
363
|
+
command = self._command(resolve_as_user=resolve_as_user)
|
|
364
|
+
with pytest.raises(ImproperlyConfigured):
|
|
365
|
+
run("echo", "--as-user", "7", command=command)
|
|
366
|
+
assert looked_up == []
|
|
367
|
+
|
|
368
|
+
def test_the_command_s_other_specs_still_run(self, owner: Any) -> None:
|
|
369
|
+
assert run("ping", "--as-user", str(owner.pk), command=self._command()) == '"pong"\n'
|
|
370
|
+
|
|
371
|
+
def test_help_and_explain_still_answer(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
372
|
+
# Asked of the spec run, never where the parser is built, so the
|
|
373
|
+
# command's listing and its report still work.
|
|
374
|
+
command = self._command()
|
|
375
|
+
parser = command.create_parser("manage.py", "notes")
|
|
376
|
+
with pytest.raises(SystemExit):
|
|
377
|
+
parser.parse_args(["--help"])
|
|
378
|
+
assert "echo" in capsys.readouterr().out
|
|
379
|
+
assert "'echo' in manage.py " in run("--explain", command=command)
|
|
380
|
+
|
|
381
|
+
@override_settings(SILENCED_SYSTEM_CHECKS=["django_spec_cli.W004"])
|
|
382
|
+
def test_silencing_the_warning_does_not_let_it_run(self, owner: Any) -> None:
|
|
383
|
+
with pytest.raises(ImproperlyConfigured):
|
|
384
|
+
run("echo", "--as-user", str(owner.pk), "--project", "3", command=self._command())
|
|
385
|
+
|
|
386
|
+
@pytest.mark.usefixtures("keep_the_test_connection")
|
|
387
|
+
def test_from_argv_it_propagates_for_a_traceback_and_not_an_exit_code(self, owner: Any) -> None:
|
|
388
|
+
# ``run_from_argv`` turns only a ``CommandError`` into ``sys.exit``;
|
|
389
|
+
# anything else reaches ``manage.py`` as a traceback and exit status 1,
|
|
390
|
+
# which is ``ExitCode.ERROR`` as the docs publish it.
|
|
391
|
+
argv = ["manage.py", "notes", "--skip-checks", "echo", "--as-user", str(owner.pk)]
|
|
392
|
+
with pytest.raises(ImproperlyConfigured, match="cannot run as declared"):
|
|
393
|
+
self._command().run_from_argv(argv)
|
|
394
|
+
|
|
395
|
+
|
|
309
396
|
class TestAbbreviation:
|
|
310
397
|
"""A prefix of an option is never read as the option, the command's own or a spec's."""
|
|
311
398
|
|
|
@@ -428,6 +515,38 @@ class TestHelp:
|
|
|
428
515
|
assert "--project" not in text
|
|
429
516
|
assert "--n INTEGER" in text
|
|
430
517
|
|
|
518
|
+
def test_a_read_a_provider_may_fill_is_offered_and_not_required(
|
|
519
|
+
self, capsys: pytest.CaptureFixture[str], owner: Any
|
|
520
|
+
) -> None:
|
|
521
|
+
# A return that says nothing about its keys may fill any read, so
|
|
522
|
+
# dispatch demands none of them of the caller and the help asks for none.
|
|
523
|
+
def may_fill(**_: Any) -> dict[str, Any]:
|
|
524
|
+
return {"project": 1}
|
|
525
|
+
|
|
526
|
+
spec = echo_spec(Parameter("project", "integer", required=True), kwargs=may_fill)
|
|
527
|
+
command = command_over(registry_of(echo=spec))
|
|
528
|
+
text = " ".join(help_of(capsys, "echo", command=command).split())
|
|
529
|
+
assert "--project INTEGER" in text
|
|
530
|
+
assert "(required)" not in text
|
|
531
|
+
assert lines(run("echo", "--as-user", str(owner.pk), command=command)) == [{"project": 1}]
|
|
532
|
+
|
|
533
|
+
def test_a_read_a_provider_requires_itself_is_required(
|
|
534
|
+
self, capsys: pytest.CaptureFixture[str], owner: Any
|
|
535
|
+
) -> None:
|
|
536
|
+
# A provider that takes the read by name, with no default, cannot be the
|
|
537
|
+
# one to fill it, so dispatch demands it of the caller. A kernel before
|
|
538
|
+
# 0.25.0 relaxed it, and a run leaving it out crashed in the provider.
|
|
539
|
+
def needs_project(*, project: int) -> dict[str, Any]:
|
|
540
|
+
return {"project": project}
|
|
541
|
+
|
|
542
|
+
spec = echo_spec(Parameter("project", "integer", required=True), kwargs=needs_project)
|
|
543
|
+
command = command_over(registry_of(echo=spec))
|
|
544
|
+
text = " ".join(help_of(capsys, "echo", command=command).split())
|
|
545
|
+
assert "--project INTEGER (required)" in text
|
|
546
|
+
error = refused("echo", "--as-user", str(owner.pk), command=command)
|
|
547
|
+
assert error.returncode == 3
|
|
548
|
+
assert str(error).splitlines()[1:] == ["project: This field is required."]
|
|
549
|
+
|
|
431
550
|
|
|
432
551
|
# --- output --------------------------------------------------------------------------------------
|
|
433
552
|
|
|
@@ -490,7 +490,7 @@ docs = [
|
|
|
490
490
|
[package.metadata]
|
|
491
491
|
requires-dist = [
|
|
492
492
|
{ name = "django", specifier = ">=4.2" },
|
|
493
|
-
{ name = "django-service-specs", specifier = ">=0.
|
|
493
|
+
{ name = "django-service-specs", specifier = ">=0.25.0" },
|
|
494
494
|
]
|
|
495
495
|
|
|
496
496
|
[package.metadata.requires-dev]
|
|
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
|
|
File without changes
|
|
File without changes
|
{django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/django_spec_cli/argv/parameter_descriptor.py
RENAMED
|
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
|
|
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
|
|
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
|
{django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/commands_app/management/commands/__init__.py
RENAMED
|
File without changes
|
{django_spec_cli-0.3.0 → django_spec_cli-0.4.0}/tests/commands_app/management/commands/notes.py
RENAMED
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|