lib-cli-exit-tools 2.3.3__tar.gz → 2.3.4__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 (56) hide show
  1. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/CHANGELOG.md +7 -0
  2. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/Makefile +2 -2
  3. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/PKG-INFO +3 -3
  4. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/pyproject.toml +3 -3
  5. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/__init__conf__.py +1 -1
  6. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/application/runner.py +19 -6
  7. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/tests/test_application_runner.py +93 -0
  8. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.codacy/cli-config.yaml +0 -0
  9. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.codacy/cli.sh +0 -0
  10. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.codacy/codacy.yaml +0 -0
  11. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.codacy/tools-configs/lizard.yaml +0 -0
  12. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.codacy/tools-configs/trivy.yaml +0 -0
  13. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.data_arch_violations.json +0 -0
  14. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.devcontainer/devcontainer.json +0 -0
  15. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.devcontainer/settings.json +0 -0
  16. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.env.example +0 -0
  17. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.github/actions/extract-metadata/action.yml +0 -0
  18. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.github/dependabot.yml +0 -0
  19. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.github/workflows/codeql.yml +0 -0
  20. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.github/workflows/default_cicd_public.yml +0 -0
  21. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.github/workflows/default_release_public.yml +0 -0
  22. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.gitignore +0 -0
  23. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.qlty/qlty.toml +0 -0
  24. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/.snyk +0 -0
  25. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/CONTRIBUTING.md +0 -0
  26. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/DEVELOPMENT.md +0 -0
  27. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/INSTALL.md +0 -0
  28. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/LICENSE +0 -0
  29. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/README.md +0 -0
  30. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/codecov.yml +0 -0
  31. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/docs/system-design/module_reference.md +0 -0
  32. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/notebooks/Quickstart.ipynb +0 -0
  33. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/__init__.py +0 -0
  34. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/__main__.py +0 -0
  35. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/adapters/__init__.py +0 -0
  36. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/adapters/signals.py +0 -0
  37. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/application/__init__.py +0 -0
  38. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/cli/__init__.py +0 -0
  39. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/cli/commands.py +0 -0
  40. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/cli/group.py +0 -0
  41. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/cli/styling.py +0 -0
  42. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/cli/typed_click.py +0 -0
  43. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/core/__init__.py +0 -0
  44. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/core/configuration.py +0 -0
  45. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/core/exit_codes.py +0 -0
  46. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/lib_cli_exit_tools.py +0 -0
  47. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/src/lib_cli_exit_tools/py.typed +0 -0
  48. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/tests/conftest.py +0 -0
  49. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/tests/test_adapters_signals.py +0 -0
  50. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/tests/test_behaviors.py +0 -0
  51. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/tests/test_cli.py +0 -0
  52. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/tests/test_core_configuration.py +0 -0
  53. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/tests/test_core_exit_codes.py +0 -0
  54. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/tests/test_metadata.py +0 -0
  55. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/tests/test_module_entry.py +0 -0
  56. {lib_cli_exit_tools-2.3.3 → lib_cli_exit_tools-2.3.4}/tests/test_signals_integration.py +0 -0
@@ -4,6 +4,13 @@ All notable changes to this project are documented here. The format follows [Kee
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.3.4] 2026-07-29 15:20:59
8
+
9
+ ### Fixed
10
+ - `run_cli()` now returns the exit code a command chose with `ctx.exit(...)` instead of always reporting `0` on the non-exception path. Click is invoked with `standalone_mode=False`, which makes it *return* that code rather than raising it, and `_run_command_with_handler` discarded the value before returning a hardcoded `0`. Only the exception path could therefore produce a non-zero status, so a command that completed normally while reporting a negative *answer* - a host that did not respond, a search that found nothing, anything distinguishing "ran fine, result is no" from "failed" - silently exited `0`. `ClickCommand.main` is annotated `-> object` to match what Click actually returns. A command that simply completes still yields `0`, since Click returns `None` there.
11
+
12
+ No released behaviour changes for existing users: a sweep of every project in this tree found no other consumer calling `ctx.exit()` with a non-zero code, no test pinned the previous behaviour, and the documented `run_cli` contract in the README already described executing the command with `standalone_mode=False` without mentioning that its result was dropped.
13
+
7
14
  ## [2.3.3] 2026-07-24 16:09:16
8
15
 
9
16
  ### Fixed
@@ -1,4 +1,4 @@
1
- # BMK MAKEFILE 3.13.0
1
+ # BMK MAKEFILE 3.13.2
2
2
  # do not alter this file - it might be overwritten on new versions of BMK
3
3
  # if You want to alter it, remove the first line # BMK MAKEFILE 1.0 - then it is a custom makefile and will not be overwritten
4
4
  # bmk Makefile - thin wrapper using `uv tool upgrade` for persistent bmk
@@ -186,7 +186,7 @@ endif
186
186
  # BMK_MIN floors the FALLBACK install. The upgrade path does not consult it: uv re-resolves
187
187
  # the requirement recorded at install time, which is itself a `bmk>=...`, so it always lands
188
188
  # on the newest release and clears the floor anyway.
