strictcli 0.11.0__tar.gz → 0.12.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 (127) hide show
  1. strictcli-0.12.0/.rlsbl/changes/.validated +1 -0
  2. strictcli-0.12.0/.rlsbl/changes/0.12.0.jsonl +2 -0
  3. strictcli-0.12.0/.rlsbl/changes/0.12.0.md +5 -0
  4. strictcli-0.12.0/.rlsbl/releases/v0.12.0.toml +4 -0
  5. {strictcli-0.11.0 → strictcli-0.12.0}/CHANGELOG.md +6 -0
  6. {strictcli-0.11.0 → strictcli-0.12.0}/PKG-INFO +1 -1
  7. {strictcli-0.11.0 → strictcli-0.12.0}/package-lock.json +2 -2
  8. {strictcli-0.11.0 → strictcli-0.12.0}/package.json +1 -1
  9. {strictcli-0.11.0 → strictcli-0.12.0}/pyproject.toml +1 -1
  10. {strictcli-0.11.0 → strictcli-0.12.0}/strictcli/__init__.py +23 -15
  11. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_check_command.py +17 -9
  12. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_check_discovery.py +93 -42
  13. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_check_runner.py +7 -5
  14. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_check_schema.py +14 -6
  15. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_toml_loading.py +94 -5
  16. {strictcli-0.11.0 → strictcli-0.12.0}/uv.lock +1 -1
  17. strictcli-0.11.0/.rlsbl/changes/.validated +0 -1
  18. {strictcli-0.11.0 → strictcli-0.12.0}/.claude/settings.json +0 -0
  19. {strictcli-0.11.0 → strictcli-0.12.0}/.github/workflows/ci.yml +0 -0
  20. {strictcli-0.11.0 → strictcli-0.12.0}/.github/workflows/publish.yml +0 -0
  21. {strictcli-0.11.0 → strictcli-0.12.0}/.gitignore +0 -0
  22. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/bases/.claude/settings.json +0 -0
  23. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
  24. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
  25. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/bases/.gitignore +0 -0
  26. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
  27. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
  28. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
  29. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
  30. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
  31. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
  32. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/bases/CHANGELOG.md +0 -0
  33. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/bases/CLAUDE.md +0 -0
  34. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/bases/LICENSE +0 -0
  35. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.10.0.jsonl +0 -0
  36. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.10.0.md +0 -0
  37. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.11.0.jsonl +0 -0
  38. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.11.0.md +0 -0
  39. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
  40. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.4.0.md +0 -0
  41. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
  42. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.4.1.md +0 -0
  43. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
  44. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.5.0.md +0 -0
  45. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
  46. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.6.0.md +0 -0
  47. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
  48. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.6.1.md +0 -0
  49. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
  50. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.7.0.md +0 -0
  51. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
  52. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.7.1.md +0 -0
  53. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
  54. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.0.md +0 -0
  55. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
  56. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.1.md +0 -0
  57. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
  58. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.2.md +0 -0
  59. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
  60. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.3.md +0 -0
  61. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
  62. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.4.md +0 -0
  63. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
  64. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.5.md +0 -0
  65. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
  66. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.6.md +0 -0
  67. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
  68. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.7.md +0 -0
  69. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
  70. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.9.0.md +0 -0
  71. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
  72. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/0.9.1.md +0 -0
  73. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/changes/unreleased.jsonl +0 -0
  74. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/config.json +0 -0
  75. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/hashes.json +0 -0
  76. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/hooks/post-release.sh +0 -0
  77. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/hooks/pre-checks.sh +0 -0
  78. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/hooks/pre-release.sh +0 -0
  79. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/lint/go.toml +0 -0
  80. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/lint/npm.toml +0 -0
  81. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/lint/python.toml +0 -0
  82. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/releases/unreleased.toml +0 -0
  83. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/releases/v0.10.0.toml +0 -0
  84. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/releases/v0.11.0.toml +0 -0
  85. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/releases/v0.8.5.toml +0 -0
  86. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/releases/v0.8.6.toml +0 -0
  87. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/releases/v0.8.7.toml +0 -0
  88. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/releases/v0.9.0.toml +0 -0
  89. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/releases/v0.9.1.toml +0 -0
  90. {strictcli-0.11.0 → strictcli-0.12.0}/.rlsbl/version +0 -0
  91. {strictcli-0.11.0 → strictcli-0.12.0}/.strictcli/schema.json +0 -0
  92. {strictcli-0.11.0 → strictcli-0.12.0}/CLAUDE.md +0 -0
  93. {strictcli-0.11.0 → strictcli-0.12.0}/LICENSE +0 -0
  94. {strictcli-0.11.0 → strictcli-0.12.0}/README.md +0 -0
  95. {strictcli-0.11.0 → strictcli-0.12.0}/index.js +0 -0
  96. {strictcli-0.11.0 → strictcli-0.12.0}/postinstall.js +0 -0
  97. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_arg_default.py +0 -0
  98. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_at_prefix.py +0 -0
  99. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_auto_version.py +0 -0
  100. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_check_types.py +0 -0
  101. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_choices.py +0 -0
  102. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_command_help_suggestion.py +0 -0
  103. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_config.py +0 -0
  104. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_config_file_path.py +0 -0
  105. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_deep_nesting.py +0 -0
  106. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_dependencies.py +0 -0
  107. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_deprecated.py +0 -0
  108. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_dump_schema.py +0 -0
  109. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_e2e.py +0 -0
  110. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_env.py +0 -0
  111. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_exit_codes.py +0 -0
  112. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_float_type.py +0 -0
  113. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_global_flags.py +0 -0
  114. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_help.py +0 -0
  115. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_int_type.py +0 -0
  116. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_mutex.py +0 -0
  117. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_nesting.py +0 -0
  118. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_parser.py +0 -0
  119. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_passthrough.py +0 -0
  120. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_registration.py +0 -0
  121. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_repeatable.py +0 -0
  122. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_tagdsl.py +0 -0
  123. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_tags.py +0 -0
  124. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_validate.py +0 -0
  125. {strictcli-0.11.0 → strictcli-0.12.0}/tests/test_variadic.py +0 -0
  126. {strictcli-0.11.0 → strictcli-0.12.0}/todo/.defer/deferred.md +0 -0
  127. {strictcli-0.11.0 → strictcli-0.12.0}/todo/.done/original-idea.md +0 -0
