cli-command-parser 2024.12.15__tar.gz → 2025.6.14__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 (70) hide show
  1. {cli_command_parser-2024.12.15/lib/cli_command_parser.egg-info → cli_command_parser-2025.6.14}/PKG-INFO +134 -130
  2. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/__init__.py +25 -25
  3. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/__version__.py +2 -2
  4. cli_command_parser-2025.6.14/lib/cli_command_parser/conversion/__init__.py +12 -0
  5. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/conversion/cli.py +1 -1
  6. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/documentation.py +17 -1
  7. cli_command_parser-2025.6.14/lib/cli_command_parser/error_handling/__init__.py +38 -0
  8. cli_command_parser-2024.12.15/lib/cli_command_parser/error_handling.py → cli_command_parser-2025.6.14/lib/cli_command_parser/error_handling/base.py +15 -68
  9. cli_command_parser-2025.6.14/lib/cli_command_parser/error_handling/other.py +20 -0
  10. cli_command_parser-2025.6.14/lib/cli_command_parser/error_handling/windows.py +55 -0
  11. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/formatting/params.py +3 -0
  12. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/inputs/__init__.py +8 -8
  13. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/inputs/choices.py +8 -2
  14. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/inputs/exceptions.py +1 -1
  15. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/inputs/time.py +77 -72
  16. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/inputs/utils.py +2 -2
  17. cli_command_parser-2025.6.14/lib/cli_command_parser/parameters/__init__.py +6 -0
  18. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/parameters/actions.py +2 -0
  19. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/parameters/options.py +9 -0
  20. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/parameters/positionals.py +3 -3
  21. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/testing.py +13 -14
  22. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14/lib/cli_command_parser.egg-info}/PKG-INFO +134 -130
  23. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser.egg-info/SOURCES.txt +4 -1
  24. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/readme.rst +4 -1
  25. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/setup.cfg +50 -50
  26. cli_command_parser-2024.12.15/lib/cli_command_parser/conversion/__init__.py +0 -3
  27. cli_command_parser-2024.12.15/lib/cli_command_parser/parameters/__init__.py +0 -6
  28. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/LICENSE +0 -0
  29. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/MANIFEST.in +0 -0
  30. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/entry_points.txt +0 -0
  31. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/__main__.py +0 -0
  32. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/annotations.py +0 -0
  33. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/command_parameters.py +0 -0
  34. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/commands.py +0 -0
  35. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/compat.py +0 -0
  36. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/config.py +0 -0
  37. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/context.py +0 -0
  38. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/conversion/__main__.py +0 -0
  39. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/conversion/argparse_ast.py +0 -0
  40. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/conversion/argparse_utils.py +0 -0
  41. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/conversion/command_builder.py +0 -0
  42. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/conversion/utils.py +0 -0
  43. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/conversion/visitor.py +0 -0
  44. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/core.py +0 -0
  45. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/exceptions.py +0 -0
  46. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/formatting/__init__.py +0 -0
  47. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/formatting/commands.py +0 -0
  48. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/formatting/restructured_text.py +0 -0
  49. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/formatting/utils.py +0 -0
  50. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/inputs/base.py +0 -0
  51. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/inputs/files.py +0 -0
  52. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/inputs/numeric.py +0 -0
  53. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/inputs/patterns.py +0 -0
  54. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/metadata.py +0 -0
  55. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/nargs.py +0 -0
  56. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/parameters/base.py +0 -0
  57. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/parameters/choice_map.py +0 -0
  58. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/parameters/groups.py +0 -0
  59. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/parameters/option_strings.py +0 -0
  60. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/parameters/pass_thru.py +0 -0
  61. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/parse_tree.py +0 -0
  62. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/parser.py +0 -0
  63. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/typing.py +0 -0
  64. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser/utils.py +0 -0
  65. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser.egg-info/dependency_links.txt +0 -0
  66. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser.egg-info/entry_points.txt +0 -0
  67. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser.egg-info/requires.txt +0 -0
  68. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/lib/cli_command_parser.egg-info/top_level.txt +0 -0
  69. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/pyproject.toml +0 -0
  70. {cli_command_parser-2024.12.15 → cli_command_parser-2025.6.14}/requirements-dev.txt +0 -0
