django-spec-cli 0.1.0__tar.gz → 0.2.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.
Files changed (98) hide show
  1. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/CHANGELOG.md +16 -1
  2. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/CLAUDE.md +6 -17
  3. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/PKG-INFO +6 -10
  4. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/README.md +4 -8
  5. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/version.py +1 -1
  6. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/docs/index.md +3 -9
  7. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/pyproject.toml +9 -6
  8. django_spec_cli-0.2.0/tests/conftest.py +19 -0
  9. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/conftest_settings.py +1 -3
  10. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/test_checks.py +7 -30
  11. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/uv.lock +4 -4
  12. django_spec_cli-0.1.0/tests/conftest.py +0 -103
  13. django_spec_cli-0.1.0/tests/test_conftest.py +0 -63
  14. django_spec_cli-0.1.0/tests/test_kernel_copy_references.py +0 -213
  15. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/.github/CODEOWNERS +0 -0
  16. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/.github/SECURITY.md +0 -0
  17. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/.github/dependabot.yml +0 -0
  18. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/.github/workflows/release.yml +0 -0
  19. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/.github/workflows/tests.yml +0 -0
  20. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/.github/workflows/upstream-drift.yml +0 -0
  21. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/.gitignore +0 -0
  22. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/.pre-commit-config.yaml +0 -0
  23. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/LICENSE +0 -0
  24. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/Makefile +0 -0
  25. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/__init__.py +0 -0
  26. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/apps.py +0 -0
  27. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/argv/__init__.py +0 -0
  28. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/argv/add_spec_arguments.py +0 -0
  29. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/argv/describe_parameters.py +0 -0
  30. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/argv/parameter_descriptor.py +0 -0
  31. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/argv/parsed_arguments.py +0 -0
  32. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/argv/utils.py +0 -0
  33. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/checks.py +0 -0
  34. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/exit_code_for.py +0 -0
  35. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/py.typed +0 -0
  36. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/render/__init__.py +0 -0
  37. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/render/render_json_lines.py +0 -0
  38. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/render/render_result.py +0 -0
  39. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/render/render_table.py +0 -0
  40. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/render/utils.py +0 -0
  41. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/render/wants_table.py +0 -0
  42. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/report/__init__.py +0 -0
  43. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/report/unguarded_message.py +0 -0
  44. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/settings.py +0 -0
  45. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/spec_command.py +0 -0
  46. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/types/__init__.py +0 -0
  47. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/django_spec_cli/types/exit_code.py +0 -0
  48. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/docs/__init__.py +0 -0
  49. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/docs/examples/__init__.py +0 -0
  50. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/docs/examples/commands.py +0 -0
  51. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/docs/reference.md +0 -0
  52. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/mkdocs.yml +0 -0
  53. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/scripts/check-changelog +0 -0
  54. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/scripts/release-publish.sh +0 -0
  55. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/__init__.py +0 -0
  56. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/argv/__init__.py +0 -0
  57. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/argv/test_add_spec_arguments.py +0 -0
  58. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/argv/test_describe_parameters.py +0 -0
  59. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/argv/test_parameter_descriptor.py +0 -0
  60. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/argv/test_parsed_arguments.py +0 -0
  61. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/argv/test_utils.py +0 -0
  62. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/argv/utils.py +0 -0
  63. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/commands_app/__init__.py +0 -0
  64. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/commands_app/apps.py +0 -0
  65. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/commands_app/management/__init__.py +0 -0
  66. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/commands_app/management/commands/__init__.py +0 -0
  67. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/commands_app/management/commands/notes.py +0 -0
  68. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/commands_app/management/commands/unguarded_bad_parser.py +0 -0
  69. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/commands_app/management/commands/unguarded_broken_import.py +0 -0
  70. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/commands_app/management/commands/unguarded_broken_init.py +0 -0
  71. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/commands_app/management/commands/unguarded_empty.py +0 -0
  72. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/commands_app/management/commands/unguarded_no_registry.py +0 -0
  73. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/commands_app/management/commands/unguarded_plain.py +0 -0
  74. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/commands_app/management/commands/unguarded_report.py +0 -0
  75. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/commands_app/unguarded_registries.py +0 -0
  76. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/dispatch_app/__init__.py +0 -0
  77. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/dispatch_app/apps.py +0 -0
  78. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/dispatch_app/models.py +0 -0
  79. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/docs/__init__.py +0 -0
  80. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/docs/test_commands_examples.py +0 -0
  81. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/docs/test_reference.py +0 -0
  82. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/render/__init__.py +0 -0
  83. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/render/test_render_json_lines.py +0 -0
  84. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/render/test_render_result.py +0 -0
  85. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/render/test_render_table.py +0 -0
  86. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/render/test_utils.py +0 -0
  87. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/render/test_wants_table.py +0 -0
  88. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/render/utils.py +0 -0
  89. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/report/__init__.py +0 -0
  90. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/report/test_unguarded_message.py +0 -0
  91. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/test_apps.py +0 -0
  92. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/test_exit_code_for.py +0 -0
  93. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/test_init.py +0 -0
  94. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/test_settings.py +0 -0
  95. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/test_spec_command.py +0 -0
  96. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/types/__init__.py +0 -0
  97. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/types/test_exit_code.py +0 -0
  98. {django_spec_cli-0.1.0 → django_spec_cli-0.2.0}/tests/utils.py +0 -0
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] — 2026-10-20
11
+
12
+ ### Changed
13
+ - **The kernel floor is `django-service-specs>=0.11.0`, the first kernel
14
+ release without its own copy of the command.** Below it the kernel still
15
+ shipped `django_service_specs.commands` and registered its own `W001` and
16
+ `W003` over its own `SpecCommand`, so a project listing both
17
+ `"django_service_specs"` and `"django_spec_cli"` saw each id for the
18
+ commands subclassing its own class, and a command that could not be loaded
19
+ was reported under both. From 0.11.0 the kernel's app registers no check, so
20
+ a project that still lists `"django_service_specs"` sees nothing from it, and
21
+ the checks are reported once, under `django_spec_cli.`, by listing
22
+ `"django_spec_cli"`.
23
+
10
24
  ## [0.1.0] — 2026-10-20
