django-spec-cli 0.4.0__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/CHANGELOG.md +13 -1
  2. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/PKG-INFO +1 -1
  3. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/version.py +1 -1
  4. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/docs/examples/commands.py +30 -1
  5. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/docs/index.md +18 -1
  6. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/pyproject.toml +1 -1
  7. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/docs/test_commands_examples.py +56 -0
  8. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/.github/CODEOWNERS +0 -0
  9. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/.github/SECURITY.md +0 -0
  10. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/.github/dependabot.yml +0 -0
  11. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/.github/workflows/release.yml +0 -0
  12. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/.github/workflows/tests.yml +0 -0
  13. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/.github/workflows/upstream-drift.yml +0 -0
  14. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/.gitignore +0 -0
  15. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/.pre-commit-config.yaml +0 -0
  16. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/CLAUDE.md +0 -0
  17. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/LICENSE +0 -0
  18. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/Makefile +0 -0
  19. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/README.md +0 -0
  20. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/__init__.py +0 -0
  21. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/apps.py +0 -0
  22. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/argv/__init__.py +0 -0
  23. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/argv/add_spec_arguments.py +0 -0
  24. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/argv/describe_parameters.py +0 -0
  25. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/argv/parameter_descriptor.py +0 -0
  26. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/argv/parsed_arguments.py +0 -0
  27. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/argv/utils.py +0 -0
  28. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/checks.py +0 -0
  29. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/exit_code_for.py +0 -0
  30. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/py.typed +0 -0
  31. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/render/__init__.py +0 -0
  32. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/render/render_json_lines.py +0 -0
  33. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/render/render_result.py +0 -0
  34. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/render/render_table.py +0 -0
  35. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/render/utils.py +0 -0
  36. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/render/wants_table.py +0 -0
  37. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/report/__init__.py +0 -0
  38. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/report/unguarded_message.py +0 -0
  39. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/report/utils.py +0 -0
  40. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/settings.py +0 -0
  41. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/spec_command.py +0 -0
  42. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/types/__init__.py +0 -0
  43. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/django_spec_cli/types/exit_code.py +0 -0
  44. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/docs/__init__.py +0 -0
  45. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/docs/examples/__init__.py +0 -0
  46. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/docs/reference.md +0 -0
  47. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/mkdocs.yml +0 -0
  48. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/scripts/check-changelog +0 -0
  49. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/scripts/release-publish.sh +0 -0
  50. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/__init__.py +0 -0
  51. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/argv/__init__.py +0 -0
  52. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/argv/test_add_spec_arguments.py +0 -0
  53. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/argv/test_describe_parameters.py +0 -0
  54. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/argv/test_parameter_descriptor.py +0 -0
  55. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/argv/test_parsed_arguments.py +0 -0
  56. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/argv/test_utils.py +0 -0
  57. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/argv/utils.py +0 -0
  58. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/commands_app/__init__.py +0 -0
  59. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/commands_app/apps.py +0 -0
  60. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/commands_app/management/__init__.py +0 -0
  61. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/commands_app/management/commands/__init__.py +0 -0
  62. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/commands_app/management/commands/notes.py +0 -0
  63. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/commands_app/management/commands/unguarded_bad_parser.py +0 -0
  64. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/commands_app/management/commands/unguarded_broken_import.py +0 -0
  65. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/commands_app/management/commands/unguarded_broken_init.py +0 -0
  66. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/commands_app/management/commands/unguarded_empty.py +0 -0
  67. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/commands_app/management/commands/unguarded_no_registry.py +0 -0
  68. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/commands_app/management/commands/unguarded_plain.py +0 -0
  69. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/commands_app/management/commands/unguarded_report.py +0 -0
  70. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/commands_app/unguarded_registries.py +0 -0
  71. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/conftest.py +0 -0
  72. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/conftest_settings.py +0 -0
  73. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/dispatch_app/__init__.py +0 -0
  74. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/dispatch_app/apps.py +0 -0
  75. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/dispatch_app/models.py +0 -0
  76. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/docs/__init__.py +0 -0
  77. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/docs/test_reference.py +0 -0
  78. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/render/__init__.py +0 -0
  79. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/render/test_render_json_lines.py +0 -0
  80. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/render/test_render_result.py +0 -0
  81. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/render/test_render_table.py +0 -0
  82. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/render/test_utils.py +0 -0
  83. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/render/test_wants_table.py +0 -0
  84. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/render/utils.py +0 -0
  85. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/report/__init__.py +0 -0
  86. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/report/test_unguarded_message.py +0 -0
  87. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/report/test_utils.py +0 -0
  88. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/test_apps.py +0 -0
  89. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/test_checks.py +0 -0
  90. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/test_exit_code_for.py +0 -0
  91. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/test_init.py +0 -0
  92. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/test_settings.py +0 -0
  93. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/test_spec_command.py +0 -0
  94. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/types/__init__.py +0 -0
  95. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/types/test_exit_code.py +0 -0
  96. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/tests/utils.py +0 -0
  97. {django_spec_cli-0.4.0 → django_spec_cli-0.4.1}/uv.lock +0 -0
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.1] — 2026-11-04
11
+
12
+ ### Added
13
+ - **The command line page shows a spec the kernel refuses.** Its system-check
14
+ section now includes an example command, `drafts`, serving one spec that
15
+ declares a plain `Parameter` over a keyword with no default, and prints the
16
+ `django_spec_cli.W004` sentence it gets. The page's tests assert that
17
+ `manage.py check` reports exactly that warning for the example and that a
18
+ run of the spec raises the same sentence, so the page cannot drift from
19
+ what the check and the command say. No change to the package.
20
+
10
21
  ## [0.4.0] — 2026-11-04