@@ -1,130 +1,134 @@
1
- Metadata-Version: 2.1
2
- Name: cli_command_parser
3
- Version: 2024.12.15
4
- Summary: CLI Command Parser
5
- Home-page: https://github.com/dskrypa/cli_command_parser
6
- Author: Doug Skrypa
7
- Author-email: dskrypa@gmail.com
8
- License: Apache 2.0
9
- Project-URL: Source, https://github.com/dskrypa/cli_command_parser
10
- Project-URL: Documentation, https://dskrypa.github.io/cli_command_parser
11
- Project-URL: Issues, https://github.com/dskrypa/cli_command_parser/issues
12
- Classifier: Development Status :: 5 - Production/Stable
13
- Classifier: Environment :: Console
14
- Classifier: Intended Audience :: Developers
15
- Classifier: License :: OSI Approved :: Apache Software License
16
- Classifier: Operating System :: OS Independent
17
- Classifier: Programming Language :: Python
18
- Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.9
20
- Classifier: Programming Language :: Python :: 3.10
21
- Classifier: Programming Language :: Python :: 3.11
22
- Classifier: Programming Language :: Python :: 3.12
23
- Classifier: Programming Language :: Python :: 3.13
24
- Classifier: Topic :: Software Development :: User Interfaces
25
- Classifier: Topic :: Text Processing
26
- Requires-Python: >=3.9
27
- Description-Content-Type: text/x-rst
28
- License-File: LICENSE
29
- Provides-Extra: wcwidth
30
- Requires-Dist: wcwidth; extra == "wcwidth"
31
-
32
- CLI Command Parser
33
- ##################
34
-
35
- |downloads| |py_version| |coverage_badge| |build_status| |Ruff|
36
-
37
- .. |py_version| image:: https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20-blue
38
- :target: https://pypi.org/project/cli-command-parser/
39
-
40
- .. |coverage_badge| image:: https://codecov.io/gh/dskrypa/cli_command_parser/branch/main/graph/badge.svg
41
- :target: https://codecov.io/gh/dskrypa/cli_command_parser
42
-
43
- .. |build_status| image:: https://github.com/dskrypa/cli_command_parser/actions/workflows/run-tests.yml/badge.svg
44
- :target: https://github.com/dskrypa/cli_command_parser/actions/workflows/run-tests.yml
45
-
46
- .. |Ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
47
- :target: https://docs.astral.sh/ruff/
48
-
49
- .. |downloads| image:: https://img.shields.io/pypi/dm/cli-command-parser
50
- :target: https://pypistats.org/packages/cli-command-parser
51
-
52
-
53
- CLI Command Parser is a class-based CLI argument parser that defines parameters with descriptors. It provides the
54
- tools to quickly and easily get started with basic CLIs, and it scales well to support even very large and complex
55
- CLIs while remaining readable and easy to maintain.
56
-
57
- The primary goals of this project:
58
- - Make it easy to define subcommands and actions in an clean and organized manner
59
- - Allow for inheritance so that common parameters don't need to be repeated
60
- - Make it easy to handle common initialization tasks for all actions / subcommands once
61
- - Reduce the amount of boilerplate code that is necessary for setting up parsing and handling argument values
62
-
63
-
64
- Example Program
65
- ***************
66
-
67
- .. code-block:: python
68
-
69
- from cli_command_parser import Command, Option, main
70
-
71
- class Hello(Command, description='Simple greeting example'):
72
- name = Option('-n', default='World', help='The person to say hello to')
73
- count: int = Option('-c', default=1, help='Number of times to repeat the message')
74
-
75
- def main(self):
76
- for _ in range(self.count):
77
- print(f'Hello {self.name}!')
78
-
79
- if __name__ == '__main__':
80
- main()
81
-
82
-
83
- .. code-block:: shell-session
84
-
85
- $ hello_world.py --name Bob -c 3
86
- Hello Bob!
87
- Hello Bob!
88
- Hello Bob!
89
-
90
- $ hello_world.py -h
91
- usage: hello_world.py [--name NAME] [--count COUNT] [--help]
92
-
93
- Simple greeting example
94
-
95
- Optional arguments:
96
- --name NAME, -n NAME The person to say hello to (default: 'World')
97
- --count COUNT, -c COUNT Number of times to repeat the message (default: 1)
98
- --help, -h Show this help message and exit
99
-
100
-
101
- Installing CLI Command Parser
102
- *****************************
103
-
104
- CLI Command Parser can be installed and updated via `pip <https://pip.pypa.io/en/stable/getting-started/>`__::
105
-
106
- $ pip install -U cli-command-parser
107
-
108
-
109
- There are no required dependencies. Support for formatting wide characters correctly in help text descriptions can
110
- be included by adding `wcwidth <https://wcwidth.readthedocs.io>`__ to your project's requirements, and/or by installing
111
- with optional dependencies::
112
-
113
- $ pip install -U cli-command-parser[wcwidth]
114
-
115
-
116
- Python Version Compatibility
117
- ============================
118
-
119
- Python versions 3.9 and above are currently supported. The last release of CLI Command Parser that supported 3.8 was
120
- 2024-09-07. Support for Python 3.8 `officially ended on 2024-10-07 <https://devguide.python.org/versions/>`__.
121
-
122
-
123
- Links
124
- *****
125
-
126
- - Documentation: https://dskrypa.github.io/cli_command_parser/
127
- - Example Scripts: https://github.com/dskrypa/cli_command_parser/tree/main/examples
128
- - PyPI Releases: https://pypi.org/project/cli-command-parser/
129
- - Source Code: https://github.com/dskrypa/cli_command_parser
130
- - Issue Tracker: https://github.com/dskrypa/cli_command_parser/issues
1
+ Metadata-Version: 2.4
2
+ Name: cli_command_parser
3
+ Version: 2025.6.14
4
+ Summary: CLI Command Parser
5
+ Home-page: https://github.com/dskrypa/cli_command_parser
6
+ Author: Doug Skrypa
7
+ Author-email: dskrypa@gmail.com
8
+ License: Apache 2.0
9
+ Project-URL: Source, https://github.com/dskrypa/cli_command_parser
10
+ Project-URL: Documentation, https://dskrypa.github.io/cli_command_parser
11
+ Project-URL: Issues, https://github.com/dskrypa/cli_command_parser/issues
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Software Development :: User Interfaces
25
+ Classifier: Topic :: Text Processing
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/x-rst
28
+ License-File: LICENSE
29
+ Provides-Extra: wcwidth
30
+ Requires-Dist: wcwidth; extra == "wcwidth"
31
+ Dynamic: license-file
32
+
33
+ CLI Command Parser
34
+ ##################
35
+
36
+ |downloads| |py_version| |coverage_badge| |build_status| |Ruff| |OpenSSF Best Practices|
37
+
38
+ .. |py_version| image:: https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20-blue
39
+ :target: https://pypi.org/project/cli-command-parser/
40
+
41
+ .. |coverage_badge| image:: https://codecov.io/gh/dskrypa/cli_command_parser/branch/main/graph/badge.svg
42
+ :target: https://codecov.io/gh/dskrypa/cli_command_parser
43
+
44
+ .. |build_status| image:: https://github.com/dskrypa/cli_command_parser/actions/workflows/run-tests.yml/badge.svg
45
+ :target: https://github.com/dskrypa/cli_command_parser/actions/workflows/run-tests.yml
46
+
47
+ .. |Ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
48
+ :target: https://docs.astral.sh/ruff/
49
+
50
+ .. |downloads| image:: https://img.shields.io/pypi/dm/cli-command-parser
51
+ :target: https://pypistats.org/packages/cli-command-parser
52
+
53
+ .. |OpenSSF Best Practices| image:: https://www.bestpractices.dev/projects/9845/badge
54
+ :target: https://www.bestpractices.dev/projects/9845
55
+
56
+
57
+ CLI Command Parser is a class-based CLI argument parser that defines parameters with descriptors. It provides the
58
+ tools to quickly and easily get started with basic CLIs, and it scales well to support even very large and complex
59
+ CLIs while remaining readable and easy to maintain.
60
+
61
+ The primary goals of this project:
62
+ - Make it easy to define subcommands and actions in an clean and organized manner
63
+ - Allow for inheritance so that common parameters don't need to be repeated
64
+ - Make it easy to handle common initialization tasks for all actions / subcommands once
65
+ - Reduce the amount of boilerplate code that is necessary for setting up parsing and handling argument values
66
+
67
+
68
+ Example Program
69
+ ***************
70
+
71
+ .. code-block:: python
72
+
73
+ from cli_command_parser import Command, Option, main
74
+
75
+ class Hello(Command, description='Simple greeting example'):
76
+ name = Option('-n', default='World', help='The person to say hello to')
77
+ count: int = Option('-c', default=1, help='Number of times to repeat the message')
78
+
79
+ def main(self):
80
+ for _ in range(self.count):
81
+ print(f'Hello {self.name}!')
82
+
83
+ if __name__ == '__main__':
84
+ main()
85
+
86
+
87
+ .. code-block:: shell-session
88
+
89
+ $ hello_world.py --name Bob -c 3
90
+ Hello Bob!
91
+ Hello Bob!
92
+ Hello Bob!
93
+
94
+ $ hello_world.py -h
95
+ usage: hello_world.py [--name NAME] [--count COUNT] [--help]
96
+
97
+ Simple greeting example
98
+
99
+ Optional arguments:
100
+ --name NAME, -n NAME The person to say hello to (default: 'World')
101
+ --count COUNT, -c COUNT Number of times to repeat the message (default: 1)
102
+ --help, -h Show this help message and exit
103
+
104
+
105
+ Installing CLI Command Parser
106
+ *****************************
107
+
108
+ CLI Command Parser can be installed and updated via `pip <https://pip.pypa.io/en/stable/getting-started/>`__::
109
+
110
+ $ pip install -U cli-command-parser
111
+
112
+
113
+ There are no required dependencies. Support for formatting wide characters correctly in help text descriptions can
114
+ be included by adding `wcwidth <https://wcwidth.readthedocs.io>`__ to your project's requirements, and/or by installing
115
+ with optional dependencies::
116
+
117
+ $ pip install -U cli-command-parser[wcwidth]
118
+
119
+
120
+ Python Version Compatibility
121
+ ============================
122
+
123
+ Python versions 3.9 and above are currently supported. The last release of CLI Command Parser that supported 3.8 was
124
+ 2024-09-07. Support for Python 3.8 `officially ended on 2024-10-07 <https://devguide.python.org/versions/>`__.
125
+
126
+
127
+ Links
128
+ *****
129
+
130
+ - Documentation: https://dskrypa.github.io/cli_command_parser/
131
+ - Example Scripts: https://github.com/dskrypa/cli_command_parser/tree/main/examples
132
+ - PyPI Releases: https://pypi.org/project/cli-command-parser/
133
+ - Source Code: https://github.com/dskrypa/cli_command_parser
134
+ - Issue Tracker: https://github.com/dskrypa/cli_command_parser/issues
@@ -4,52 +4,52 @@ Command Parser
4
4
  :author: Doug Skrypa