11
25
 
12
26
  ### Added
@@ -31,5 +45,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
31
45
  `django-service-specs>=0.10.0`, the first kernel release with every name the
32
46
  command reads at its root.
33
47
 
34
- [Unreleased]: https://github.com/Artui/django-spec-cli/compare/v0.1.0...HEAD
48
+ [Unreleased]: https://github.com/Artui/django-spec-cli/compare/v0.2.0...HEAD
49
+ [0.2.0]: https://github.com/Artui/django-spec-cli/compare/v0.1.0...v0.2.0
35
50
  [0.1.0]: https://github.com/Artui/django-spec-cli/compare/v0.0.0...v0.1.0
@@ -12,8 +12,8 @@ refusal. Two system checks, `django_spec_cli.W001` and `W003`, report a served
12
12
  spec that authorizes nothing and a command that cannot be loaded.
13
13
 
14
14
  It is the kernel's `commands/` subpackage, moved out with its history so that
15
- the kernel can carry no transport. Kernel 0.10.x still ships its own copy, until
16
- a later kernel release removes it. Two positions follow from the move:
15
+ the kernel can carry no transport, and the kernel carries no copy of it from
16
+ 0.11.0. Two positions follow from the move:
17
17
 
18
18
  - **The kernel is read through its root and nothing else.** Every kernel name
19
19
  this package uses is in `django_service_specs.__all__`, and every import of
@@ -84,20 +84,9 @@ Each of these reads as an oversight and is not.
84
84
  - **The checks are registered by this package's own `AppConfig`.** A project
85
85
  lists `"django_spec_cli"` in `INSTALLED_APPS` to get `W001` and `W003`. The
86
86
  kernel keeps the registry fact the check reads, `unguarded_specs`, because
87
- another transport can ask the same question of a registry.
88
- - **Until the kernel removes its own copy of the command, both copies exist.**
89
- Kernel 0.10.x still ships `commands/` and registers `django_service_specs.W001`
90
- over its own `SpecCommand`, and its root re-exports every moved name, so a
91
- missed import rewrite resolves against the kernel's copy and passes while
92
- testing the wrong code. The suite cannot keep `django_service_specs.commands`
93
- out of `sys.modules`, since the kernel's root imports it. Two guards hold the
94
- line instead. `tests/test_kernel_copy_references.py` parses every module and
95
- every python block in the docs and fails on any reference through
96
- `django_service_specs` to a name this package also exports, to
97
- `check_unguarded_specs` or `command_output`, or to a moved module. The
98
- session-finish hook in `tests/conftest.py` fails the run if the kernel's
99
- `checks` or `apps` was imported, or if a module of this repository holds an
100
- object from the kernel's copy.
87
+ another transport can ask the same question of a registry. The kernel's own
88
+ app stays importable and registers nothing, so listing only it gets no check
89
+ (`tests/test_checks.py`, `test_the_kernel_s_app_registers_no_check_of_its_own`).
101
90
  - **The test settings list no kernel app.** The package needs nothing of the
102
91
  kernel switched on, and the suite shows it.
103
92
 
@@ -148,7 +137,7 @@ configured and enforced. isort order is stdlib, third party, first party.
148
137
  | --- | --- | --- |
149
138
  | Python | 3.10 | 3.10 through 3.14 |
150
139
  | Django | 4.2 | 4.2, 5.0, 5.1, 5.2, 6.0, 6.1 |