@@ -0,0 +1 @@
1
+ c161283293520cc94f39d9cfec589f6f4cf5a352
@@ -0,0 +1,2 @@
1
+ {"commits":["495cf93ada5d5870e1fa83594b7c856f2f493f19","d604e97d59fa0e252bfd179a2e4d0f0f62757677"],"user_facing":true,"description":"**Breaking.** CWD auto-discovery of checks.toml removed; checks must be explicitly enabled via `checks_path=`. checks.toml now requires a top-level `app` field matching the app name.","type":"breaking"}
2
+ {"commits":["fb4899a3e17880d31ac56b42aca24d359528c8af"],"user_facing":false}
@@ -0,0 +1,5 @@
1
+ ## 0.12.0
2
+
3
+ ### Breaking
4
+
5
+ - **Breaking.** CWD auto-discovery of checks.toml removed; checks must be explicitly enabled via `checks_path=`. checks.toml now requires a top-level `app` field matching the app name.
@@ -0,0 +1,4 @@
1
+ # Version bump type: patch, minor, or major
2
+ bump = "minor"
3
+ include = ["pypi", "npm"]
4
+ exclude = []
@@ -2,6 +2,12 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## 0.12.0
6
+
7
+ ### Breaking
8
+
9
+ - **Breaking.** CWD auto-discovery of checks.toml removed; checks must be explicitly enabled via `checks_path=`. checks.toml now requires a top-level `app` field matching the app name.
10
+
5
11
  ## 0.11.0
6
12
 
7
13
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strictcli
3
- Version: 0.11.0
3
+ Version: 0.12.0
4
4
  Summary: A strict, zero-dependency CLI framework for Python
5
5
  Project-URL: Homepage, https://github.com/smm-h/strictcli
6
6
  Project-URL: Repository, https://github.com/smm-h/strictcli
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "strictcli",
9
- "version": "0.11.0",
9
+ "version": "0.12.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT"
12
12
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "A strict, zero-dependency CLI framework for Python (npm wrapper)",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "strictcli"
7
- version = "0.11.0"
7
+ version = "0.12.0"
8
8
  description = "A strict, zero-dependency CLI framework for Python"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.11.0"
5
+ __version__ = "0.12.0"
6
6
 
