strictcli 0.33.0__tar.gz → 0.34.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 (88) hide show
  1. {strictcli-0.33.0 → strictcli-0.34.0}/PKG-INFO +1 -1
  2. {strictcli-0.33.0 → strictcli-0.34.0}/pyproject.toml +1 -1
  3. {strictcli-0.33.0 → strictcli-0.34.0}/strictcli/__init__.py +24 -2
  4. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_connection_env.py +36 -2
  5. {strictcli-0.33.0 → strictcli-0.34.0}/uv.lock +1 -1
  6. {strictcli-0.33.0 → strictcli-0.34.0}/.claude/settings.json +0 -0
  7. {strictcli-0.33.0 → strictcli-0.34.0}/.github/workflows/ci.yml +0 -0
  8. {strictcli-0.33.0 → strictcli-0.34.0}/.github/workflows/publish.yml +0 -0
  9. {strictcli-0.33.0 → strictcli-0.34.0}/.gitignore +0 -0
  10. {strictcli-0.33.0 → strictcli-0.34.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
  11. {strictcli-0.33.0 → strictcli-0.34.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
  12. {strictcli-0.33.0 → strictcli-0.34.0}/.rlsbl/bases/.gitignore +0 -0
  13. {strictcli-0.33.0 → strictcli-0.34.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
  14. {strictcli-0.33.0 → strictcli-0.34.0}/.rlsbl/config.json +0 -0
  15. {strictcli-0.33.0 → strictcli-0.34.0}/.rlsbl/lint/python.toml +0 -0
  16. {strictcli-0.33.0 → strictcli-0.34.0}/.rlsbl/managed-files.json +0 -0
  17. {strictcli-0.33.0 → strictcli-0.34.0}/.rlsbl/version +0 -0
  18. {strictcli-0.33.0 → strictcli-0.34.0}/.strictcli/schema.json +0 -0
  19. {strictcli-0.33.0 → strictcli-0.34.0}/CLAUDE.md +0 -0
  20. {strictcli-0.33.0 → strictcli-0.34.0}/LICENSE +0 -0
  21. {strictcli-0.33.0 → strictcli-0.34.0}/README.md +0 -0
  22. {strictcli-0.33.0 → strictcli-0.34.0}/strictcli/py.typed +0 -0
  23. {strictcli-0.33.0 → strictcli-0.34.0}/tests/conftest.py +0 -0
  24. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_arg_default.py +0 -0
  25. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_arg_default_validation.py +0 -0
  26. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_at_prefix.py +0 -0
  27. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_auto_version.py +0 -0
  28. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_call.py +0 -0
  29. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_check_command.py +0 -0
  30. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_check_discovery.py +0 -0
  31. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_check_provider.py +0 -0
  32. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_check_public_api.py +0 -0
  33. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_check_runner.py +0 -0
  34. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_check_schema.py +0 -0
  35. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_check_types.py +0 -0
  36. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_choices.py +0 -0
  37. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_choices_none.py +0 -0
  38. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_command_help_suggestion.py +0 -0
  39. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_command_tags.py +0 -0
  40. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_compound_types.py +0 -0
  41. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_config.py +0 -0
  42. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_config_fields.py +0 -0
  43. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_config_file_path.py +0 -0
  44. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_config_set_bugs.py +0 -0
  45. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_context.py +0 -0
  46. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_coverage.py +0 -0
  47. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_deep_nesting.py +0 -0
  48. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_dependencies.py +0 -0
  49. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_deprecated.py +0 -0
  50. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_dump_schema.py +0 -0
  51. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_e2e.py +0 -0
  52. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_env.py +0 -0
  53. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_exit_codes.py +0 -0
  54. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_flag_sets.py +0 -0
  55. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_float_format.py +0 -0
  56. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_float_type.py +0 -0
  57. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_float_vectors.py +0 -0
  58. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_global_flag_conflict_position.py +0 -0
  59. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_global_flags.py +0 -0
  60. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_help.py +0 -0
  61. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_hermetic.py +0 -0
  62. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_infra_env.py +0 -0
  63. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_int_type.py +0 -0
  64. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_invoke.py +0 -0
  65. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_keyword_flags.py +0 -0
  66. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_mcp.py +0 -0
  67. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_mutex.py +0 -0
  68. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_nesting.py +0 -0
  69. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_parser.py +0 -0
  70. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_passthrough.py +0 -0
  71. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_provenance.py +0 -0
  72. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_provenance_phase2.py +0 -0
  73. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_registration.py +0 -0
  74. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_repeatable.py +0 -0
  75. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_reserved_global_flags.py +0 -0
  76. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_tagdsl.py +0 -0
  77. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_toml_loading.py +0 -0
  78. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_tool_export.py +0 -0
  79. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_typed_args.py +0 -0
  80. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_unique.py +0 -0
  81. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_utilities.py +0 -0
  82. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_validate.py +0 -0
  83. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_variadic.py +0 -0
  84. {strictcli-0.33.0 → strictcli-0.34.0}/tests/test_visibility.py +0 -0
  85. {strictcli-0.33.0 → strictcli-0.34.0}/todo/.defer/deferred.md +0 -0
  86. {strictcli-0.33.0 → strictcli-0.34.0}/todo/.done/keyword-collision-in-flag-param-name.md +0 -0
  87. {strictcli-0.33.0 → strictcli-0.34.0}/todo/.done/original-idea.md +0 -0
  88. {strictcli-0.33.0 → strictcli-0.34.0}/todo/.done/public-check-runner-api.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strictcli
3
- Version: 0.33.0
3
+ Version: 0.34.0
4
4
  Summary: A strict 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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "strictcli"
7
- version = "0.33.0"
7
+ version = "0.34.0"
8
8
  description = "A strict 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.33.0"
5
+ __version__ = "0.34.0"
6
6
 
7
7
  __all__ = [
8
8
  "App", "Flag", "Arg", "FlagSet", "MutexGroup", "CoRequired", "Requires",
@@ -2651,10 +2651,27 @@ class ConnectionEnvReader(Protocol):
2651
2651
  instead of connecting. The check command wraps the tool-supplied check
2652
2652
  context in a value that satisfies this protocol, backed by the app's declared
2653
2653
  connection envs and the invocation's hermetic state. Checks that need a
2654
- connection URL call ``ctx.connection_env_value("DATABASE_URL")``."""
2654
+ connection URL call ``ctx.connection_env_value("DATABASE_URL")``.
2655
+
2656
+ ``is_hermetic()`` reports whether the invocation ran under --hermetic. It
2657
+ exists so a check can DISTINGUISH the two cases that
2658
+ ``connection_env_value``'s ``present=False`` otherwise conflates:
2659
+ "--hermetic suppressed the connection env" vs "the env var is simply unset".
2660
+ A check that layers config fallbacks below the env must honor hermetic even
2661
+ when the env is unset -- otherwise it falls through to a config URL and
2662
+ connects, violating the hermetic guarantee::
2663
+
2664
+ dsn, present = ctx.connection_env_value("DATABASE_URL")
2665
+ if not present:
2666
+ if ctx.is_hermetic():
2667
+ return reporter.skipped("hermetic: connection suppressed")
2668
+ # env unset but not hermetic -- config fallback is allowed here
2669
+ """
2655
2670
 
2656
2671
  def connection_env_value(self, env_var: str) -> "tuple[str | None, bool]": ...
2657
2672
 
2673
+ def is_hermetic(self) -> bool: ...
2674
+
2658
2675
 
2659
2676
  class _CheckContextWithConn:
2660
2677
  """Wraps a tool-supplied check context, delegating attribute access while
@@ -2679,6 +2696,11 @@ class _CheckContextWithConn:
2679
2696
  return None, False
2680
2697
  raise KeyError(f'"{env_var}" is not a declared connection env var')
2681
2698
 
2699
+ def is_hermetic(self) -> bool:
2700
+ """Report whether the invocation ran under --hermetic. Mirrors the
2701
+ hermetic flag captured when the wrapper was built."""
2702
+ return self._hermetic
2703
+
2682
2704
 
2683
2705
  @dataclass
2684
2706
  class _CheckDef:
@@ -178,9 +178,15 @@ def _make_conn_check_app(tmp_path):
178
178
 
179
179
  @app.error_check("db-reachable")
180
180
  def db_reachable(ctx, reporter):
181
+ reporter.note(f"hermetic={ctx.is_hermetic()}")
181
182
  dsn, present = ctx.connection_env_value("DATABASE_URL")
182
183
  if not present:
183
- return reporter.skipped("DATABASE_URL absent (hermetic or unset)")
184
+ # Distinguish hermetic suppression from a plainly-unset env: a
185
+ # consumer layering config fallbacks below the env must honor
186
+ # hermetic (skip) rather than fall through and connect.
187
+ if ctx.is_hermetic():
188
+ return reporter.skipped("DATABASE_URL suppressed by --hermetic")
189
+ return reporter.skipped("DATABASE_URL unset")
184
190
  reporter.note(f"dsn={dsn}")
185
191
  return reporter.passed("connection env visible")
186
192
 
@@ -194,14 +200,42 @@ def test_connection_env_check_side_access(tmp_path, monkeypatch):
194
200
  r = app.test(["check", "--tag", "db", "--verbose"])
195
201
  assert "dsn=postgres://check/db" in r.stdout
196
202
  assert "PASS" in r.stdout
203
+ # A non-hermetic invocation reports is_hermetic()==False to the check.
204
+ assert "hermetic=False" in r.stdout
197
205
 
198
206
 
199
207
  def test_connection_env_check_side_hermetic_skips(tmp_path, monkeypatch):
200
208
  monkeypatch.setenv("DATABASE_URL", "postgres://check/db")
201
209
  app = _make_conn_check_app(tmp_path)
202
- r = app.test(["--hermetic", "check", "--tag", "db"])
210
+ r = app.test(["--hermetic", "check", "--tag", "db", "--verbose"])
203
211
  assert "SKIP" in r.stdout
204
212
  assert "dsn=" not in r.stdout
213
+ # The check SEES hermetic (is_hermetic()==True) and skips for that reason.
214
+ assert "hermetic=True" in r.stdout
215
+ assert "suppressed by --hermetic" in r.stdout
216
+
217
+
218
+ def test_connection_env_check_side_hermetic_conflation(tmp_path, monkeypatch):
219
+ """Documents the exact gap is_hermetic() closes: under --hermetic with the
220
+ env var UNSET, connection_env_value returns present=False (indistinguishable
221
+ from a plain unset), yet is_hermetic() returns True -- so a check layering
222
+ config fallbacks can honor hermetic instead of connecting via a config URL."""
223
+ monkeypatch.delenv("DATABASE_URL", raising=False) # env UNSET
224
+ app = _make_conn_check_app(tmp_path)
225
+ r = app.test(["--hermetic", "check", "--tag", "db", "--verbose"])
226
+ assert "hermetic=True" in r.stdout
227
+ assert "suppressed by --hermetic" in r.stdout
228
+ assert "DATABASE_URL unset" not in r.stdout
229
+
230
+
231
+ def test_connection_env_check_side_unset_not_hermetic(tmp_path, monkeypatch):
232
+ """Counterpart: env unset and NOT hermetic -> is_hermetic()==False, so a
233
+ consumer is free to consult a config fallback (here reported as plain unset)."""
234
+ monkeypatch.delenv("DATABASE_URL", raising=False) # env UNSET, no --hermetic
235
+ app = _make_conn_check_app(tmp_path)
236
+ r = app.test(["check", "--tag", "db", "--verbose"])
237
+ assert "hermetic=False" in r.stdout
238
+ assert "DATABASE_URL unset" in r.stdout
205
239
 
206
240
 
207
241
  # --- Registration-time enforcement ---
@@ -232,7 +232,7 @@ wheels = [
232
232
 
233
233
  [[package]]
234
234
  name = "strictcli"
235
- version = "0.33.0"
235
+ version = "0.34.0"
236
236
  source = { editable = "." }
237
237
  dependencies = [
238
238
  { name = "tomlkit" },
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