189
- BMK_MIN := 3.13.0
189
+ BMK_MIN := 3.13.2
190
190
 
191
191
  # Absent env, missing check, or damaged env - all three mean "do not upgrade, rebuild".
192
192
  # `test -x` keeps a fresh machine quiet: there is no interpreter to run yet.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lib_cli_exit_tools
3
- Version: 2.3.3
3
+ Version: 2.3.4
4
4
  Summary: CLI exit handling helpers: clean signals, exit codes, and error printing
5
5
  Project-URL: Homepage, https://github.com/bitranox/lib_cli_exit_tools
6
6
  Project-URL: Repository, https://github.com/bitranox/lib_cli_exit_tools.git
@@ -27,7 +27,7 @@ Requires-Dist: bandit>=1.9.4; extra == 'dev'
27
27
  Requires-Dist: build>=1.5.0; extra == 'dev'
28
28
  Requires-Dist: click>=8.4.2; extra == 'dev'
29
29
  Requires-Dist: coverage[toml]>=7.15.2; extra == 'dev'
30
- Requires-Dist: hypothesis>=6.161.2; extra == 'dev'
30
+ Requires-Dist: hypothesis>=6.163.0; extra == 'dev'
31
31
  Requires-Dist: import-linter>=2.13; extra == 'dev'
32
32
  Requires-Dist: pip-audit>=2.10.1; extra == 'dev'
33
33
  Requires-Dist: pyright[nodejs]>=1.1.411; extra == 'dev'
@@ -37,7 +37,7 @@ Requires-Dist: pytest>=9.1.1; extra == 'dev'
37
37
  Requires-Dist: rtoml>=0.13.0; extra == 'dev'
38
38
  Requires-Dist: ruff>=0.16.0; extra == 'dev'
39
39
  Requires-Dist: textual>=8.2.8; extra == 'dev'
40
- Requires-Dist: twine>=6.2.0; extra == 'dev'
40
+ Requires-Dist: twine>=7.0.0; extra == 'dev'
41
41
  Description-Content-Type: text/markdown
42
42
 
43
43
  # lib_cli_exit_tools
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lib_cli_exit_tools"
3
- version = "2.3.3"
3
+ version = "2.3.4"
4
4
  description = "CLI exit handling helpers: clean signals, exit codes, and error printing"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -51,7 +51,7 @@ dev = [
51
51
  # Transitive CVE pins
52
52
  "click>=8.4.2", # CVE-2026-7246
53
53
  "coverage[toml]>=7.15.2",
54
- "hypothesis>=6.161.2",
54
+ "hypothesis>=6.163.0",
55
55
  "import-linter>=2.13",
56
56
  "bandit>=1.9.4",
57
57
  "pip-audit>=2.10.1",
@@ -62,7 +62,7 @@ dev = [
62
62
  "ruff>=0.16.0",
63
63
  "textual>=8.2.8",
64
64
  "rtoml>=0.13.0",
65
- "twine>=6.2.0"
65
+ "twine>=7.0.0"
66
66
  ]
67
67
 
68
68
  [project.scripts]
@@ -26,7 +26,7 @@ name = "lib_cli_exit_tools"
26
26
  #: Human-readable summary shown in CLI help output.
27
27
  title = "CLI exit handling helpers: clean signals, exit codes, and error printing"
28
28
  #: Current release version pulled from ``pyproject.toml`` by automation.
29
- version = "2.3.3"
29
+ version = "2.3.4"
30
30
  #: Repository homepage presented to users.
31
31
  homepage = "https://github.com/bitranox/lib_cli_exit_tools"
32
32
  #: Author attribution surfaced in CLI output.
@@ -60,7 +60,9 @@ class ClickCommand(Protocol):
60
60
  complete_var: str | None = ...,
61
61
  standalone_mode: bool = ..., # noqa: FBT001 - mirrors Click's actual positional main() signature
62
62
  **_: object,
63
- ) -> None: ...
63
+ ) -> object:
64
+ """Return Click's own result: ctx.exit()'s code, or None on success."""
65
+ ...
64
66
 
65
67
 
