etlplus 0.4.6__tar.gz → 0.4.7__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 (130) hide show
  1. {etlplus-0.4.6 → etlplus-0.4.7}/.pre-commit-config.yaml +5 -1
  2. {etlplus-0.4.6/etlplus.egg-info → etlplus-0.4.7}/PKG-INFO +1 -1
  3. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/cli/main.py +131 -26
  4. {etlplus-0.4.6 → etlplus-0.4.7/etlplus.egg-info}/PKG-INFO +1 -1
  5. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/cli/test_u_cli_main.py +44 -6
  6. {etlplus-0.4.6 → etlplus-0.4.7}/.coveragerc +0 -0
  7. {etlplus-0.4.6 → etlplus-0.4.7}/.editorconfig +0 -0
  8. {etlplus-0.4.6 → etlplus-0.4.7}/.gitattributes +0 -0
  9. {etlplus-0.4.6 → etlplus-0.4.7}/.github/actions/python-bootstrap/action.yml +0 -0
  10. {etlplus-0.4.6 → etlplus-0.4.7}/.github/workflows/ci.yml +0 -0
  11. {etlplus-0.4.6 → etlplus-0.4.7}/.gitignore +0 -0
  12. {etlplus-0.4.6 → etlplus-0.4.7}/.ruff.toml +0 -0
  13. {etlplus-0.4.6 → etlplus-0.4.7}/CODE_OF_CONDUCT.md +0 -0
  14. {etlplus-0.4.6 → etlplus-0.4.7}/CONTRIBUTING.md +0 -0
  15. {etlplus-0.4.6 → etlplus-0.4.7}/DEMO.md +0 -0
  16. {etlplus-0.4.6 → etlplus-0.4.7}/LICENSE +0 -0
  17. {etlplus-0.4.6 → etlplus-0.4.7}/Makefile +0 -0
  18. {etlplus-0.4.6 → etlplus-0.4.7}/README.md +0 -0
  19. {etlplus-0.4.6 → etlplus-0.4.7}/REFERENCES.md +0 -0
  20. {etlplus-0.4.6 → etlplus-0.4.7}/docs/pipeline-guide.md +0 -0
  21. {etlplus-0.4.6 → etlplus-0.4.7}/docs/snippets/installation_version.md +0 -0
  22. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/__init__.py +0 -0
  23. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/__main__.py +0 -0
  24. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/__version__.py +0 -0
  25. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/README.md +0 -0
  26. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/__init__.py +0 -0
  27. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/auth.py +0 -0
  28. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/config.py +0 -0
  29. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/endpoint_client.py +0 -0
  30. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/errors.py +0 -0
  31. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/pagination/__init__.py +0 -0
  32. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/pagination/client.py +0 -0
  33. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/pagination/config.py +0 -0
  34. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/pagination/paginator.py +0 -0
  35. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/rate_limiting/__init__.py +0 -0
  36. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/rate_limiting/config.py +0 -0
  37. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/rate_limiting/rate_limiter.py +0 -0
  38. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/request_manager.py +0 -0
  39. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/retry_manager.py +0 -0
  40. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/transport.py +0 -0
  41. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/api/types.py +0 -0
  42. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/cli/__init__.py +0 -0
  43. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/cli/app.py +0 -0
  44. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/cli/handlers.py +0 -0
  45. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/config/__init__.py +0 -0
  46. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/config/connector.py +0 -0
  47. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/config/jobs.py +0 -0
  48. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/config/pipeline.py +0 -0
  49. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/config/profile.py +0 -0
  50. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/config/types.py +0 -0
  51. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/config/utils.py +0 -0
  52. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/enums.py +0 -0
  53. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/extract.py +0 -0
  54. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/file.py +0 -0
  55. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/load.py +0 -0
  56. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/mixins.py +0 -0
  57. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/py.typed +0 -0
  58. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/run.py +0 -0
  59. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/run_helpers.py +0 -0
  60. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/transform.py +0 -0
  61. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/types.py +0 -0
  62. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/utils.py +0 -0
  63. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/validate.py +0 -0
  64. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/validation/__init__.py +0 -0
  65. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus/validation/utils.py +0 -0
  66. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus.egg-info/SOURCES.txt +0 -0
  67. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus.egg-info/dependency_links.txt +0 -0
  68. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus.egg-info/entry_points.txt +0 -0
  69. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus.egg-info/requires.txt +0 -0
  70. {etlplus-0.4.6 → etlplus-0.4.7}/etlplus.egg-info/top_level.txt +0 -0
  71. {etlplus-0.4.6 → etlplus-0.4.7}/examples/README.md +0 -0
  72. {etlplus-0.4.6 → etlplus-0.4.7}/examples/configs/pipeline.yml +0 -0
  73. {etlplus-0.4.6 → etlplus-0.4.7}/examples/data/sample.csv +0 -0
  74. {etlplus-0.4.6 → etlplus-0.4.7}/examples/data/sample.json +0 -0
  75. {etlplus-0.4.6 → etlplus-0.4.7}/examples/data/sample.xml +0 -0
  76. {etlplus-0.4.6 → etlplus-0.4.7}/examples/data/sample.xsd +0 -0
  77. {etlplus-0.4.6 → etlplus-0.4.7}/examples/data/sample.yaml +0 -0
  78. {etlplus-0.4.6 → etlplus-0.4.7}/examples/quickstart_python.py +0 -0
  79. {etlplus-0.4.6 → etlplus-0.4.7}/pyproject.toml +0 -0
  80. {etlplus-0.4.6 → etlplus-0.4.7}/pytest.ini +0 -0
  81. {etlplus-0.4.6 → etlplus-0.4.7}/setup.cfg +0 -0
  82. {etlplus-0.4.6 → etlplus-0.4.7}/setup.py +0 -0
  83. {etlplus-0.4.6 → etlplus-0.4.7}/tests/__init__.py +0 -0
  84. {etlplus-0.4.6 → etlplus-0.4.7}/tests/conftest.py +0 -0
  85. {etlplus-0.4.6 → etlplus-0.4.7}/tests/integration/conftest.py +0 -0
  86. {etlplus-0.4.6 → etlplus-0.4.7}/tests/integration/test_i_cli.py +0 -0
  87. {etlplus-0.4.6 → etlplus-0.4.7}/tests/integration/test_i_examples_data_parity.py +0 -0
  88. {etlplus-0.4.6 → etlplus-0.4.7}/tests/integration/test_i_pagination_strategy.py +0 -0
  89. {etlplus-0.4.6 → etlplus-0.4.7}/tests/integration/test_i_pipeline_smoke.py +0 -0
  90. {etlplus-0.4.6 → etlplus-0.4.7}/tests/integration/test_i_pipeline_yaml_load.py +0 -0
  91. {etlplus-0.4.6 → etlplus-0.4.7}/tests/integration/test_i_run.py +0 -0
  92. {etlplus-0.4.6 → etlplus-0.4.7}/tests/integration/test_i_run_profile_pagination_defaults.py +0 -0
  93. {etlplus-0.4.6 → etlplus-0.4.7}/tests/integration/test_i_run_profile_rate_limit_defaults.py +0 -0
  94. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/api/conftest.py +0 -0
  95. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/api/test_u_auth.py +0 -0
  96. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/api/test_u_config.py +0 -0
  97. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/api/test_u_endpoint_client.py +0 -0
  98. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/api/test_u_mocks.py +0 -0
  99. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/api/test_u_pagination_client.py +0 -0
  100. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/api/test_u_pagination_config.py +0 -0
  101. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/api/test_u_paginator.py +0 -0
  102. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/api/test_u_rate_limit_config.py +0 -0
  103. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/api/test_u_rate_limiter.py +0 -0
  104. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/api/test_u_request_manager.py +0 -0
  105. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/api/test_u_retry_manager.py +0 -0
  106. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/api/test_u_transport.py +0 -0
  107. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/api/test_u_types.py +0 -0
  108. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/cli/conftest.py +0 -0
  109. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/cli/test_u_cli_app.py +0 -0
  110. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/cli/test_u_cli_handlers.py +0 -0
  111. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/config/test_u_config_utils.py +0 -0
  112. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/config/test_u_connector.py +0 -0
  113. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/config/test_u_jobs.py +0 -0
  114. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/config/test_u_pipeline.py +0 -0
  115. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/conftest.py +0 -0
  116. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/test_u_enums.py +0 -0
  117. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/test_u_extract.py +0 -0
  118. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/test_u_file.py +0 -0
  119. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/test_u_load.py +0 -0
  120. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/test_u_main.py +0 -0
  121. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/test_u_mixins.py +0 -0
  122. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/test_u_run.py +0 -0
  123. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/test_u_run_helpers.py +0 -0
  124. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/test_u_transform.py +0 -0
  125. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/test_u_utils.py +0 -0
  126. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/test_u_validate.py +0 -0
  127. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/test_u_version.py +0 -0
  128. {etlplus-0.4.6 → etlplus-0.4.7}/tests/unit/validation/test_u_validation_utils.py +0 -0
  129. {etlplus-0.4.6 → etlplus-0.4.7}/tools/run_pipeline.py +0 -0
  130. {etlplus-0.4.6 → etlplus-0.4.7}/tools/update_demo_snippets.py +0 -0