11
22
 
12
23
  ### Added
@@ -124,7 +135,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
124
135
  `django-service-specs>=0.10.0`, the first kernel release with every name the
125
136
  command reads at its root.
126
137
 
127
- [Unreleased]: https://github.com/Artui/django-spec-cli/compare/v0.4.0...HEAD
138
+ [Unreleased]: https://github.com/Artui/django-spec-cli/compare/v0.4.1...HEAD
139
+ [0.4.1]: https://github.com/Artui/django-spec-cli/compare/v0.4.0...v0.4.1
128
140
  [0.4.0]: https://github.com/Artui/django-spec-cli/compare/v0.3.1...v0.4.0
129
141
  [0.3.1]: https://github.com/Artui/django-spec-cli/compare/v0.3.0...v0.3.1
130
142
  [0.3.0]: https://github.com/Artui/django-spec-cli/compare/v0.2.1...v0.3.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: django-spec-cli
3
- Version: 0.4.0
3
+ Version: 0.4.1
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__: str = "0.4.0"
5
+ __version__: str = "0.4.1"
6
6
 
7
7
  __all__ = ["__version__"]
@@ -1,4 +1,5 @@
1
- """A registry served as a management command, and a command that looks its user up by username.
1
+ """A registry served as a management command, a command that looks its user up by username, and
2
+ a command serving a spec the kernel refuses as declared.
2
3
 
3
4
  ``tests/docs/test_commands_examples.py`` runs every command line
4
5
  ``docs/index.md`` shows through this module's ``Command``, from argv as
@@ -137,3 +138,31 @@ class ByUsername(SpecCommand):
137
138
 
138
139
 
139
140
  # --8<-- [end:by_username]
141
+
142
+
143
+ # --8<-- [start:refused]
144
+ def retitle(*, title: str) -> str:
145
+ return title
146
+
147
+
148
+ drafts = SpecRegistry()
149
+ drafts.register(
150
+ "retitle",
151
+ ServiceSpec(
152
+ service=retitle,
153
+ permissions=[IsSignedIn()],
154
+ # Optional, as a Parameter is unless it says otherwise, over a keyword
155
+ # with no default: a call leaving it out could not run, so the kernel's
156
+ # check_seeds refuses the spec, and manage.py check reports it as
157
+ # django_spec_cli.W004.
158
+ reads=Parameters.of(Parameter("title", "string")),
159
+ ),
160
+ )
161
+
162
+
163
+ # yourapp/management/commands/drafts.py
164
+ class Drafts(SpecCommand):
165
+ registry = drafts
166
+
167
+
168
+ # --8<-- [end:refused]
@@ -264,6 +264,23 @@ 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
+ A `Parameter` is optional unless it says `required=True`, so a read declared
268
+ plainly over a keyword with no default is one such declaration:
269
+
270
+ ```python
271
+ --8<--
272
+ docs/examples/commands.py:refused
273
+ --8<--
274
+ ```
275
+
276
+ `manage.py check` reports it as `django_spec_cli.W004`, and
277
+ `manage.py drafts retitle --as-user 1 --title Final` is refused with the same
278
+ sentence:
279
+
280
+ ```text
281
+ 'retitle' in manage.py drafts cannot run as declared, and a run of it is refused: ServiceSpec.service requires the keyword(s) ['title'], declared as optional reads that nothing else fills, so a call leaving one out is refused as missing. Declare the read required, or give the keyword a default.
282
+ ```
283
+
267
284
  All three are warnings, so a spec nobody runs never stops `migrate`, and
268
285
  `SILENCED_SYSTEM_CHECKS` silences one by id. Silencing `W004` hides the report
269
286
  and does not let the spec run.
@@ -275,7 +292,7 @@ listing only `"django_service_specs"` gets no check for a command. List
275
292
  A `SpecCommand` keeps Django's system checks on, so on the command line the
276
293
  report is printed to stderr before every run of every command. The runs on
277
294
  this page are shown without it; in a project listing the app, each would be