151
- | django-service-specs | 0.10.0 | the newest release, and the floor in the `floor` job |
140
+ | django-service-specs | 0.11.0 | the newest release, and the floor in the `floor` job |
152
141
 
153
142
  The Python and Django floors are the kernel's, and cannot sit below them.
154
143
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: django-spec-cli
3
- Version: 0.1.0
3
+ Version: 0.2.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.10.0
33
+ Requires-Dist: django-service-specs>=0.11.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.10.0 or
64
+ Python 3.10 or later, Django 4.2 or later, and `django-service-specs` 0.11.0 or
65
65
  later, which it installs.
66
66
 
67
67
  ## Quickstart
@@ -96,13 +96,9 @@ every open spec a `SpecCommand` serves as `django_spec_cli.W001`, and a command
96
96
  that could not be loaded as `django_spec_cli.W003`. The command itself needs no
97
97
  app listed.
98
98
 
99
- Until the kernel removes its own copy of the command, kernel 0.10.x still
100
- registers `django_service_specs.W001` and `W003` over its own `SpecCommand`. A
101
- project listing both `"django_service_specs"` and `"django_spec_cli"` sees both
102
- ids: each walk reports the commands subclassing its own class, and a command
103
- that cannot be loaded is reported by each. A project that moves its imports
104
- here and lists only `"django_service_specs"` gets no check for the moved
105
- commands, so list `"django_spec_cli"`.
99
+ The kernel keeps no check of its own: its app registers nothing, so a project
100
+ listing only `"django_service_specs"` gets no check for a command. List
101
+ `"django_spec_cli"`.
106
102
 
107
103
  ## Documentation
108
104
 
@@ -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.10.0 or
28
+ Python 3.10 or later, Django 4.2 or later, and `django-service-specs` 0.11.0 or
29
29
  later, which it installs.
30
30
 
31
31
  ## Quickstart
@@ -60,13 +60,9 @@ every open spec a `SpecCommand` serves as `django_spec_cli.W001`, and a command
60
60
  that could not be loaded as `django_spec_cli.W003`. The command itself needs no
61
61
  app listed.
62
62
 
63
- Until the kernel removes its own copy of the command, kernel 0.10.x still
64
- registers `django_service_specs.W001` and `W003` over its own `SpecCommand`. A
65
- project listing both `"django_service_specs"` and `"django_spec_cli"` sees both
66
- ids: each walk reports the commands subclassing its own class, and a command
67
- that cannot be loaded is reported by each. A project that moves its imports
68
- here and lists only `"django_service_specs"` gets no check for the moved
69
- commands, so list `"django_spec_cli"`.
63
+ The kernel keeps no check of its own: its app registers nothing, so a project
64
+ listing only `"django_service_specs"` gets no check for a command. List
65
+ `"django_spec_cli"`.
70
66
 
71
67
  ## Documentation
72
68
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__: str = "0.1.0"
5
+ __version__: str = "0.2.0"
6
6
 
7
7
  __all__ = ["__version__"]
@@ -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.10.0, on Python 3.10 and Django 4.2 or later. The command line was the
30
+ from 0.11.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
 
@@ -247,14 +247,8 @@ be loaded to be checked as `django_spec_cli.W003`. There is no
247
247
  none reaches a registry. Both are warnings, and `SILENCED_SYSTEM_CHECKS`
248
248
  silences one by id.
249
249
 
250
- Until the kernel removes its own copy of the command, kernel 0.10.x still
251
- registers `django_service_specs.W001` and `W003`, over its own `SpecCommand`
252
- class rather than this package's. A project that lists both
253
- `"django_service_specs"` and `"django_spec_cli"` sees both ids, the kernel's
254
- for commands still subclassing its copy and this package's for commands
255
- subclassing `django_spec_cli.SpecCommand`, and a command that cannot be loaded
256
- gets a `W003` from each. A project that moves its imports here and lists only
257
- `"django_service_specs"` gets no check for the moved commands at all, so list
250
+ The kernel keeps no check of its own: its app registers nothing, so a project
251
+ listing only `"django_service_specs"` gets no check for a command. List
258
252
  `"django_spec_cli"`.
259
253
 
260
254
  A `SpecCommand` keeps Django's system checks on, so on the command line the
@@ -40,11 +40,14 @@ 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 with ``relax_partial``, ``refusal_messages`` and
44
- # ``unguarded_specs`` at its root. A separate distribution reads only the
45
- # kernel's root surface, and below 0.10.0 those names are private or live
46
- # elsewhere.
47
- "django-service-specs>=0.10.0",
43
+ # The first kernel release without its own copy of the command. A separate
44
+ # distribution reads only the kernel's root surface, and below 0.11.0 the
45
+ # kernel still ships ``commands/``, registers its own ``W001`` and ``W003``
46
+ # and re-exports every moved name, so a project listing both apps sees two
47
+ # sets of ids and a missed import rewrite resolves against the wrong code.
48
+ # ``relax_partial``, ``refusal_messages`` and ``unguarded_specs`` are at its
49
+ # root from 0.10.0.
50
+ "django-service-specs>=0.11.0",
48
51
  ]