@@ -159,7 +159,11 @@ repos:
159
159
  rev: v1.19.0
160
160
  hooks:
161
161
  - id: mypy
162
- args: [--ignore-missing-imports, --install-types, --non-interactive]
162
+ args:
163
+ - --cache-dir=.mypy_cache/pre-commit
164
+ - --ignore-missing-imports
165
+ - --install-types
166
+ - --non-interactive
163
167
 
164
168
  - repo: https://github.com/pycqa/flake8
165
169
  rev: 7.3.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: etlplus
3
- Version: 0.4.6
3
+ Version: 0.4.7
4
4
  Summary: A Swiss Army knife for simple ETL operations
5
5
  Home-page: https://github.com/Dagitali/ETLPlus
6
6
  Author: ETLPlus Team
@@ -10,10 +10,12 @@ This module exposes :func:`main` for the console script as well as
10
10
  from __future__ import annotations
11
11
 
12
12
  import argparse
13
+ import contextlib
13
14
  import sys
14
15
  from collections.abc import Sequence
15
16
  from typing import Literal
16
17
 
18
+ import click
17
19
  import typer
18
20
 
19
21
  from .. import __version__
@@ -68,6 +70,32 @@ class _FormatAction(argparse.Action):
68
70
  # SECTION: INTERNAL FUNCTIONS =============================================== #