5
5
  """
6
6
 
7
+ from .commands import AsyncCommand, Command, main
7
8
  from .config import (
9
+ AllowLeadingDash,
10
+ AmbiguousComboMode,
8
11
  CommandConfig,
9
- ShowDefaults,
10
12
  OptionNameMode,
13
+ ShowDefaults,
11
14
  SubcommandAliasHelpMode,
12
- AmbiguousComboMode,
13
- AllowLeadingDash,
14
15
  )
15
- from .commands import Command, AsyncCommand, main
16
- from .context import Context, get_current_context, ctx, get_parsed, get_context, get_raw_arg
16
+ from .context import Context, ctx, get_context, get_current_context, get_parsed, get_raw_arg
17
+ from .error_handling import ErrorHandler, error_handler, extended_error_handler, no_exit_handler
17
18
  from .exceptions import (
18
- CommandParserException,
19
- CommandDefinitionError,
20
- ParameterDefinitionError,
21
- UsageError,
22
- ParamUsageError,
19
+ AmbiguousParseTree,
23
20
  BadArgument,
21
+ CommandDefinitionError,
22
+ CommandParserException,
24
23
  InvalidChoice,
25
24
  MissingArgument,
26
- TooManyArguments,
25
+ NoActiveContext,
27
26
  NoSuchOption,
28
- ParserExit,
29
27
  ParamConflict,
28
+ ParameterDefinitionError,
30
29
  ParamsMissing,
31
- NoActiveContext,
32
- AmbiguousParseTree,
30
+ ParamUsageError,
31
+ ParserExit,
32
+ TooManyArguments,
33
+ UsageError,
33
34
  )
34
- from .error_handling import ErrorHandler, error_handler, no_exit_handler, extended_error_handler
35
35
  from .formatting.commands import get_formatter
36
36
  from .nargs import REMAINDER
37
37
  from .parameters import (
38
+ Action,
39
+ ActionFlag,
40
+ BaseOption,
41
+ BasePositional,
42
+ Counter,
43
+ Flag,
44
+ Option,
38
45
  Parameter,
46
+ ParamGroup,
39
47
  PassThru,
40
- BasePositional,
41
48
  Positional,
42
49
  SubCommand,
43
- Action,
44
- BaseOption,
45
- Option,
46
- Flag,
47
- Counter,
48
- ActionFlag,
50
+ TriFlag,
49
51
  action_flag,
50
- before_main,
51
52
  after_main,
52
- ParamGroup,
53
- TriFlag,
53
+ before_main,
54
54
  )
55
55
  from .typing import Param, ParamOrGroup
@@ -1,8 +1,8 @@
1
1
  __title__ = 'cli_command_parser'
2
2
  __description__ = 'CLI Command Parser'
3
3
  __url__ = 'https://github.com/dskrypa/cli_command_parser'
4
- __version__ = '2024.12.15'
4
+ __version__ = '2025.06.14'
5
5
  __author__ = 'Doug Skrypa'
6
6
  __author_email__ = 'dskrypa@gmail.com'
7
7
  __license__ = 'Apache 2.0'
8
- __copyright__ = 'Copyright 2023 Doug Skrypa'
8
+ __copyright__ = 'Copyright 2024 Doug Skrypa'
@@ -0,0 +1,12 @@
1
+ from .argparse_ast import (
2
+ AddVisitedChild,
3
+ ArgCollection,
4
+ ArgGroup,
5
+ AstArgumentParser,
6
+ AstCallable,
7
+ MutuallyExclusiveGroup,
8
+ Script,
9
+ SubParser,
10
+ visit_func,
11
+ )
12
+ from .command_builder import Converter, convert_script
@@ -4,7 +4,7 @@ import logging
4
4
  from functools import cached_property
5
5
  from pathlib import Path
6
6
 
7
- from cli_command_parser import Command, Counter, Positional, Flag, ParamGroup, SubCommand, main
7
+ from cli_command_parser import Command, Counter, Flag, ParamGroup, Positional, SubCommand, main
8
8
  from cli_command_parser.inputs import Path as IPath
9
9
 
10
10
  log = logging.getLogger(__name__)
@@ -132,15 +132,17 @@ def top_level_commands(commands: Commands) -> Commands:
132
132
  def import_module(path: PathLike):
133
133
  """Import the module / package from the given path"""
134
134
  path = Path(path)
135
- name = path.stem
135
+ name = _module_name(path)
136
136
  if path.is_dir():
137
137
  path /= '__init__.py'
138
+
138
139
  spec = spec_from_file_location(name, path)
139
140
  try:
140
141
  module = module_from_spec(spec)
141
142
  except AttributeError as e:
142
143
  path_str = path.as_posix()
143
144
  raise ImportError(f'Invalid path={path_str!r} - are you sure it is a Python module?', path=path_str) from e
145
+
144
146
  sys.modules[spec.name] = module # This is required for the program metadata introspection
145
147
  try:
146
148
  spec.loader.exec_module(module)
@@ -150,6 +152,20 @@ def import_module(path: PathLike):
150
152
  return module
151
153
 
152
154
 
155
+ def _module_name(path: Path) -> str:
156
+ if path.name == '__init__.py':
157
+ path = path.parent
158
+
159
+ parts = [path.stem]
160
+ while (path := path.parent).name: # / has no name
161
+ if path.joinpath('__init__.py').exists(): # it is a package
162
+ parts.append(path.name)
163
+ else:
164
+ break
165
+
166
+ return '.'.join(parts[::-1])
167
+
168
+
153
169
  def _is_command(obj, include_abc: Bool = False) -> bool:
154
170
  if not (isinstance(obj, CommandMeta) and obj is not Command):
155
171
  return False
@@ -0,0 +1,38 @@
1
+ """
2
+ Error handling for expected / unexpected exceptions.
3
+
4
+ The default handler will...
5
+
6
+ - Call ``print()`` after catching a :class:`python:KeyboardInterrupt`, before exiting
7
+ - Exit gracefully after catching a :class:`python:BrokenPipeError` (often caused by piping output to a tool like
8
+ ``tail``)
9
+
10
+ .. note::
11
+ Parameters defined in a base Command will be processed in the context of that Command. I.e., if a valid
12
+ subcommand argument was provided, but an Option defined in the parent Command has an invalid value, then the
13
+ exception that is raised about that invalid value will be raised before transferring control to the
14
+ subcommand's error handler.
15
+
16
+ :author: Doug Skrypa
17
+ """
18
+
19
+ from platform import system as _system
20
+
21
+ from .base import (
22
+ ErrorHandler,
23
+ Handler,
24
+ HandlerFunc,
25
+ NullErrorHandler,
26
+ error_handler,
27
+ extended_error_handler,
28
+ no_exit_handler,
29
+ )
30
+
31
+ if _system().lower() == 'windows':
32
+ from .windows import handle_kb_interrupt
33
+ else:
34
+ from .other import handle_kb_interrupt
35
+
36
+ __all__ = ['ErrorHandler', 'Handler', 'error_handler', 'extended_error_handler', 'no_exit_handler', 'NullErrorHandler']
37
+
38
+ error_handler.register(handle_kb_interrupt, KeyboardInterrupt)
@@ -1,34 +1,19 @@
1
1
  """