49
52
 
50
53
  [project.urls]
@@ -144,7 +147,7 @@ skip-magic-trailing-comma = false
144
147
  root = ["./django_spec_cli"]
145
148
 
146
149
  [tool.bumpversion]
147
- current_version = "0.1.0"
150
+ current_version = "0.2.0"
148
151
  allow_dirty = false
149
152
  commit = false
150
153
  tag = false
@@ -0,0 +1,19 @@
1
+ """Fixtures the suite shares.
2
+
3
+ ``keep_the_test_connection`` is the one: two modules call ``run_from_argv`` and
4
+ need it.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import pytest
10
+
11
+
12
+ @pytest.fixture
13
+ def keep_the_test_connection(monkeypatch: pytest.MonkeyPatch) -> None:
14
+ """For a test calling ``run_from_argv``, which closes every connection when it ends.
15
+
16
+ That would close the one the test's transaction is on. Opt-in rather than
17
+ autouse, so no other test runs with ``close_all`` patched out.
18
+ """
19
+ monkeypatch.setattr("django.db.connections.close_all", lambda: None)
@@ -3,9 +3,7 @@
3
3
  SQLite, and no DRF anywhere: the kernel this package serves exists for
4
4
  dispatch without it, and a suite that could import it could not notice either
5
5
  package reaching for it. No kernel app is listed either: the command needs
6
- nothing of the kernel switched on, and with the kernel's app out its own copy
7
- of the system check never runs beside this package's. The two contrib apps are
8
- the ones a spec's permission check and principal resolution reach, since a
6
+ nothing of the kernel switched on. The two contrib apps are the ones a spec's permission check and principal resolution reach, since a
9
7
  principal is a Django user.
10
8
 
11
9
  The test database is a **file**, not ``:memory:``. An in-memory SQLite database
@@ -285,35 +285,12 @@ execute_from_command_line(["manage.py", "unguarded_plain"])
285
285
  completed.stderr
286
286
  )
287
287
 
288
-
289
- class TestBesideTheKernelsCopy:
290
- """Kernel 0.10.x still ships the command and registers its own check over its own class.
291
-
292
- A fresh interpreter each, listing the kernel's app, which the suite's own
293
- settings never do. When the kernel's copy goes, these fail, and the page's
294
- paragraph on running both goes with them.
295
- """
296
-
297
- def test_listing_both_apps_reports_each_copy_s_commands_under_its_own_id(self) -> None:
298
- completed = run_project([*BASE_APPS, "django_service_specs", "django_spec_cli"], CHECK)
299
- assert completed.returncode == 0, completed.stderr
300
- # The commands here subclass this package's class, which the kernel's
301
- # walk does not recognise, so their open spec is reported once.
302
- assert "(django_spec_cli.W001) 'open_service' in manage.py unguarded_report" in (
303
- completed.stdout
304
- )
305
- assert "django_service_specs.W001" not in completed.stdout
306
- # Both walks load every command, so one that cannot be loaded is
307
- # reported by each.
308
- for check in ("django_spec_cli.W003", "django_service_specs.W003"):
309
- assert f"({check}) The command 'unguarded_broken_import'" in completed.stdout
310
-
311
- def test_listing_only_the_kernel_s_app_checks_no_moved_command(self) -> None:
288
+ def test_the_kernel_s_app_registers_no_check_of_its_own(self) -> None:
289
+ # The kernel keeps an ``AppConfig`` so a project listing it still
290
+ # starts, but it registers nothing: the checks live here, so listing
291
+ # only the kernel's app reports no command, moved or not.
312
292
  completed = run_project([*BASE_APPS, "django_service_specs"], CHECK)
313
293
  assert completed.returncode == 0, completed.stderr
314
- assert "authorizes nothing" not in completed.stdout
315
- assert "django_spec_cli." not in completed.stdout
316
- # Not a probe that ran no check: the kernel's own walk did run.
317
- assert "(django_service_specs.W003) The command 'unguarded_broken_import'" in (
318
- completed.stdout
319
- )
294
+ # The one assertion that holds the claim: on a kernel still carrying its
295
+ # copy, this fails on the kernel's W003 for the unloadable command.
296
+ assert "django_service_specs." not in completed.stdout
@@ -438,16 +438,16 @@ wheels = [
438
438
 
439
439
  [[package]]
440
440
  name = "django-service-specs"
441
- version = "0.10.0"
441
+ version = "0.11.0"
442
442
  source = { registry = "https://pypi.org/simple" }
443
443
  dependencies = [
444
444
  { name = "django", version = "5.2.18", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
445
445
  { name = "django", version = "6.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
446
446
  { name = "typing-extensions" },
447
447
  ]
448
- sdist = { url = "https://files.pythonhosted.org/packages/b5/b3/d56cbc38f2cef7cb81e6561e5c31d18a19e2be79b57e87a66287976bf4c0/django_service_specs-0.10.0.tar.gz", hash = "sha256:12cbb3dc75320a6d1d7673512eda94df6bc0beef0212a726e3dba722b6cbab69", size = 1005273, upload-time = "2026-10-20T13:21:24.558Z" }
448
+ sdist = { url = "https://files.pythonhosted.org/packages/4d/cc/edf2c86492b44d4a313978440f510a2faeed85da86833c1418db98e54dc2/django_service_specs-0.11.0.tar.gz", hash = "sha256:926fa982735696153dc0ce3238c42c10c95a30b51c7a274b4aea0e55114ee0a3", size = 951537, upload-time = "2026-10-20T19:28:24.563Z" }
449
449
  wheels = [
450
- { url = "https://files.pythonhosted.org/packages/5a/dd/47306f1c33108b881c9bf74266383f71d5a2e97eea87c09870c58890fbd1/django_service_specs-0.10.0-py3-none-any.whl", hash = "sha256:0a4e1f5824ddff85345628f3fd289a55425e2463721a437d1c10b79e45ecb4e4", size = 463139, upload-time = "2026-10-20T13:21:22.442Z" },
450
+ { url = "https://files.pythonhosted.org/packages/67/01/34ed8dc96613e5aff52058c659359a315186e38711c81145c89f195cdbe3/django_service_specs-0.11.0-py3-none-any.whl", hash = "sha256:a4ddc9ff79ec3df475dcef2a04b1eebee98c37f7775d116e5ccb28fbe232aeaa", size = 432549, upload-time = "2026-10-20T19:28:22.69Z" },
451
451
  ]
452
452
 
453
453
  [package.optional-dependencies]
@@ -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.10.0" },
493
+ { name = "django-service-specs", specifier = ">=0.11.0" },
494
494
  ]
495
495
 
496
496
  [package.metadata.requires-dev]
@@ -1,103 +0,0 @@
1
- """Suite-wide hooks: the guard that the suite tests this package and not the kernel's copy.
2
-
3
- It also holds the one fixture two modules share, ``keep_the_test_connection``.
4
-
5
- Kernel 0.10.x still ships the command half this package was moved out of:
6
- ``django_service_specs.commands``, ``types/exit_code.py``, ``checks.py`` and
7
- ``apps.py``, and its root re-exports every moving name. A missed import rewrite
8
- therefore still resolves, against the kernel's copy, and a test passes while
9
- testing the wrong code. Nothing at import fails, so the guard reads what the run
10
- actually loaded once it is over.
11
-
12
- It cannot ask that ``django_service_specs.commands`` stay out of ``sys.modules``:
13
- the kernel's root imports it, and every kernel name this package reads comes
14
- through the root. It asks the two questions that do separate the copies:
15
-
16
- - the kernel's ``checks`` and ``apps`` are never imported, which nothing does
17
- while the test settings list no kernel app, except a ``monkeypatch`` string
18
- or an import still naming them;
19
- - no module of this package, of the suite or of the docs examples holds an
20
- object the kernel's copy defines: a module, class or function from one of the
21
- moving kernel modules, or the kernel's ``command_output``, which lives in a
22
- module that does not move.
23
- """
24
-
25
- from __future__ import annotations
26
-
27
- import sys
28
- from types import ModuleType
29
- from typing import Any
30
-
31
- import pytest
32
-
33
- _KERNEL_COPY = (
34
- "django_service_specs.commands",
35
- "django_service_specs.types.exit_code",
36
- "django_service_specs.checks",
37
- "django_service_specs.apps",
38
- )
39
- _NEVER_IMPORTED = ("django_service_specs.checks", "django_service_specs.apps")
40
- _OURS = ("django_spec_cli", "tests", "docs")
41
-
42
-
43
- def _within(name: str, packages: tuple[str, ...]) -> bool:
44
- return any(name == package or name.startswith(f"{package}.") for package in packages)
45
-
46
-
47
- def _kernel_copy_of(value: Any) -> str | None:
48
- """The kernel module ``value`` comes from, when that module is the kernel's copy.
49
-
50
- The ``command_output`` name condition is held by
51
- test_the_guard_passes_the_kernel_s_settings_reader_that_stays_the_kernel_s,
52
- the module condition beside it by
53
- test_the_guard_passes_this_package_s_own_setting_reader.
54
- """
55
- if isinstance(value, ModuleType):
56
- origin = value.__name__
57
- else:
58
- origin = getattr(value, "__module__", None)
59
- if not isinstance(origin, str):
60
- return None
61
- if origin == "django_service_specs.settings" and (
62
- getattr(value, "__name__", None) == "command_output"
63
- ):
64
- return f"{origin}.command_output"
65
- return origin if _within(origin, _KERNEL_COPY) else None
66
-
67
-
68
- def kernel_copies_loaded(modules: dict[str, ModuleType | None]) -> list[str]:
69
- """Every place in ``modules`` that reaches the kernel's copy of this package.
70
-
71
- The ``module is None`` condition is held by
72
- test_the_guard_skips_a_module_import_blocked_with_none, the package
73
- condition beside it by test_the_guard_reads_only_this_repository_s_modules.
74
- """
75
- found = [f"{name} is imported" for name in _NEVER_IMPORTED if name in modules]
76
- for name, module in sorted(modules.items()):
77
- if not _within(name, _OURS) or module is None:
78
- continue
79
- for attribute, value in sorted(vars(module).items()):
80
- origin = _kernel_copy_of(value)
81
- if origin is not None:
82
- found.append(f"{name}.{attribute} is the kernel's {origin}")
83
- return found
84
-
85
-
86
- @pytest.fixture
87
- def keep_the_test_connection(monkeypatch: pytest.MonkeyPatch) -> None:
88
- """For a test calling ``run_from_argv``, which closes every connection when it ends.
89
-
90
- That would close the one the test's transaction is on. Opt-in rather than
91
- autouse, so no other test runs with ``close_all`` patched out.
92
- """
93
- monkeypatch.setattr("django.db.connections.close_all", lambda: None)
94
-
95
-
96
- def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
97
- found = kernel_copies_loaded(dict(sys.modules))
98
- if found:
99
- reporter = session.config.pluginmanager.get_plugin("terminalreporter")
100
- lines = ["The suite reached the kernel's copy of this package:", *found]
101
- if reporter is not None:
102
- reporter.write_line("\n".join(lines), red=True)
103
- session.exitstatus = pytest.ExitCode.TESTS_FAILED
@@ -1,63 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import sys
4
- from types import ModuleType
5
-
6
- import django_service_specs
7
-
8
- from tests.conftest import kernel_copies_loaded
9
-
10
-
11
- def _module(name: str, **values: object) -> ModuleType:
12
- module = ModuleType(name)
13
- vars(module).update(values)
14
- return module
15
-
16
-
17
- def test_the_guard_finds_a_name_imported_from_the_kernel_s_copy() -> None:
18
- # Read off the root inside the test, never held as a global here, or the
19
- # guard would find this module too.
20
- copy = django_service_specs.SpecCommand
21
- found = kernel_copies_loaded({"tests.stray": _module("tests.stray", SpecCommand=copy)})
22
- assert found == [
23
- "tests.stray.SpecCommand is the kernel's django_service_specs.commands.spec_command"
24
- ]
25
-
26
-
27
- def test_the_guard_finds_a_kernel_copy_module_and_the_kernel_s_setting_reader() -> None:
28
- commands = sys.modules["django_service_specs.commands"]
29
- reader = sys.modules["django_service_specs.settings"].command_output
30
- found = kernel_copies_loaded(
31
- {"docs.stray": _module("docs.stray", commands=commands, command_output=reader)}
32
- )
33
- assert found == [
34
- "docs.stray.command_output is the kernel's django_service_specs.settings.command_output",
35
- "docs.stray.commands is the kernel's django_service_specs.commands",
36
- ]
37
-
38
-
39
- def test_the_guard_finds_the_kernel_s_checks_imported_at_all() -> None:
40
- found = kernel_copies_loaded({"django_service_specs.checks": _module("x")})
41
- assert found == ["django_service_specs.checks is imported"]
42
-
43
-
44
- def test_the_guard_reads_only_this_repository_s_modules() -> None:
45
- copy = django_service_specs.SpecCommand
46
- assert kernel_copies_loaded({"elsewhere": _module("elsewhere", SpecCommand=copy)}) == []
47
-
48
-
49
- def test_the_guard_passes_the_kernel_s_settings_reader_that_stays_the_kernel_s() -> None:
50
- # ``settings.py`` does not move: only ``command_output`` in it is the copy.
51
- reader = sys.modules["django_service_specs.settings"].max_array_length
52
- assert kernel_copies_loaded({"tests.stray": _module("tests.stray", reader=reader)}) == []
53
-
54
-
55
- def test_the_guard_skips_a_module_import_blocked_with_none() -> None:
56
- # ``sys.modules[name] = None`` is how an import is blocked, and such an
57
- # entry has no namespace to read.
58
- assert kernel_copies_loaded({"tests.blocked": None}) == []
59
-
60
-
61
- def test_the_guard_passes_this_package_s_own_setting_reader() -> None:
62
- reader = sys.modules["django_spec_cli.settings"].command_output
63
- assert kernel_copies_loaded({"tests.own": _module("tests.own", reader=reader)}) == []
@@ -1,213 +0,0 @@
1
- """No source in this repository reaches the kernel's copy of this package by name.
2
-
3
- Kernel 0.10.x still ships the command half this package was moved out of, and
4
- its root re-exports every moving name, so ``django_service_specs.ExitCode``
5
- resolves, to the kernel's class, and a test reading it passes while testing the
6
- wrong code. ``tests/conftest.py``'s session-finish guard sees only module
7
- globals the run loaded; a function-local import, an attribute read through the
8
- kernel's root and a python block in a page are invisible to it. This reads the
9
- source instead, with ``ast``, so an import at any depth, an aliased module and
10
- an attribute chain are all one shape.
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- import ast
16
- import re
17
- from pathlib import Path
18
-
19
- import django_service_specs
20
- import pytest
21
-
22
- import django_spec_cli
23
- from tests.utils import REPO_ROOT
24
-
25
- KERNEL = "django_service_specs"
26
- _MOVED_MODULES = ("commands", "checks", "apps")
27
- _MOVED_PATHS = tuple(f"{KERNEL}.{module}" for module in (*_MOVED_MODULES, "types.exit_code"))
28
- # ``__version__`` is in both roots and is each package's own, not a moved name.
29
- FORBIDDEN = (set(django_spec_cli.__all__) & set(django_service_specs.__all__)) - {"__version__"} | {
30
- "check_unguarded_specs",
31
- "command_output",
32
- *_MOVED_MODULES,
33
- }
34
- """Names that, read through the kernel, are the kernel's copy of this package."""
35
-
36
- EXEMPT = {
37
- # The guard names the kernel's copy to look for it.
38
- "tests/conftest.py",
39
- # The guard's own tests hand it the kernel's objects to find.
40
- "tests/test_conftest.py",
41
- # This file, whose fixtures below are the shapes it rejects.
42
- "tests/test_kernel_copy_references.py",
43
- }
44
- """Files that name the kernel's copy on purpose, each for the reason beside it."""
45
-
46
-
47
- def _forbidden_path(path: str) -> bool:
48
- if path in _MOVED_PATHS or path.startswith(tuple(f"{p}." for p in _MOVED_PATHS)):
49
- return True
50
- head, _, rest = path.partition(".")
51
- return head == KERNEL and rest.split(".")[0] in FORBIDDEN
52
-
53
-
54
- def _dotted(node: ast.expr) -> list[str] | None:
55
- parts: list[str] = []
56
- while isinstance(node, ast.Attribute):
57
- parts.append(node.attr)
58
- node = node.value
59
- if not isinstance(node, ast.Name):
60
- return None
61
- return [node.id, *reversed(parts)]
62
-
63
-
64
- def kernel_copy_references(source: str) -> list[str]:
65
- """Every reference in ``source`` that reaches the kernel's copy of this package."""
66
- tree = ast.parse(source)
67
- # Every name the kernel's root is bound to, ``import django_service_specs``
68
- # and ``import django_service_specs.x`` included, wherever it is bound.
69
- aliases = {
70
- alias.asname or alias.name.split(".")[0]
71
- for node in ast.walk(tree)
72
- if isinstance(node, ast.Import)
73
- for alias in node.names
74
- if alias.name == KERNEL or (alias.name.startswith(f"{KERNEL}.") and not alias.asname)
75
- }
76
- found: list[str] = []
77
- for node in ast.walk(tree):
78
- if isinstance(node, ast.Import):
79
- found += [
80
- f"{node.lineno}: import {alias.name}"
81
- for alias in node.names
82
- if _forbidden_path(alias.name)
83
- ]
84
- elif isinstance(node, ast.ImportFrom) and (node.module or "").split(".")[0] == KERNEL:
85
- module = node.module or ""
86
- found += [
87
- f"{node.lineno}: from {module} import {alias.name}"
88
- for alias in node.names
89
- if _forbidden_path(module) or alias.name in FORBIDDEN
90
- ]
91
- elif isinstance(node, ast.Attribute) and isinstance(node.ctx, ast.Load):
92
- parts = _dotted(node)
93
- if parts and parts[0] in aliases:
94
- path = [KERNEL, *parts[1:]]
95
- # The shortest forbidden prefix of a chain only, so one read
96
- # is one finding: ``k.SpecCommand.help`` reports
97
- # ``k.SpecCommand``.
98
- if _forbidden_path(".".join(path)) and not _forbidden_path(".".join(path[:-1])):
99
- found.append(f"{node.lineno}: {'.'.join(parts)}")
100
- elif (
101
- isinstance(node, ast.Constant)
102
- and isinstance(node.value, str)
103
- # A dotted path handed to ``monkeypatch.setattr`` or ``import_module``.
104
- and re.fullmatch(rf"{KERNEL}(\.\w+)+", node.value)
105
- and _forbidden_path(node.value)
106
- ):
107
- found.append(f"{node.lineno}: {node.value!r}")
108
- return found
109
-
110
-
111
- def _python_sources() -> list[Path]:
112
- return sorted(
113
- path
114
- for directory in ("django_spec_cli", "tests", "docs")
115
- for path in (REPO_ROOT / directory).rglob("*.py")
116
- if path.relative_to(REPO_ROOT).as_posix() not in EXEMPT
117
- )
118
-
119
-
120
- def _page_blocks() -> list[tuple[str, int, str]]:
121
- blocks = []
122
- for page in sorted([*(REPO_ROOT / "docs").glob("*.md"), REPO_ROOT / "README.md"]):
123
- for index, block in enumerate(re.findall(r"```python\n(.*?)```", page.read_text(), re.S)):
124
- # A snippet include names a file under docs/examples, read above.
125
- if "--8<--" not in block:
126
- blocks.append((page.relative_to(REPO_ROOT).as_posix(), index, block))
127
- return blocks
128
-
129
-
130
- def test_the_sources_read_are_the_package_the_suite_and_the_pages() -> None:
131
- # An empty walk would pass the two tests below with nothing read.
132
- read = {path.relative_to(REPO_ROOT).as_posix() for path in _python_sources()}
133
- assert {"django_spec_cli/spec_command.py", "docs/examples/commands.py"} <= read
134
- assert {"tests/test_checks.py", "tests/test_spec_command.py"} <= read
135
- assert {page for page, _, _ in _page_blocks()} == {"README.md", "docs/index.md"}
136
- assert {"SpecCommand", "ExitCode", "exit_code_for"} <= FORBIDDEN
137
-
138
-
139
- @pytest.mark.parametrize(
140
- "path", _python_sources(), ids=lambda path: path.relative_to(REPO_ROOT).as_posix()
141
- )
142
- def test_no_module_reaches_the_kernel_s_copy(path: Path) -> None:
143
- assert kernel_copy_references(path.read_text()) == []
144
-
145
-
146
- @pytest.mark.parametrize(
147
- ("page", "index", "block"), _page_blocks(), ids=lambda value: str(value)[:20]
148
- )
149
- def test_no_python_block_on_a_page_reaches_the_kernel_s_copy(
150
- page: str, index: int, block: str
151
- ) -> None:
152
- assert kernel_copy_references(block) == [], (page, index)
153
-
154
-
155
- @pytest.mark.parametrize(
156
- ("source", "expected"),
157
- [
158
- (
159
- "import django_service_specs\ndjango_service_specs.ExitCode\n",
160
- ["2: django_service_specs.ExitCode"],
161
- ),
162
- ("import django_service_specs as k\nk.SpecCommand.help\n", ["2: k.SpecCommand"]),
163
- (
164
- "import django_service_specs.registry\ndjango_service_specs.exit_code_for(e)\n",
165
- ["2: django_service_specs.exit_code_for"],
166
- ),
167
- (
168
- "def f():\n from django_service_specs import ExitCode\n",
169
- ["2: from django_service_specs import ExitCode"],
170
- ),
171
- (
172
- "from django_service_specs.settings import command_output\n",
173
- ["1: from django_service_specs.settings import command_output"],
174
- ),
175
- (
176
- "from django_service_specs import check_unguarded_specs\n",
177
- ["1: from django_service_specs import check_unguarded_specs"],
178
- ),
179
- (
180
- "from django_service_specs.commands.argv import utils\n",
181
- ["1: from django_service_specs.commands.argv import utils"],
182
- ),
183
- (
184
- "import django_service_specs.types.exit_code\n",
185
- ["1: import django_service_specs.types.exit_code"],
186
- ),
187
- (
188
- "import django_service_specs\ndjango_service_specs.types.exit_code.ExitCode\n",
189
- ["2: django_service_specs.types.exit_code"],
190
- ),
191
- (
192
- "patch('django_service_specs.checks.get_commands')\n",
193
- ["1: 'django_service_specs.checks.get_commands'"],
194
- ),
195
- ],
196
- )
197
- def test_each_route_to_the_kernel_s_copy_is_found(source: str, expected: list[str]) -> None:
198
- assert kernel_copy_references(source) == expected
199
-
200
-
201
- @pytest.mark.parametrize(
202
- "source",
203
- [
204
- "from django_service_specs import SpecRegistry, unguarded_specs\n",
205
- "import django_service_specs\ndjango_service_specs.REFUSALS\n",
206
- "from django_service_specs.adapters.pydantic import PydanticPresenter\n",
207
- "from django_spec_cli import ExitCode, SpecCommand\n",
208
- "ids = ['django_service_specs.W001', 'django_service_specs']\n",
209
- "import other as django_service_specs\nother.ExitCode\n",
210
- ],
211
- )
212
- def test_a_kernel_name_that_stays_the_kernel_s_is_not_found(source: str) -> None:
213
- assert kernel_copy_references(source) == []
File without changes