278
- preceded by the warning for `whoami`. `call_command` skips
295
+ preceded by the warnings for `whoami` and `retitle`. `call_command` skips
279
296
  system checks for every command but `check`, so a command run through it
280
297
  does not print them.
281
298
 
@@ -149,7 +149,7 @@ skip-magic-trailing-comma = false
149
149
  root = ["./django_spec_cli"]
150
150
 
151
151
  [tool.bumpversion]
152
- current_version = "0.4.0"
152
+ current_version = "0.4.1"
153
153
  allow_dirty = false
154
154
  commit = false
155
155
  tag = false
@@ -7,6 +7,10 @@
7
7
  shows them - must be the lines printed under it, and its exit status the
8
8
  number an ``echo $?`` after it shows. So the page cannot show an option, an
9
9
  output or an exit code the command does not have.
10
+
11
+ The sentence the page prints for the example's ``Drafts``, whose spec the
12
+ kernel refuses, must be the ``django_spec_cli.W004`` the system check reports
13
+ and the refusal a run of the spec raises.
10
14
  """
11
15
 
12
16
  from __future__ import annotations
@@ -18,10 +22,13 @@ from pathlib import Path
18
22
  from typing import Any
19
23
 
20
24
  import pytest
25
+ from django.core.checks import Warning
26
+ from django.core.exceptions import ImproperlyConfigured
21
27
  from django.core.management import call_command
22
28
  from django.core.management.base import CommandError
23
29
  from django_service_specs import UnguardedReason, UnguardedSpec
24
30
 
31
+ from django_spec_cli.checks import check_unguarded_specs
25
32
  from django_spec_cli.report.unguarded_message import unguarded_message
26
33
  from docs.examples import commands
27
34
  from tests.dispatch_app.models import Note
@@ -30,6 +37,7 @@ pytestmark = [pytest.mark.django_db, pytest.mark.usefixtures("keep_the_test_conn
30
37
 
31
38
  PAGE = Path(__file__).resolve().parents[2] / "docs" / "index.md"
32
39
  PROMPT = "$ python manage.py notes "
40
+ RETITLE_ARGV = "retitle --as-user 1 --title Final"
33
41
 
34
42
 
35
43
  def _runs() -> list[tuple[str, list[str], int]]:
@@ -112,6 +120,54 @@ def test_the_username_lookup_runs_as_the_user_it_names() -> None:
112
120
  assert (caught.value.returncode, str(caught.value)) == (5, "No user named 'nobody'.")
113
121
 
114
122
 
123
+ def _refused_line() -> str:
124
+ """The line the page prints for the W004 example: the one opening with its spec and command.
125
+
126
+ Found by that opening rather than by its block, so another ``text`` block
127
+ on the page leaves it alone, and a page no longer printing it fails here.
128
+ """
129
+ (line,) = [
130
+ line
131
+ for line in PAGE.read_text().splitlines()
132
+ if line.startswith("'retitle' in manage.py drafts ")
133
+ ]
134
+ return line
135
+
136
+
137
+ def test_the_refused_spec_is_reported_as_w004_with_the_page_s_sentence(
138
+ monkeypatch: pytest.MonkeyPatch,
139
+ ) -> None:
140
+ # The examples are no installed app, so the walk is pointed at the one
141
+ # command; it would otherwise never load it, and no other test's check
142
+ # sees this command.
143
+ monkeypatch.setattr("django_spec_cli.checks.get_commands", lambda: {"drafts": "yourapp"})
144
+ monkeypatch.setattr(
145
+ "django_spec_cli.checks.load_command_class", lambda app_name, name: commands.Drafts()
146
+ )
147
+ assert check_unguarded_specs(None) == [
148
+ Warning(
149
+ _refused_line(),
150
+ hint=(
151
+ "Change the declaration as the message says. Until then a run of the spec "
152
+ "is refused, whether or not django_spec_cli.W004 is silenced."
153
+ ),
154
+ obj=commands.Drafts,
155
+ id="django_spec_cli.W004",
156
+ )
157
+ ]
158
+
159
+
160
+ @pytest.mark.usefixtures("world")
161
+ def test_a_run_of_the_refused_spec_raises_the_page_s_sentence() -> None:
162
+ # The run the page names, which sends every option the spec declares, and
163
+ # is still refused.
164
+ assert f"`manage.py drafts {RETITLE_ARGV}`" in PAGE.read_text()
165
+ command = commands.Drafts()
166
+ with pytest.raises(ImproperlyConfigured) as caught:
167
+ command.run_from_argv(["manage.py", "drafts", "--skip-checks", *shlex.split(RETITLE_ARGV)])
168
+ assert str(caught.value) == _refused_line()
169
+
170
+
115
171
  @pytest.mark.usefixtures("world")
116
172
  def test_whoami_is_the_user_named(capsys: pytest.CaptureFixture[str]) -> None:
117
173
  assert _run_from_argv("whoami --as-user 1") == 0
File without changes
File without changes