2
- Error handling for expected / unexpected exceptions.
3
-
4
- The default handler will...
5
-
6
- - Call ``print()`` after catching a :class:`python:KeyboardInterrupt`, before exiting
7
- - Exit gracefully after catching a :class:`python:BrokenPipeError` (often caused by piping output to a tool like
8
- ``tail``)
9
-
10
- .. note::
11
- Parameters defined in a base Command will be processed in the context of that Command. I.e., if a valid
12
- subcommand argument was provided, but an Option defined in the parent Command has an invalid value, then the
13
- exception that is raised about that invalid value will be raised before transferring control to the
14
- subcommand's error handler.
15
-
16
- :author: Doug Skrypa
2
+ Platform-agnostic error handling framework / handlers.
17
3
  """
18
4
 
19
5
  from __future__ import annotations
20
6
 
21
- import platform
22
7
  import sys
23
8
  from collections import ChainMap
24
- from typing import Callable, Iterator, Optional, Type, Union
9
+ from typing import Callable, Iterator, Type, TypeVar, Union
25
10
 
26
- from .exceptions import CommandParserException
11
+ from ..exceptions import CommandParserException
27
12
 
28
13
  __all__ = ['ErrorHandler', 'error_handler', 'extended_error_handler', 'no_exit_handler', 'NullErrorHandler']
29
14
 
30
- WINDOWS = platform.system().lower() == 'windows'
31
- HandlerFunc = Callable[[BaseException], Optional[bool]]
15
+ E = TypeVar('E', bound=BaseException)
16
+ HandlerFunc = Callable[[E], Union[bool, int, None]]
32
17
 
33
18
 
34
19
  class ErrorHandler:
@@ -42,7 +27,7 @@ class ErrorHandler:
42
27
  def __repr__(self) -> str:
43
28
  return f'<{self.__class__.__name__}[handlers={len(self.exc_handler_map)}]>'
44
29
 
45
- def register(self, handler: HandlerFunc, *exceptions: Type[BaseException]):
30
+ def register(self, handler: HandlerFunc, *exceptions: Type[E]):
46
31
  for exc in exceptions:
47
32
  self.exc_handler_map[exc] = Handler(exc, handler)
48
33
 
@@ -61,7 +46,7 @@ class ErrorHandler:
61
46
  return _handler
62
47
 
63
48
  @classmethod
64
- def cls_handler(cls, *exceptions: Type[BaseException]):
49
+ def cls_handler(cls, *exceptions: Type[E]):
65
50
  def _cls_handler(handler: Union[HandlerFunc, staticmethod]):
66
51
  for exc in exceptions:
67
52
  cls._exc_handler_map[exc] = Handler(exc, handler)
@@ -91,6 +76,7 @@ class ErrorHandler:
91
76
  for handler in self.iter_handlers(exc_type, exc_val):
92
77
  result = handler(exc_val)
93
78
  if result is True:
79
+ # This explicitly checks for True since 1 == True, but 1 is treated as an intended exit code
94
80
  return True
95
81
  if result or (isinstance(result, int) and result is not False):
96
82
  sys.exit(result)
@@ -130,62 +116,23 @@ class Handler:
130
116
  return issubclass(self.exc_cls, other.exc_cls)
131
117
 
132
118
 
133
- ErrorHandler.cls_handler(CommandParserException)(CommandParserException.exit)
119
+ # By default, all error handlers should call :meth:`CommandParserException.exit` for CommandParserExceptions
120
+ ErrorHandler.cls_handler(CommandParserException)(CommandParserException.exit) # noqa
134
121
 
135
122
  #: Default base :class:`ErrorHandler`
136
123
  error_handler: ErrorHandler = ErrorHandler()
137
- error_handler.register(lambda e: True, BrokenPipeError)
138
124
 
139
125
 
140
- @error_handler(KeyboardInterrupt)
141
- def handle_kb_interrupt(exc: KeyboardInterrupt) -> int:
142
- """
143
- Handles :class:`python:KeyboardInterrupt` by calling :func:`python:print` to avoid ending the program in a way that
144
- causes the next terminal prompt to be printed on the same line as the last (possibly incomplete) line of output.
145
- """
146
- try:
147
- print(flush=True) # Flush forces any potential closed/broken pipe-related error to be caught/handled here
148
- except BrokenPipeError:
149
- pass
150
- except OSError as e:
151
- # Handle the closed/broken pipe incorrect errno bug if triggered during the above print
152
- if not WINDOWS or not handle_win_os_pipe_error(e):
153
- raise
154
- return 130
126
+ @error_handler(BrokenPipeError)
127
+ def _handle_broken_pipe(exc: BrokenPipeError):
128
+ # This can't be registered as a lambda function because it would break the ability to pickle the handler
129
+ return True
155
130
 
156
131
 
157
132
  #: An :class:`ErrorHandler` that does not call :func:`python:sys.exit` for
158
133
  #: :class:`CommandParserExceptions<.CommandParserException>`
159
134
  no_exit_handler: ErrorHandler = error_handler.copy()
160
- no_exit_handler(CommandParserException)(CommandParserException.show)
135
+ no_exit_handler.register(CommandParserException.show, CommandParserException) # noqa
161
136
 
162
137
  #: The default :class:`ErrorHandler` (extends :obj:`error_handler`)
163
138
  extended_error_handler: ErrorHandler = error_handler.copy()
164
-
165
- if WINDOWS:
166
- import ctypes
167
-
168
- RtlGetLastNtStatus = ctypes.WinDLL('ntdll').RtlGetLastNtStatus
169
- RtlGetLastNtStatus.restype = ctypes.c_ulong
170
- NT_STATUSES = {0xC000_00B1: 'STATUS_PIPE_CLOSING', 0xC000_014B: 'STATUS_PIPE_BROKEN'}
171
-
172
- @extended_error_handler(OSError)
173
- def handle_win_os_pipe_error(exc: OSError):
174
- """
175
- This is a workaround for `[Windows] I/O on a broken pipe may raise an EINVAL OSError instead of BrokenPipeError
176
- <https://github.com/python/cpython/issues/79935>`_, which is a bug in the way that the
177
- windows error code for a broken pipe is translated into an errno value. It should be translated to
178
- :data:`~errno.EPIPE`, but it uses :data:`~errno.EINVAL` (22) instead.
179
-
180
- Prevents the following when piping output to utilities such as ``| head``::\n
181
- Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>
182
- OSError: [Errno 22] Invalid argument
183
- """
184
- if exc.errno == 22 and RtlGetLastNtStatus() in NT_STATUSES:
185
- try:
186
- sys.stdout.close()
187
- except OSError:
188
- pass
189
- return True
190
-
191
- return False
@@ -0,0 +1,20 @@
1
+ """
2
+ Error handling for expected / unexpected exceptions on non-Windows systems.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ __all__ = ['handle_kb_interrupt']
8
+
9
+
10
+ def handle_kb_interrupt(exc: KeyboardInterrupt) -> int:
11
+ """
12
+ Handles :class:`python:KeyboardInterrupt` by calling :func:`python:print` to avoid ending the program in a way that
13
+ causes the next terminal prompt to be printed on the same line as the last (possibly incomplete) line of output.
14
+ """
15
+ try:
16
+ print(flush=True) # Flush forces any potential closed/broken pipe-related error to be caught/handled here
17
+ except BrokenPipeError:
18
+ pass
19
+ # 130 (= 128 + SIGINT (2)) is used/expected by Bash; see: https://tldp.org/LDP/abs/html/exitcodes.html
20
+ return 130