69
71
 
70
72
 
73
+ def _add_boolean_flag(
74
+ parser: argparse.ArgumentParser,
75
+ *,
76
+ name: str,
77
+ help_text: str,
78
+ ) -> None:
79
+ """Add a toggle that also supports the ``--no-`` prefix via 3.13.
80
+
81
+ Parameters
82
+ ----------
83
+ parser : argparse.ArgumentParser
84
+ Parser receiving the flag.
85
+ name : str
86
+ Primary flag name without leading dashes.
87
+ help_text : str
88
+ Help text rendered in ``--help`` output.
89
+ """
90
+
91
+ parser.add_argument(
92
+ f'--{name}',
93
+ action=argparse.BooleanOptionalAction,
94
+ default=False,
95
+ help=help_text,
96
+ )
97
+
98
+
71
99
  def _add_config_option(
72
100
  parser: argparse.ArgumentParser,
73
101
  *,
@@ -129,32 +157,6 @@ def _add_format_options(
129
157
  )
130
158
 
131
159
 
132
- def _add_boolean_flag(
133
- parser: argparse.ArgumentParser,
134
- *,
135
- name: str,
136
- help_text: str,
137
- ) -> None:
138
- """Add a toggle that also supports the ``--no-`` prefix via 3.13.
139
-
140
- Parameters
141
- ----------
142
- parser : argparse.ArgumentParser
143
- Parser receiving the flag.
144
- name : str
145
- Primary flag name without leading dashes.
146
- help_text : str
147
- Help text rendered in ``--help`` output.
148
- """
149
-
150
- parser.add_argument(
151
- f'--{name}',
152
- action=argparse.BooleanOptionalAction,
153
- default=False,
154
- help=help_text,
155
- )
156
-
157
-
158
160
  def _cli_description() -> str:
159
161
  return '\n'.join(
160
162
  [
@@ -188,6 +190,93 @@ def _cli_epilog() -> str:
188
190
  )
189
191
 
190
192
 
193
+ def _emit_context_help(
194
+ ctx: click.Context | None,
195
+ ) -> bool:
196
+ """
197
+ Mirror Click help output for the provided context onto stderr.
198
+
199
+ Parameters
200
+ ----------
201
+ ctx : click.Context | None
202
+ The Click context to emit help for.
203
+
204
+ Returns
205
+ -------
206
+ bool
207
+ ``True`` when help was emitted, ``False`` when ``ctx`` was ``None``.
208
+ """
209
+ if ctx is None:
210
+ return False
211
+
212
+ with contextlib.redirect_stdout(sys.stderr):
213
+ ctx.get_help()
214
+ return True
215
+
216
+
217
+ def _emit_root_help(
218
+ command: click.Command,
219
+ ) -> None:
220
+ """
221
+ Print the root ``etlplus`` help text to stderr.
222
+
223
+ Parameters
224
+ ----------
225
+ command : click.Command
226
+ The root Typer/Click command.
227
+ """
228
+ ctx = command.make_context('etlplus', [], resilient_parsing=True)
229
+ try:
230
+ _emit_context_help(ctx)
231
+ finally:
232
+ ctx.close()
233
+
234
+
235
+ def _is_illegal_option_error(
236
+ exc: click.exceptions.UsageError,
237
+ ) -> bool:
238
+ """
239
+ Return ``True`` when usage errors stem from invalid options.
240
+
241
+ Parameters
242
+ ----------
243
+ exc : click.exceptions.UsageError
244
+ The usage error to inspect.
245
+
246
+ Returns
247
+ -------
248
+ bool
249
+ ``True`` when the error indicates illegal options.
250
+ """
251
+ return isinstance(
252
+ exc,
253
+ (
254
+ click.exceptions.BadOptionUsage,
255
+ click.exceptions.NoSuchOption,
256
+ ),
257
+ )
258
+
259
+
260
+ def _is_unknown_command_error(
261
+ exc: click.exceptions.UsageError,
262
+ ) -> bool:
263
+ """
264
+ Return ``True`` when a :class:`UsageError` indicates bad subcommand.
265
+
266
+ Parameters
267
+ ----------
268
+ exc : click.exceptions.UsageError
269
+ The usage error to inspect.
270
+
271
+ Returns
272
+ -------
273
+ bool
274
+ ``True`` when the error indicates an unknown command.
275
+ """
276
+ message = getattr(exc, 'message', None) or str(exc)
277
+ return message.startswith('No such command ')
278
+
279
+
191
280
  # SECTION: FUNCTIONS ======================================================== #
192
281
 
193
282
 
@@ -422,6 +511,9 @@ def main(
422
511
 
423
512
  Raises
424
513
  ------
514
+ click.exceptions.UsageError
515
+ Re-raises Typer/Click usage errors after printing help for unknown
516
+ commands.
425
517
  SystemExit
426
518
  Re-raises SystemExit exceptions to preserve exit codes.
427
519
 
@@ -442,6 +534,19 @@ def main(
442
534
  )
443
535
  return int(result or 0)
444
536
 
537
+ except click.exceptions.UsageError as exc:
538
+ if _is_unknown_command_error(exc):
539
+ typer.echo(f'Error: {exc}', err=True)
540
+ _emit_root_help(command)
541
+ return int(getattr(exc, 'exit_code', 2))
542
+ if _is_illegal_option_error(exc):
543
+ typer.echo(f'Error: {exc}', err=True)
544
+ if not _emit_context_help(exc.ctx):
545
+ _emit_root_help(command)
546
+ return int(getattr(exc, 'exit_code', 2))
547
+
548
+ raise
549
+
445
550
  except typer.Exit as exc:
446
551
  return int(exc.exit_code)
447
552
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: etlplus
3
- Version: 0.4.6
3
+ Version: 0.4.7
4
4
  Summary: A Swiss Army knife for simple ETL operations
5
5
  Home-page: https://github.com/Dagitali/ETLPlus
6
6
  Author: ETLPlus Team
@@ -60,6 +60,7 @@ class TestCreateParser:
60
60
 
61
61
  def test_extract_parser_sets_handler_and_format_flag(self) -> None:
62
62
  """Extract parser should bind handlers and flag explicit formats."""
63
+ # pylint: disable=protected-access
63
64
 
64
65
  parser = cli_main_module.create_parser()
65
66
  namespace = parser.parse_args(
@@ -74,7 +75,6 @@ class TestCreateParser:
74
75
 
75
76
  def test_list_parser_supports_boolean_flags(self) -> None:
76
77
  """List parser should surface boolean flag wiring."""
77
-
78
78
  parser = cli_main_module.create_parser()
79
79
  namespace = parser.parse_args(
80
80
  ['list', '--config', 'pipelines.yml', '--targets', '--transforms'],
@@ -95,7 +95,6 @@ class TestMain:
95
95
  monkeypatch: pytest.MonkeyPatch,
96
96
  ) -> None:
97
97
  """Test that the command return value is normalized into an ``int``."""
98
-
99
98
  captured: dict[str, object] = {}
100
99
 
101
100
  def _action(**kwargs: object) -> object:
@@ -161,7 +160,8 @@ class TestMain:
161
160
  monkeypatch: pytest.MonkeyPatch,
162
161
  ) -> None:
163
162
  """
164
- ``typer.Abort`` should surface as a generic failure (exit code 1).
163
+ Test that ``typer.Abort`` propagates as a generic failure (exit code
164
+ 1).
165
165
  """
166
166
 
167
167
  def _action(**kwargs: object) -> object: # noqa: ARG001
@@ -175,7 +175,7 @@ class TestMain:
175
175
  self,
176
176
  monkeypatch: pytest.MonkeyPatch,
177
177
  ) -> None:
178
- """``typer.Exit`` should propagate its exit code."""
178
+ """Test that ``typer.Exit`` propagates its exit code."""
179
179
 
180
180
  def _action(**kwargs: object) -> object: # noqa: ARG001
181
181
  raise typer.Exit(17)
@@ -188,14 +188,52 @@ class TestMain:
188
188
  """Test that no args prints help and exits with exit code 0."""
189
189
  assert cli_main([]) == 0
190
190
 
191
+ def test_unknown_subcommand_emits_usage(
192
+ self,
193
+ capsys: pytest.CaptureFixture[str],
194
+ ) -> None:
195
+ """Test that illegal subcommands show help and exit with code 2."""
196
+ exit_code = cli_main(['definitely-not-real'])
197
+ captured = capsys.readouterr()
198
+
199
+ assert exit_code == 2
200
+ assert 'No such command' in captured.err
201
+ assert 'Usage:' in captured.err
202
+
203
+ def test_unknown_root_option_emits_usage(
204
+ self,
205
+ capsys: pytest.CaptureFixture[str],
206
+ ) -> None:
207
+ """Test that Unknown root options echo usage details to stderr."""
208
+
209
+ exit_code = cli_main(['--definitely-not-real-option'])
210
+ captured = capsys.readouterr()
211
+
212
+ assert exit_code == 2
213
+ assert 'No such option' in captured.err
214
+ assert 'Usage:' in captured.err
215
+
216
+ def test_unknown_subcommand_option_emits_usage(
217
+ self,
218
+ capsys: pytest.CaptureFixture[str],
219
+ ) -> None:
220
+ """Test that unknown subcommand options surface usage help."""
221
+
222
+ exit_code = cli_main(['extract', '--definitely-not-real-option'])
223
+ captured = capsys.readouterr()
224
+
225
+ assert exit_code == 2
226
+ assert 'No such option' in captured.err
227
+ assert 'Usage:' in captured.err
228
+
191
229
  def test_value_error_returns_exit_code_1(
192
230
  self,
193
231
  monkeypatch: pytest.MonkeyPatch,
194
232
  capsys: pytest.CaptureFixture[str],
195
233
  ) -> None:
196
234
  """
197
- Test that :class:`ValueError` from a command maps to exit code 1."""
198
-
235
+ Test that :class:`ValueError` from a command maps to exit code 1.
236
+ """
199
237
  monkeypatch.setattr(
200
238
  cli_app_module,
201
239
  'cmd_extract',
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
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
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
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
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
File without changes
File without changes
File without changes
File without changes