66
68
  __all__ = [
@@ -681,10 +683,15 @@ def _run_command_with_handler(
681
683
  ) -> int:
682
684
  """Invoke the Click command and delegate failures to ``handler``."""
683
685
  try:
684
- _invoke_command(cli, argv, prog_name)
686
+ outcome = _invoke_command(cli, argv, prog_name)
685
687
  except BaseException as exc: # single funnel for exit codes; handler translates every exception type
686
688
  return handler(exc)
687
- return 0
689
+ # Click is invoked with standalone_mode=False, which makes it *return*
690
+ # ctx.exit()'s code rather than raising it. That is how a command reports a
691
+ # negative answer - nothing found, host did not respond - as distinct from
692
+ # an error. Discarding this value would collapse every such outcome to
693
+ # success; None is Click's plain-success result and stays 0.
694
+ return outcome if isinstance(outcome, int) else 0
688
695
 
689
696
 
690
697
  def _finalise_cli_run(restorer: Callable[[], None] | None) -> None:
@@ -693,9 +700,15 @@ def _finalise_cli_run(restorer: Callable[[], None] | None) -> None:
693
700
  flush_streams()
694
701
 
695
702
 
696
- def _invoke_command(cli: ClickCommand, argv: Sequence[str] | None, prog_name: str | None) -> None:
697
- """Invoke the Click command with ``standalone_mode`` disabled."""
698
- cli.main(args=_normalised_args(argv), standalone_mode=False, prog_name=prog_name)
703
+ def _invoke_command(cli: ClickCommand, argv: Sequence[str] | None, prog_name: str | None) -> object:
704
+ """Invoke the Click command with ``standalone_mode`` disabled.
705
+
706
+ Returns:
707
+ Whatever Click returns: the code passed to ``ctx.exit()``, or ``None``
708
+ when the command simply completed.
709
+ """
710
+
711
+ return cli.main(args=_normalised_args(argv), standalone_mode=False, prog_name=prog_name)
699
712
 
700
713
 
701
714
  def _normalised_args(argv: Sequence[str] | None) -> Sequence[str] | None:
@@ -476,3 +476,96 @@ def test_run_cli_skips_signal_installation_when_disabled(monkeypatch: pytest.Mon
476
476
  runner.run_cli(DummyCommand(lambda: None), install_signals=False)
477
477
 
478
478
  assert called == []
479
+
480
+
481
+ # =============================================================================
482
+ # Exit codes a command chose deliberately
483
+ # =============================================================================
484
+
485
+
486
+ def _no_signal_handlers(monkeypatch: pytest.MonkeyPatch) -> None:
487
+ """Keep these tests from touching process-wide signal state."""
488
+
489
+ def install(_specs: Sequence[SignalSpec]) -> Callable[[], None]:
490
+ def restore() -> None:
491
+ return None
492
+
493
+ return restore
494
+
495
+ monkeypatch.setattr(runner, "install_signal_handlers", install)
496
+
497
+
498
+ def _command_exiting_with(code: int) -> click.Command:
499
+ """Return a real Click command that deliberately exits with ``code``.
500
+
501
+ A real command rather than the stub above, because the stub cannot
502
+ reproduce Click's return-value behaviour and that is exactly what is
503
+ under test here.
504
+ """
505
+
506
+ @click.pass_context
507
+ def body(ctx: click.Context) -> None:
508
+ ctx.exit(code)
509
+
510
+ return click.command()(body)
511
+
512
+
513
+ def _group_with_subcommand_exiting_with(code: int) -> click.Group:
514
+ """Return a group whose subcommand exits with ``code``."""
515
+
516
+ @click.group()
517
+ def app() -> None: ...
518
+
519
+ @click.pass_context
520
+ def body(ctx: click.Context) -> None:
521
+ ctx.exit(code)
522
+
523
+ app.add_command(click.command("check")(body))
524
+ return app
525
+
526
+
527
+ @pytest.mark.os_agnostic
528
+ def test_run_cli_propagates_a_deliberate_non_zero_exit(monkeypatch: pytest.MonkeyPatch) -> None:
529
+ """A command that calls ctx.exit(1) must exit 1, not 0.
530
+
531
+ ``ctx.exit`` is how a Click command reports a negative *answer* - a host
532
+ that did not respond, a search that found nothing - as distinct from an
533
+ error. With ``standalone_mode=False`` Click returns that code rather than
534
+ raising it, so it only reaches the caller if the return value is passed on.
535
+ Dropping it collapses every such outcome into success.
536
+ """
537
+
538
+ _no_signal_handlers(monkeypatch)
539
+
540
+ assert runner.run_cli(_command_exiting_with(1), [], prog_name="t") == 1
541
+
542
+
543
+ @pytest.mark.os_agnostic
544
+ @pytest.mark.parametrize("code", [1, 2, 3, 42])
545
+ def test_run_cli_propagates_every_deliberate_exit_code(monkeypatch: pytest.MonkeyPatch, code: int) -> None:
546
+ """Whatever code the command chose is the code the caller receives."""
547
+
548
+ _no_signal_handlers(monkeypatch)
549
+
550
+ assert runner.run_cli(_command_exiting_with(code), [], prog_name="t") == code
551
+
552
+
553
+ @pytest.mark.os_agnostic
554
+ def test_run_cli_still_returns_zero_for_a_command_that_just_returns(monkeypatch: pytest.MonkeyPatch) -> None:
555
+ """Click returns None on a plain success, which must still mean zero."""
556
+
557
+ _no_signal_handlers(monkeypatch)
558
+
559
+ def body() -> None:
560
+ click.echo("done")
561
+
562
+ assert runner.run_cli(click.command()(body), [], prog_name="t") == 0
563
+
564
+
565
+ @pytest.mark.os_agnostic
566
+ def test_run_cli_propagates_a_deliberate_exit_from_a_subcommand(monkeypatch: pytest.MonkeyPatch) -> None:
567
+ """The same holds one level down, which is where real CLIs exit from."""
568
+
569
+ _no_signal_handlers(monkeypatch)
570
+
571
+ assert runner.run_cli(_group_with_subcommand_exiting_with(1), ["check"], prog_name="t") == 1