7
7
  __all__ = [
8
8
  "App", "Flag", "Arg", "Tag", "MutexGroup", "CoRequired", "Requires",
@@ -130,8 +130,8 @@ _CHECK_REQUIRED_FIELDS = {"tags", "severity", "fast", "pure", "needs_network", "
130
130
  _CHECK_VALID_SEVERITIES = {"error", "warn"}
131
131
 
132
132
 
133
- def _load_checks_toml(path: str | Path) -> dict[str, _CheckDef]:
134
- """Parse and validate a checks.toml file, returning check definitions.
133
+ def _load_checks_toml(path: str | Path) -> tuple[str, dict[str, _CheckDef]]:
134
+ """Parse and validate a checks.toml file, returning (app_name, check_defs).
135
135
 
136
136
  Raises ValueError on any schema violation, file error, or invalid TOML.
137
137
  """
@@ -145,13 +145,20 @@ def _load_checks_toml(path: str | Path) -> dict[str, _CheckDef]:
145
145
  except (tomllib.TOMLDecodeError, UnicodeDecodeError) as exc:
146
146
  raise ValueError(f"checks.toml: {exc}") from exc
147
147
 
148
- # Only [checks] is allowed at the top level
148
+ # Only "app" and [checks] are allowed at the top level
149
149
  for key in data:
150
- if key != "checks":
150
+ if key not in ("app", "checks"):
151
151
  raise ValueError(f'checks.toml: unknown top-level key "{key}"')
152
152
 
153
+ # Validate required "app" field
154
+ if "app" not in data:
155
+ raise ValueError('checks.toml: missing required top-level key "app"')
156
+ if not isinstance(data["app"], str) or not data["app"]:
157
+ raise ValueError('checks.toml: "app" must be a non-empty string')
158
+ app_name = data["app"]
159
+
153
160
  if "checks" not in data:
154
- return {}
161
+ return (app_name, {})
155
162
 
156
163
  checks_section = data["checks"]
157
164
  if not isinstance(checks_section, dict):
@@ -243,7 +250,7 @@ def _load_checks_toml(path: str | Path) -> dict[str, _CheckDef]:
243
250
  f'unknown check "{dep}"'
244
251
  )
245
252
 
246
- return result
253
+ return (app_name, result)
247
254
 
248
255
 
249
256
  class _HelpRequested(Exception):
@@ -709,14 +716,15 @@ class App:
709
716
  checks_toml_path = Path(self.checks_path).resolve()
710
717
  if not checks_toml_path.is_file():
711
718
  raise ValueError(f"checks_path does not exist: {self.checks_path}")
712
- else:
713
- checks_toml_path = Path.cwd() / ".strictcli" / "checks.toml"
714
- if checks_toml_path.is_file():
715
- self._check_defs: dict[str, _CheckDef] = _load_checks_toml(checks_toml_path)
719
+ app_name, self._check_defs = _load_checks_toml(checks_toml_path)
720
+ if app_name != self.name:
721
+ raise ValueError(
722
+ f'checks.toml: app "{app_name}" does not match app name "{self.name}"'
723
+ )
716
724
  self._checks_enabled = True
717
725
  self._register_check_command()
718
726
  else:
719
- self._check_defs = {}
727
+ self._check_defs: dict[str, _CheckDef] = {}
720
728
  self._checks_enabled = False
721
729
 
722
730
  @property
@@ -730,12 +738,12 @@ class App:
730
738
  if not self._checks_enabled:
731
739
  raise ValueError(
732
740
  f'cannot register check "{name}": '
733
- f"no .strictcli/checks.toml found"
741
+ f"checks not enabled"
734
742
  )
735
743
  if name not in self._check_defs:
736
744
  raise ValueError(
737
745
  f'cannot register check "{name}": '
738
- f"not declared in .strictcli/checks.toml"
746
+ f"not declared in checks.toml"
739
747
  )
740
748
  if self._check_defs[name].impl is not None:
741
749
  raise ValueError(f'check "{name}": duplicate registration')
@@ -756,7 +764,7 @@ class App:
756
764
  )
757
765
  if missing:
758
766
  return (
759
- "checks declared in .strictcli/checks.toml but not registered: "
767
+ "checks declared in checks.toml but not registered: "
760
768
  + ", ".join(missing)
761
769
  )
762
770
  return None
@@ -11,6 +11,8 @@ from strictcli import CheckResult
11
11
 
12
12
 
13
13
  TWO_CHECKS_TOML = """\
14
+ app = "testapp"
15
+
14
16
  [checks.version-check]
15
17
  tags = ["release"]
16
18
  severity = "error"
@@ -29,6 +31,8 @@ depends_on = []
29
31
  """
30
32
 
31
33
  THREE_CHECKS_WITH_DEP_TOML = """\
34
+ app = "testapp"
35
+
32
36
  [checks.base-check]
33
37
  tags = ["infra"]
34
38
  severity = "error"
@@ -66,11 +70,13 @@ def _setup_checks_app(tmp_path, monkeypatch, toml_content, register_impls=True,
66
70
 
67
71
  pass_results: dict mapping check name to CheckResult. If None, all return pass.
68
72
  """
69
- (tmp_path / ".strictcli").mkdir(exist_ok=True)
70
- (tmp_path / ".strictcli" / "checks.toml").write_text(toml_content)
71
- monkeypatch.chdir(tmp_path)
73
+ toml_file = tmp_path / "checks.toml"
74
+ toml_file.write_text(toml_content)
72
75
 
73
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
76
+ app = strictcli.App(
77
+ name="testapp", version="1.0.0", help="test app",
78
+ checks_path=str(toml_file),
79
+ )
74
80
 
75
81
  if register_impls:
76
82
  if pass_results is None:
@@ -317,13 +323,15 @@ class TestCheckIgnoreWarnings:
317
323
 
318
324
 
319
325
  class TestCheckNoContextFactory:
320
- def test_no_context_factory_error(self, tmp_path, monkeypatch):
326
+ def test_no_context_factory_error(self, tmp_path):
321
327
  """Running checks without set_check_context produces error."""
322
- (tmp_path / ".strictcli").mkdir(exist_ok=True)
323
- (tmp_path / ".strictcli" / "checks.toml").write_text(TWO_CHECKS_TOML)
324
- monkeypatch.chdir(tmp_path)
328
+ toml_file = tmp_path / "checks.toml"
329
+ toml_file.write_text(TWO_CHECKS_TOML)
325
330
 
326
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
331
+ app = strictcli.App(
332
+ name="testapp", version="1.0.0", help="test app",
333
+ checks_path=str(toml_file),
334
+ )
327
335
  for name in app._check_defs:
328
336
  app._check_defs[name].impl = (
329
337
  lambda ctx, n=name: CheckResult(status="pass", message=f"{n} OK")
@@ -1,13 +1,13 @@
1
1
  """Tests for check discovery and double-entry registration (Phase 3)."""
2
2
 
3
- import os
4
-
5
3
  import pytest
6
4
 
7
5
  import strictcli
8
6
 
9
7
 
10
8
  VALID_TOML = """\
9
+ app = "testapp"
10
+
11
11
  [checks.lint-code]
12
12
  tags = ["code", "fast"]
13
13
  severity = "error"
@@ -26,6 +26,8 @@ depends_on = ["lint-code"]
26
26
  """
27
27
 
28
28
  INVALID_TOML = """\
29
+ app = "testapp"
30
+
29
31
  [checks.BadName]
30
32
  tags = ["a"]
31
33
  severity = "error"
@@ -37,15 +39,15 @@ depends_on = []
37
39
 
38
40
 
39
41
  class TestDiscovery:
40
- def test_discovers_checks_toml_in_cwd(self, tmp_path, monkeypatch):
42
+ def test_no_checks_path_ignores_cwd(self, tmp_path, monkeypatch):
43
+ """Without checks_path, CWD checks.toml is NOT discovered."""
41
44
  (tmp_path / ".strictcli").mkdir()
42
45
  (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
43
46
  monkeypatch.chdir(tmp_path)
44
47
 
45
48
  app = strictcli.App(name="testapp", version="1.0.0", help="test app")
46
- assert app._checks_enabled is True
47
- assert "lint-code" in app._check_defs
48
- assert "check-deps" in app._check_defs
49
+ assert app._checks_enabled is False
50
+ assert app._check_defs == {}
49
51
 
50
52
  def test_no_toml_sets_checks_disabled(self, tmp_path, monkeypatch):
51
53
  monkeypatch.chdir(tmp_path)
@@ -54,13 +56,15 @@ class TestDiscovery:
54
56
  assert app._checks_enabled is False
55
57
  assert app._check_defs == {}
56
58
 
57
- def test_invalid_toml_raises_valueerror(self, tmp_path, monkeypatch):
58
- (tmp_path / ".strictcli").mkdir()
59
- (tmp_path / ".strictcli" / "checks.toml").write_text(INVALID_TOML)
60
- monkeypatch.chdir(tmp_path)
59
+ def test_invalid_toml_raises_valueerror(self, tmp_path):
60
+ toml_file = tmp_path / "checks.toml"
61
+ toml_file.write_text(INVALID_TOML)
61
62
 
62
63
  with pytest.raises(ValueError, match='invalid check name "BadName"'):
63
- strictcli.App(name="testapp", version="1.0.0", help="test app")
64
+ strictcli.App(
65
+ name="testapp", version="1.0.0", help="test app",
66
+ checks_path=str(toml_file),
67
+ )
64
68
 
65
69
 
66
70
  class TestChecksPath:
@@ -116,8 +120,8 @@ class TestChecksPath:
116
120
  checks_path=str(bad_path),
117
121
  )
118
122
 
119
- def test_none_uses_cwd_discovery(self, tmp_path, monkeypatch):
120
- """checks_path=None (default) falls back to CWD-based discovery."""
123
+ def test_none_means_disabled(self, tmp_path, monkeypatch):
124
+ """checks_path=None (default) means checks are disabled."""
121
125
  (tmp_path / ".strictcli").mkdir()
122
126
  (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
123
127
  monkeypatch.chdir(tmp_path)
@@ -126,17 +130,19 @@ class TestChecksPath:
126
130
  name="testapp", version="1.0.0", help="test app",
127
131
  checks_path=None,
128
132
  )
129
- assert app._checks_enabled is True
130
- assert "lint-code" in app._check_defs
133
+ assert app._checks_enabled is False
134
+ assert app._check_defs == {}
131
135
 
132
136
 
133
137
  class TestCheckDecorator:
134
- def test_registers_implementation(self, tmp_path, monkeypatch):
135
- (tmp_path / ".strictcli").mkdir()
136
- (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
137
- monkeypatch.chdir(tmp_path)
138
+ def test_registers_implementation(self, tmp_path):
139
+ toml_file = tmp_path / "checks.toml"
140
+ toml_file.write_text(VALID_TOML)
138
141
 
139
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
142
+ app = strictcli.App(
143
+ name="testapp", version="1.0.0", help="test app",
144
+ checks_path=str(toml_file),
145
+ )
140
146
 
141
147
  @app.check("lint-code")
142
148
  def lint_impl(ctx):
@@ -144,24 +150,28 @@ class TestCheckDecorator:
144
150
 
145
151
  assert app._check_defs["lint-code"].impl is lint_impl
146
152
 
147
- def test_undeclared_name_raises(self, tmp_path, monkeypatch):
148
- (tmp_path / ".strictcli").mkdir()
149
- (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
150
- monkeypatch.chdir(tmp_path)
153
+ def test_undeclared_name_raises(self, tmp_path):
154
+ toml_file = tmp_path / "checks.toml"
155
+ toml_file.write_text(VALID_TOML)
151
156
 
152
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
157
+ app = strictcli.App(
158
+ name="testapp", version="1.0.0", help="test app",
159
+ checks_path=str(toml_file),
160
+ )
153
161
 
154
162
  with pytest.raises(ValueError, match='cannot register check "nonexistent"'):
155
163
  @app.check("nonexistent")
156
164
  def bad_impl(ctx):
157
165
  pass
158
166
 
159
- def test_duplicate_registration_raises(self, tmp_path, monkeypatch):
160
- (tmp_path / ".strictcli").mkdir()
161
- (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
162
- monkeypatch.chdir(tmp_path)
167
+ def test_duplicate_registration_raises(self, tmp_path):
168
+ toml_file = tmp_path / "checks.toml"
169
+ toml_file.write_text(VALID_TOML)
163
170
 
164
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
171
+ app = strictcli.App(
172
+ name="testapp", version="1.0.0", help="test app",
173
+ checks_path=str(toml_file),
174
+ )
165
175
 
166
176
  @app.check("lint-code")
167
177
  def first(ctx):
@@ -177,19 +187,33 @@ class TestCheckDecorator:
177
187
 
178
188
  app = strictcli.App(name="testapp", version="1.0.0", help="test app")
179
189
 
180
- with pytest.raises(ValueError, match="no .strictcli/checks.toml found"):
190
+ with pytest.raises(ValueError, match="checks not enabled"):
181
191
  @app.check("anything")
182
192
  def bad(ctx):
183
193
  pass
184
194
 
195
+ def test_register_check_not_enabled_message(self, tmp_path):
196
+ """Verify exact error message for registering check when not enabled."""
197
+ app = strictcli.App(name="testapp", version="1.0.0", help="test app")
198
+
199
+ with pytest.raises(
200
+ ValueError,
201
+ match=r'cannot register check "my-check": checks not enabled',
202
+ ):
203
+ @app.check("my-check")
204
+ def impl(ctx):
205
+ pass
206
+
185
207
 
186
208
  class TestDoubleEntryValidation:
187
- def test_missing_impl_error_in_test(self, tmp_path, monkeypatch):
188
- (tmp_path / ".strictcli").mkdir()
189
- (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
190
- monkeypatch.chdir(tmp_path)
209
+ def test_missing_impl_error_in_test(self, tmp_path):
210
+ toml_file = tmp_path / "checks.toml"
211
+ toml_file.write_text(VALID_TOML)
191
212
 
192
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
213
+ app = strictcli.App(
214
+ name="testapp", version="1.0.0", help="test app",
215
+ checks_path=str(toml_file),
216
+ )
193
217
 
194
218
  # Register a dummy command so the app has something to run
195
219
  @app.command("hello", help="say hello")
@@ -204,15 +228,17 @@ class TestDoubleEntryValidation:
204
228
  # check-deps is not registered -- should fail validation
205
229
  result = app.test(["hello"])
206
230
  assert result.exit_code == 1
207
- assert "checks declared in .strictcli/checks.toml but not registered" in result.stderr
231
+ assert "checks declared in checks.toml but not registered" in result.stderr
208
232
  assert "check-deps" in result.stderr
209
233
 
210
- def test_all_registered_passes_validation(self, tmp_path, monkeypatch):
211
- (tmp_path / ".strictcli").mkdir()
212
- (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
213
- monkeypatch.chdir(tmp_path)
234
+ def test_all_registered_passes_validation(self, tmp_path):
235
+ toml_file = tmp_path / "checks.toml"
236
+ toml_file.write_text(VALID_TOML)
214
237
 
215
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
238
+ app = strictcli.App(
239
+ name="testapp", version="1.0.0", help="test app",
240
+ checks_path=str(toml_file),
241
+ )
216
242
 
217
243
  @app.command("hello", help="say hello")
218
244
  def hello(**kw):
@@ -242,3 +268,28 @@ class TestDoubleEntryValidation:
242
268
  result = app.test(["hello"])
243
269
  assert result.exit_code == 0
244
270
  assert "hello" in result.stdout
271
+
272
+ def test_app_mismatch_raises(self, tmp_path):
273
+ """checks_path with app='wrong' but App name='testapp' raises ValueError."""
274
+ toml = """\
275
+ app = "wrong"
276
+
277
+ [checks.my-check]
278
+ tags = ["a"]
279
+ severity = "error"
280
+ fast = true
281
+ pure = true
282
+ needs_network = false
283
+ depends_on = []
284
+ """
285
+ toml_file = tmp_path / "checks.toml"
286
+ toml_file.write_text(toml)
287
+
288
+ with pytest.raises(
289
+ ValueError,
290
+ match=r'checks.toml: app "wrong" does not match app name "testapp"',
291
+ ):
292
+ strictcli.App(
293
+ name="testapp", version="1.0.0", help="test app",
294
+ checks_path=str(toml_file),
295
+ )
@@ -113,7 +113,7 @@ class TestRunChecks:
113
113
  def _make_app_with_checks(self, check_defs, tmp_path, monkeypatch):
114
114
  """Build an App with pre-populated check definitions."""
115
115
  # Write a minimal TOML so the app discovers checks
116
- toml_lines = []
116
+ toml_lines = ['app = "testapp"', ""]
117
117
  for name, cdef in check_defs.items():
118
118
  tags_str = ", ".join(f'"{t}"' for t in cdef.tags)
119
119
  deps_str = ", ".join(f'"{d}"' for d in cdef.depends_on)
@@ -126,11 +126,13 @@ class TestRunChecks:
126
126
  toml_lines.append(f"depends_on = [{deps_str}]")
127
127
  toml_lines.append("")
128
128
 
129
- (tmp_path / ".strictcli").mkdir(exist_ok=True)
130
- (tmp_path / ".strictcli" / "checks.toml").write_text("\n".join(toml_lines))
131
- monkeypatch.chdir(tmp_path)
129
+ toml_file = tmp_path / "checks.toml"
130
+ toml_file.write_text("\n".join(toml_lines))
132
131
 
133
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
132
+ app = strictcli.App(
133
+ name="testapp", version="1.0.0", help="test app",
134
+ checks_path=str(toml_file),
135
+ )
134
136
  # Inject the impl functions from our check_defs
135
137
  for name, cdef in check_defs.items():
136
138
  if cdef.impl is not None:
@@ -6,6 +6,8 @@ import strictcli
6
6
 
7
7
 
8
8
  CHECKS_TOML = """\
9
+ app = "testapp"
10
+
9
11
  [checks.lint-code]
10
12
  tags = ["code", "fast"]
11
13
  severity = "error"
@@ -28,11 +30,14 @@ class TestSchemaWithChecks:
28
30
  """--dump-schema includes checks key when checks are enabled."""
29
31
 
30
32
  def test_checks_key_present(self, tmp_path, monkeypatch):
31
- (tmp_path / ".strictcli").mkdir()
32
- (tmp_path / ".strictcli" / "checks.toml").write_text(CHECKS_TOML)
33
+ toml_file = tmp_path / "checks.toml"
34
+ toml_file.write_text(CHECKS_TOML)
33
35
  monkeypatch.chdir(tmp_path)
34
36
 
35
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
37
+ app = strictcli.App(
38
+ name="testapp", version="1.0.0", help="test app",
39
+ checks_path=str(toml_file),
40
+ )
36
41
 
37
42
  @app.check("lint-code")
38
43
  def lint_impl(ctx):
@@ -54,11 +59,14 @@ class TestSchemaWithChecks:
54
59
  assert len(data["checks"]) == 2
55
60
 
56
61
  def test_check_metadata_correct(self, tmp_path, monkeypatch):
57
- (tmp_path / ".strictcli").mkdir()
58
- (tmp_path / ".strictcli" / "checks.toml").write_text(CHECKS_TOML)
62
+ toml_file = tmp_path / "checks.toml"
63
+ toml_file.write_text(CHECKS_TOML)
59
64
  monkeypatch.chdir(tmp_path)
60
65
 
61
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
66
+ app = strictcli.App(
67
+ name="testapp", version="1.0.0", help="test app",
68
+ checks_path=str(toml_file),
69
+ )
62
70
 
63
71
  @app.check("lint-code")
64
72
  def lint_impl(ctx):
@@ -7,6 +7,8 @@ from strictcli import _load_checks_toml
7
7
 
8
8
 
9
9
  VALID_TOML = """\
10
+ app = "testapp"
11
+
10
12
  [checks.lint-code]
11
13
  tags = ["code", "fast"]
12
14
  severity = "error"
@@ -29,7 +31,8 @@ class TestLoadChecksToml:
29
31
  def test_valid_toml_multiple_checks(self, tmp_path):
30
32
  f = tmp_path / "checks.toml"
31
33
  f.write_text(VALID_TOML)
32
- result = _load_checks_toml(f)
34
+ app_name, result = _load_checks_toml(f)
35
+ assert app_name == "testapp"
33
36
  assert len(result) == 2
34
37
  assert "lint-code" in result
35
38
  assert "check-deps" in result
@@ -51,6 +54,8 @@ class TestLoadChecksToml:
51
54
 
52
55
  def test_missing_required_field(self, tmp_path):
53
56
  toml = """\
57
+ app = "testapp"
58
+
54
59
  [checks.my-check]
55
60
  tags = ["a"]
56
61
  fast = true
@@ -65,6 +70,8 @@ depends_on = []
65
70
 
66
71
  def test_wrong_type_string_where_bool_expected(self, tmp_path):
67
72
  toml = """\
73
+ app = "testapp"
74
+
68
75
  [checks.my-check]
69
76
  tags = ["a"]
70
77
  severity = "error"
@@ -80,6 +87,8 @@ depends_on = []
80
87
 
81
88
  def test_unknown_field(self, tmp_path):
82
89
  toml = """\
90
+ app = "testapp"
91
+
83
92
  [checks.my-check]
84
93
  tags = ["a"]
85
94
  severity = "error"
@@ -96,6 +105,8 @@ extra = "nope"
96
105
 
97
106
  def test_unknown_top_level_key(self, tmp_path):
98
107
  toml = """\
108
+ app = "testapp"
109
+
99
110
  [metadata]
100
111
  version = "1.0"
101
112
 
@@ -114,6 +125,8 @@ depends_on = []
114
125
 
115
126
  def test_invalid_check_name_uppercase(self, tmp_path):
116
127
  toml = """\
128
+ app = "testapp"
129
+
117
130
  [checks.MyCheck]
118
131
  tags = ["a"]
119
132
  severity = "error"
@@ -129,6 +142,8 @@ depends_on = []
129
142
 
130
143
  def test_invalid_check_name_dots(self, tmp_path):
131
144
  toml = """\
145
+ app = "testapp"
146
+
132
147
  [checks."my.check"]
133
148
  tags = ["a"]
134
149
  severity = "error"
@@ -144,6 +159,8 @@ depends_on = []
144
159
 
145
160
  def test_invalid_check_name_spaces(self, tmp_path):
146
161
  toml = """\
162
+ app = "testapp"
163
+
147
164
  [checks."my check"]
148
165
  tags = ["a"]
149
166
  severity = "error"
@@ -159,6 +176,8 @@ depends_on = []
159
176
 
160
177
  def test_depends_on_nonexistent_check(self, tmp_path):
161
178
  toml = """\
179
+ app = "testapp"
180
+
162
181
  [checks.my-check]
163
182
  tags = ["a"]
164
183
  severity = "error"
@@ -172,11 +191,11 @@ depends_on = ["nonexistent"]
172
191
  with pytest.raises(ValueError, match='depends_on references unknown check "nonexistent"'):
173
192
  _load_checks_toml(f)
174
193
 
175
- def test_empty_file_returns_empty_dict(self, tmp_path):
194
+ def test_empty_file_raises_missing_app(self, tmp_path):
176
195
  f = tmp_path / "checks.toml"
177
196
  f.write_text("")
178
- result = _load_checks_toml(f)
179
- assert result == {}
197
+ with pytest.raises(ValueError, match='missing required top-level key "app"'):
198
+ _load_checks_toml(f)
180
199
 
181
200
  def test_invalid_toml_syntax(self, tmp_path):
182
201
  f = tmp_path / "checks.toml"
@@ -186,6 +205,8 @@ depends_on = ["nonexistent"]
186
205
 
187
206
  def test_invalid_severity_value(self, tmp_path):
188
207
  toml = """\
208
+ app = "testapp"
209
+
189
210
  [checks.my-check]
190
211
  tags = ["a"]
191
212
  severity = "critical"
@@ -201,6 +222,8 @@ depends_on = []
201
222
 
202
223
  def test_tags_empty_list(self, tmp_path):
203
224
  toml = """\
225
+ app = "testapp"
226
+
204
227
  [checks.my-check]
205
228
  tags = []
206
229
  severity = "error"
@@ -211,11 +234,13 @@ depends_on = []
211
234
  """
212
235
  f = tmp_path / "checks.toml"
213
236
  f.write_text(toml)
214
- defs = _load_checks_toml(f)
237
+ _, defs = _load_checks_toml(f)
215
238
  assert defs["my-check"].tags == []
216
239
 
217
240
  def test_tags_with_empty_string(self, tmp_path):
218
241
  toml = """\
242
+ app = "testapp"
243
+
219
244
  [checks.my-check]
220
245
  tags = [""]
221
246
  severity = "error"
@@ -233,3 +258,67 @@ depends_on = []
233
258
  f = tmp_path / "nonexistent.toml"
234
259
  with pytest.raises(ValueError, match="checks.toml:"):
235
260
  _load_checks_toml(f)
261
+
262
+ def test_missing_app_field(self, tmp_path):
263
+ toml = """\
264
+ [checks.my-check]
265
+ tags = ["a"]
266
+ severity = "error"
267
+ fast = true
268
+ pure = true
269
+ needs_network = false
270
+ depends_on = []
271
+ """
272
+ f = tmp_path / "checks.toml"
273
+ f.write_text(toml)
274
+ with pytest.raises(ValueError, match='missing required top-level key "app"'):
275
+ _load_checks_toml(f)
276
+
277
+ def test_app_field_wrong_type(self, tmp_path):
278
+ toml = """\
279
+ app = 42
280
+
281
+ [checks.my-check]
282
+ tags = ["a"]
283
+ severity = "error"
284
+ fast = true
285
+ pure = true
286
+ needs_network = false
287
+ depends_on = []
288
+ """
289
+ f = tmp_path / "checks.toml"
290
+ f.write_text(toml)
291
+ with pytest.raises(ValueError, match='"app" must be a non-empty string'):
292
+ _load_checks_toml(f)
293
+
294
+ def test_app_field_empty_string(self, tmp_path):
295
+ toml = """\
296
+ app = ""
297
+
298
+ [checks.my-check]
299
+ tags = ["a"]
300
+ severity = "error"
301
+ fast = true
302
+ pure = true
303
+ needs_network = false
304
+ depends_on = []
305
+ """
306
+ f = tmp_path / "checks.toml"
307
+ f.write_text(toml)
308
+ with pytest.raises(ValueError, match='"app" must be a non-empty string'):
309
+ _load_checks_toml(f)
310
+
311
+ def test_app_field_only_no_checks_section(self, tmp_path):
312
+ toml = 'app = "testapp"\n'
313
+ f = tmp_path / "checks.toml"
314
+ f.write_text(toml)
315
+ app_name, result = _load_checks_toml(f)
316
+ assert app_name == "testapp"
317
+ assert result == {}
318
+
319
+ def test_app_name_returned(self, tmp_path):
320
+ f = tmp_path / "checks.toml"
321
+ f.write_text(VALID_TOML)
322
+ app_name, result = _load_checks_toml(f)
323
+ assert app_name == "testapp"
324
+ assert len(result) == 2
@@ -232,7 +232,7 @@ wheels = [
232
232
 
233
233
  [[package]]
234
234
  name = "strictcli"
235
- version = "0.11.0"
235
+ version = "0.12.0"
236
236
  source = { editable = "." }
237
237
 
238
238
  [package.dev-dependencies]
@@ -1 +0,0 @@
1
- 228f7f6e4e51965e7f3cc19ea1d112e915b585c8
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