cli-command-parser 2024.11.2__tar.gz → 2025.5.10__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 (65) hide show
  1. {cli_command_parser-2024.11.2/lib/cli_command_parser.egg-info → cli_command_parser-2025.5.10}/PKG-INFO +134 -130
  2. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/__version__.py +2 -2
  3. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/exceptions.py +8 -8
  4. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/inputs/base.py +3 -3
  5. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/inputs/choices.py +6 -0
  6. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/inputs/numeric.py +1 -1
  7. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/parameters/actions.py +23 -21
  8. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/parameters/base.py +16 -8
  9. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/parameters/options.py +9 -6
  10. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/parser.py +4 -3
  11. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/testing.py +3 -4
  12. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10/lib/cli_command_parser.egg-info}/PKG-INFO +134 -130
  13. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/readme.rst +4 -1
  14. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/setup.cfg +50 -50
  15. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/LICENSE +0 -0
  16. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/MANIFEST.in +0 -0
  17. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/entry_points.txt +0 -0
  18. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/__init__.py +0 -0
  19. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/__main__.py +0 -0
  20. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/annotations.py +0 -0
  21. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/command_parameters.py +0 -0
  22. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/commands.py +0 -0
  23. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/compat.py +0 -0
  24. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/config.py +0 -0
  25. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/context.py +0 -0
  26. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/conversion/__init__.py +0 -0
  27. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/conversion/__main__.py +0 -0
  28. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/conversion/argparse_ast.py +0 -0
  29. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/conversion/argparse_utils.py +0 -0
  30. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/conversion/cli.py +0 -0
  31. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/conversion/command_builder.py +0 -0
  32. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/conversion/utils.py +0 -0
  33. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/conversion/visitor.py +0 -0
  34. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/core.py +0 -0
  35. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/documentation.py +0 -0
  36. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/error_handling.py +0 -0
  37. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/formatting/__init__.py +0 -0
  38. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/formatting/commands.py +0 -0
  39. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/formatting/params.py +0 -0
  40. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/formatting/restructured_text.py +0 -0
  41. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/formatting/utils.py +0 -0
  42. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/inputs/__init__.py +0 -0
  43. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/inputs/exceptions.py +0 -0
  44. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/inputs/files.py +0 -0
  45. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/inputs/patterns.py +0 -0
  46. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/inputs/time.py +0 -0
  47. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/inputs/utils.py +0 -0
  48. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/metadata.py +0 -0
  49. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/nargs.py +0 -0
  50. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/parameters/__init__.py +0 -0
  51. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/parameters/choice_map.py +0 -0
  52. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/parameters/groups.py +0 -0
  53. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/parameters/option_strings.py +0 -0
  54. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/parameters/pass_thru.py +0 -0
  55. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/parameters/positionals.py +0 -0
  56. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/parse_tree.py +0 -0
  57. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/typing.py +0 -0
  58. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser/utils.py +0 -0
  59. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser.egg-info/SOURCES.txt +0 -0
  60. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser.egg-info/dependency_links.txt +0 -0
  61. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser.egg-info/entry_points.txt +0 -0
  62. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser.egg-info/requires.txt +0 -0
  63. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/lib/cli_command_parser.egg-info/top_level.txt +0 -0
  64. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/pyproject.toml +0 -0
  65. {cli_command_parser-2024.11.2 → cli_command_parser-2025.5.10}/requirements-dev.txt +0 -0
@@ -1,130 +1,134 @@
1
- Metadata-Version: 2.1
2
- Name: cli_command_parser
3
- Version: 2024.11.2
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.5.10
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
@@ -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.11.02'
4
+ __version__ = '2025.05.10'
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'
@@ -8,14 +8,14 @@ Exceptions for Command Parser
8
8
  from __future__ import annotations
9
9
 
10
10
  import sys
11
- from typing import TYPE_CHECKING, Any, Optional, Collection, Mapping
11
+ from typing import TYPE_CHECKING, Any, Collection, Mapping, Optional
12
12
 
13
13
  from .utils import _parse_tree_target_repr
14
14
 
15
15
  if TYPE_CHECKING:
16
- from .parameters import Parameter, BaseOption
16
+ from .parameters import BaseOption, Parameter
17
+ from .parse_tree import PosNode, Target, Word
17
18
  from .typing import ParamOrGroup
18
- from .parse_tree import PosNode, Word, Target
19
19
 
20
20
  __all__ = [
21
21
  'CommandParserException',
@@ -211,13 +211,13 @@ class BadArgument(ParamUsageError):
211
211
  class InvalidChoice(BadArgument):
212
212
  """Error raised when a value that does not match one of the pre-defined choices was provided for a Parameter"""
213
213
 
214
- def __init__(self, param: Optional[Parameter], invalid: Any, choices: Collection[Any]):
214
+ def __init__(self, param: Optional[Parameter], invalid: Any, choices: Collection[Any], env_var: str = None):
215
+ src = f' from env var={env_var!r}' if env_var else ''
215
216
  if isinstance(invalid, Collection) and not isinstance(invalid, str):
216
- bad_str = f'choices: {", ".join(map(repr, invalid))}'
217
+ bad_str = f'choices{src}: {", ".join(map(repr, invalid))}'
217
218
  else:
218
- bad_str = f'choice: {invalid!r}'
219
- choices_str = ', '.join(map(repr, choices))
220
- super().__init__(param, f'invalid {bad_str} (choose from: {choices_str})')
219
+ bad_str = f'choice{src}: {invalid!r}'
220
+ super().__init__(param, f'invalid {bad_str} (choose from: {", ".join(map(repr, choices))})')
221
221
 
222
222
 
223
223
  class MissingArgument(BadArgument):
@@ -7,7 +7,7 @@ Custom input handlers for Parameters
7
7
  from abc import ABC, abstractmethod
8
8
  from typing import Any, Generic, Optional
9
9
 
10
- from ..typing import T, Bool
10
+ from ..typing import Bool, T
11
11
 
12
12
  __all__ = ['InputType']
13
13
 
@@ -25,11 +25,11 @@ class InputType(Generic[T], ABC):
25
25
 
26
26
  def is_valid_type(self, value: str) -> bool: # pylint: disable=W0613
27
27
  """
28
- Called during parsing when :meth:`.Parameter.would_accept` is called to determine if the value would be
28
+ Called during parsing when :meth:`.ParamAction.would_accept` is called to determine if the value would be
29
29
  accepted later for processing / conversion via :meth:`.__call__`. May be overridden in subclasses to
30
30
  provide actual validation, if necessary.
31
31
 
32
- Not called by :meth:`.Parameter.take_action` - value validation should happen in :meth:`.__call__`
32
+ Note: value validation should happen in :meth:`.__call__`, not in this method.
33
33
 
34
34
  :param value: A parsed argument
35
35
  :return: True if this input would accept it for processing later (where it may still be rejected), False if
@@ -147,6 +147,12 @@ class ChoiceMap(Choices[T]):
147
147
 
148
148
  return self._case_insensitive_map_choice(value)
149
149
 
150
+ def fix_default(self, value: Any) -> T:
151
+ if value in self.choices.values():
152
+ return value
153
+ else:
154
+ return self(value)
155
+
150
156
 
151
157
  class EnumChoices(_ChoicesBase[EnumT]):
152
158
  """
@@ -26,7 +26,7 @@ class NumericInput(InputType[NT], ABC):
26
26
 
27
27
  def is_valid_type(self, value: str) -> bool:
28
28
  """
29
- Called during parsing when :meth:`.Parameter.would_accept` is called to determine if the value would be
29
+ Called during parsing when :meth:`.ParamAction.would_accept` is called to determine if the value would be
30
30
  accepted later for processing / conversion when called.
31
31
 
32
32
  :param value: The parsed argument to validate
@@ -72,41 +72,41 @@ class ParamAction(ABC):
72
72
  # region Add Parsed Value / Constant Methods
73
73
 
74
74
  @abstractmethod
75
- def add_value(self, value: str, *, opt: str = None, combo: bool = False, joined: bool = False) -> Found:
75
+ def add_value(self, value: str, *, combo: bool = False, joined: bool = False, env_var: str = None) -> Found:
76
76
  """
77
77
  Execute this action for the given Parameter and value.
78
78
 
79
79
  :param value: The value that was provided, if any.
80
- :param opt: The option string that preceded the given value in the case of optional params, or that
81
- represents a flag so a constant value can be stored, if any.
82
80
  :param combo: Only True when a short option was provided, where the option string was combined with
83
81
  either a real value or a sequence of 1-char combinable versions of short option strings.
84
82
  :param joined: True if the value was provided as ``--option=value``, False otherwise.
83
+ :param env_var: The name of the environment variable that was used as the source of the given value, if
84
+ applicable.
85
85
  :return: The number of new values discovered
86
86
  """
87
87
  raise NotImplementedError
88
88
 
89
+ def add_env_value(self, value: str, env_var: str) -> Found:
90
+ return self.add_value(value, env_var=env_var)
91
+
89
92
  # Note: Not used yet
90
- # def add_values(self, values: Sequence[str], *, opt: str = None, combo: bool = False) -> Found:
93
+ # def add_values(self, values: Sequence[str], *, combo: bool = False) -> Found:
91
94
  # added = 0
92
95
  # for value in values:
93
- # added += self.add_value(value, opt=opt, combo=combo)
96
+ # added += self.add_value(value, combo=combo)
94
97
  # return added
95
98
 
96
99
  def add_const(self, *, opt: str = None, combo: bool = False) -> Found: # noqa
97
100
  ctx.record_action(self.param)
98
101
  raise MissingArgument(self.param)
99
102
 
100
- def add_env_value(self, value: str, env_var: str):
101
- return self.add_value(value)
102
-
103
103
  # endregion
104
104
 
105
105
  # region Parsing
106
106
 
107
107
  def would_accept(self, value: str, combo: bool = False) -> bool:
108
108
  try:
109
- normalized = self.param.prepare_value(value, combo, True)
109
+ normalized = self.param.prepare_validation_value(value, combo)
110
110
  except BadArgument:
111
111
  return False
112
112
  return self.param.is_valid_arg(normalized)
@@ -145,6 +145,8 @@ class ParamAction(ABC):
145
145
  return self.default
146
146
 
147
147
  def finalize_default(self, value):
148
+ if self.param.strict_default:
149
+ return value
148
150
  if (type_func := self.param.type) and isinstance(type_func, InputType):
149
151
  return type_func.fix_default(value)
150
152
  return value
@@ -225,7 +227,7 @@ class ConstMixin:
225
227
  #
226
228
  # parsed.extend(consts)
227
229
 
228
- def add_env_value(self, value: str, env_var: str):
230
+ def add_env_value(self, value: str, env_var: str) -> Found:
229
231
  const, use_value = self.param.get_env_const(value, env_var)
230
232
  # The const may only be _NotSet once StoreValueOrConst / AppendValueOrConst are put into use
231
233
  # if const is _NotSet: # It does not support storing constants
@@ -250,15 +252,15 @@ class Store(ValueMixin, ParamAction, default=None, accepts_values=True):
250
252
 
251
253
  # region Add Parsed Value / Constant Methods
252
254
 
253
- def add_value(self, value: str, *, opt: str = None, combo: bool = False, joined: Bool = False) -> Found:
255
+ def add_value(self, value: str, *, combo: bool = False, joined: Bool = False, env_var: str = None) -> Found:
254
256
  ctx.record_action(self.param)
255
- value = self.param.prepare_value(value, combo)
257
+ value = self.param.prepare_value(value, combo, env_var)
256
258
  self.param.validate(value, joined)
257
259
  self.set_value(value)
258
260
  return 1
259
261
 
260
262
  # Note: Not used yet
261
- # def add_values(self, values: Sequence[str], *, opt: str = None, combo: bool = False) -> Found:
263
+ # def add_values(self, values: Sequence[str], *, combo: bool = False) -> Found:
262
264
  # ctx.record_action(self.param)
263
265
  # if not values:
264
266
  # raise MissingArgument(self.param)
@@ -286,15 +288,15 @@ class Append(ValueMixin, ParamAction, accepts_values=True):
286
288
 
287
289
  # region Add Parsed Value / Constant Methods
288
290
 
289
- def add_value(self, value: str, *, opt: str = None, combo: bool = False, joined: Bool = False) -> Found:
291
+ def add_value(self, value: str, *, combo: bool = False, joined: Bool = False, env_var: str = None) -> Found:
290
292
  ctx.record_action(self.param)
291
- value = self.param.prepare_value(value, combo)
293
+ value = self.param.prepare_value(value, combo, env_var)
292
294
  self.param.validate(value)
293
295
  self.append_value(value)
294
296
  return 1
295
297
 
296
298
  # Note: Not used yet
297
- # def add_values(self, values: Sequence[str], *, opt: str = None, combo: bool = False) -> Found:
299
+ # def add_values(self, values: Sequence[str], *, combo: bool = False) -> Found:
298
300
  # ctx.record_action(self.param)
299
301
  # if not values:
300
302
  # raise MissingArgument(self.param)
@@ -385,7 +387,7 @@ class BasicConstAction(ConstMixin, ParamAction, ABC, accepts_consts=True):
385
387
 
386
388
  # region Add Parsed Value / Constant Methods
387
389
 
388
- def add_value(self, value: str, *, opt: str = None, combo: bool = False, joined: Bool = False) -> Found: # noqa
390
+ def add_value(self, value: str, *, combo: bool = False, joined: Bool = False, env_var: str = None) -> Found: # noqa
389
391
  ctx.record_action(self.param)
390
392
  raise BadArgument(self.param, f'does not accept values, but {value=} was provided')
391
393
 
@@ -471,9 +473,9 @@ class Count(ParamAction, accepts_values=True, accepts_consts=True):
471
473
  self._add(self.param.get_const(opt))
472
474
  return 1
473
475
 
474
- def add_value(self, value: str, *, opt: str = None, combo: bool = False, joined: Bool = False) -> Found:
476
+ def add_value(self, value: str, *, combo: bool = False, joined: Bool = False, env_var: str = None) -> Found:
475
477
  ctx.record_action(self.param)
476
- value = self.param.prepare_value(value, combo)
478
+ value = self.param.prepare_value(value, combo, env_var)
477
479
  self.param.validate(value, joined)
478
480
  self._add(value)
479
481
  return 1
@@ -486,7 +488,7 @@ class Concatenate(Append):
486
488
 
487
489
  # region Add Parsed Value / Constant Methods
488
490
 
489
- def add_value(self, value: str, *, opt: str = None, combo: bool = False, joined: Bool = False) -> Found:
491
+ def add_value(self, value: str, *, combo: bool = False, joined: Bool = False, env_var: str = None) -> Found:
490
492
  param = self.param
491
493
  values = value.split()
492
494
  if not param.is_valid_arg(' '.join(values)):
@@ -525,7 +527,7 @@ class StoreAll(Store):
525
527
 
526
528
  # region Add Parsed Value / Constant Methods
527
529
 
528
- def add_values(self, values: list[str], *, opt: str = None, combo: bool = False) -> Found:
530
+ def add_values(self, values: list[str], *, combo: bool = False) -> Found:
529
531
  param = self.param
530
532
  ctx.record_action(param)
531
533
 
@@ -325,20 +325,28 @@ class Parameter(ParamBase, Generic[T_co], ABC):
325
325
  def get_env_const(self, value: str, env_var: str) -> tuple[T_co, bool]:
326
326
  return _NotSet, False
327
327
 
328
- def prepare_value(self, value: str, short_combo: Bool = False, pre_action: Bool = False) -> T_co:
329
- type_func = self.type
330
- if type_func is None or (pre_action and isinstance(type_func, InputType) and type_func.is_valid_type(value)):
328
+ def prepare_value(self, value: str, short_combo: Bool = False, env_var: str = None) -> T_co:
329
+ if self.type is None:
331
330
  return value
332
331
  try:
333
- return type_func(value)
332
+ return self.type(value)
334
333
  except InvalidChoiceError as e:
335
- raise InvalidChoice(self, e.invalid, e.choices) from e
334
+ raise InvalidChoice(self, e.invalid, e.choices, env_var) from e
336
335
  except InputValidationError as e:
337
- raise BadArgument(self, str(e)) from e
336
+ suffix = f' from env var={env_var!r}' if env_var else ''
337
+ raise BadArgument(self, f'invalid input{suffix} - {e}') from e
338
338
  except (TypeError, ValueError) as e:
339
- raise BadArgument(self, f'bad {value=} for type={type_func!r}: {e}') from e
339
+ suffix = f' from env var={env_var!r}' if env_var else ''
340
+ raise BadArgument(self, f'bad {value=} for type={self.type!r}{suffix}: {e}') from e
340
341
  except Exception as e:
341
- raise BadArgument(self, f'unable to cast {value=} to type={type_func!r}') from e
342
+ suffix = f' from env var={env_var!r}' if env_var else ''
343
+ raise BadArgument(self, f'unable to cast {value=} to type={self.type!r}{suffix}') from e
344
+
345
+ def prepare_validation_value(self, value: str, short_combo: Bool = False) -> T_co:
346
+ if self.type is None or (isinstance(self.type, InputType) and self.type.is_valid_type(value)):
347
+ return value
348
+ else:
349
+ return self.prepare_value(value, short_combo)
342
350
 
343
351
  def validate(self, value: Union[T_co, None], joined: Bool = False):
344
352
  if not isinstance(value, str) or not value or not value[0] == '-':
@@ -192,9 +192,9 @@ class Flag(BaseOption[Union[TD, TC]], actions=(StoreConst, AppendConst)):
192
192
  try:
193
193
  parsed = self.type(value)
194
194
  except Exception as e:
195
- raise ParamUsageError(self, f'unable to parse {value=} from {env_var=}: {e}') from e
195
+ raise ParamUsageError(self, f'unable to parse {value=} from env var={env_var!r}: {e}') from e
196
196
  if self.use_env_value and parsed != self.const and parsed != self.default:
197
- raise BadArgument(self, f'invalid value={parsed!r} from {env_var=}')
197
+ raise BadArgument(self, f'invalid value={parsed!r} from env var={env_var!r}')
198
198
  return parsed, self.use_env_value
199
199
 
200
200
 
@@ -308,10 +308,10 @@ class TriFlag(BaseOption[Union[TD, TC, TA]], ABC, actions=(StoreConst, AppendCon
308
308
  try:
309
309
  parsed = self.type(value)
310
310
  except Exception as e:
311
- raise ParamUsageError(self, f'unable to parse {value=} from {env_var=}: {e}') from e
311
+ raise ParamUsageError(self, f'unable to parse {value=} from env var={env_var!r}: {e}') from e
312
312
  if self.use_env_value:
313
313
  if parsed not in self.consts and parsed != self.default:
314
- raise BadArgument(self, f'invalid value={parsed!r} from {env_var=}')
314
+ raise BadArgument(self, f'invalid value={parsed!r} from env var={env_var!r}')
315
315
  return parsed, True
316
316
  else:
317
317
  const = self.consts[0] if parsed else self.consts[1]
@@ -491,14 +491,17 @@ class Counter(BaseOption[int], actions=(Count,)):
491
491
  self.default_cb = None
492
492
  return super().register_default_cb(method)
493
493
 
494
- def prepare_value(self, value: Optional[str], short_combo: bool = False, pre_action: bool = False) -> int:
494
+ def prepare_value(self, value: Optional[str], short_combo: bool = False, env_var: str = None) -> int:
495
495
  try:
496
496
  return self.type(value)
497
497
  except (ValueError, TypeError) as e:
498
498
  combinable = self.option_strs.combinable
499
499
  if short_combo and combinable and all(c in combinable for c in value):
500
500
  return len(value) + 1 # +1 for the -short that preceded this value
501
- raise BadArgument(self, f'bad counter {value=}') from e
501
+ suffix = f' from env var={env_var!r}' if env_var else ''
502
+ raise BadArgument(self, f'bad counter {value=}{suffix}') from e
503
+
504
+ prepare_validation_value = prepare_value
502
505
 
503
506
  def validate(self, value: Any, joined: Bool = False):
504
507
  if value is None or isinstance(value, self.type):
@@ -36,6 +36,9 @@ log = logging.getLogger(__name__)
36
36
 
37
37
  _PRE_INIT = ActionPhase.PRE_INIT
38
38
 
39
+ # TODO: When an invalid choice for a positional is provided with -h / --help, the invalid choice error is shown instead
40
+ # of help, but help should be shown instead
41
+
39
42
 
40
43
  class CommandParser:
41
44
  """Stateful parser used for a single pass of argument parsing"""
@@ -111,8 +114,6 @@ class CommandParser:
111
114
  self._parse_env_vars(ctx)
112
115
 
113
116
  def _parse_env_vars(self, ctx: Context):
114
- # TODO: It would be helpful to store arg provenance for error messages, especially for a conflict between
115
- # mutually exclusive params when they were provided via env
116
117
  for param in ctx.missing_options_with_env_var():
117
118
  for env_var in param.env_vars():
118
119
  try:
@@ -220,7 +221,7 @@ class CommandParser:
220
221
  self, opt: str, param: BaseOption, value: OptStr, combo: bool = False, joined: Bool = False
221
222
  ):
222
223
  if value is not None:
223
- param.action.add_value(value, opt=opt, combo=combo, joined=joined)
224
+ param.action.add_value(value, combo=combo, joined=joined)
224
225
  elif param.action.accepts_consts and not param.action.accepts_values:
225
226
  param.action.add_const(opt=opt, combo=combo)
226
227
  elif not self.consume_values(param) and param.action.accepts_consts:
@@ -94,9 +94,6 @@ class ParserTest(TestCase):
94
94
  def assert_dict_equal(self, d1, d2, msg: str = None):
95
95
  self.assertIsInstance(d1, dict, 'First argument is not a dictionary')
96
96
  self.assertIsInstance(d2, dict, 'Second argument is not a dictionary')
97
- self._assert_dict_equal(d1, d2, msg)
98
-
99
- def _assert_dict_equal(self, d1, d2, msg: str = None):
100
97
  if d1 != d2:
101
98
  self.fail(self._formatMessage(msg, f'{d1} != {d2}\n{format_dict_diff(d1, d2)}'))
102
99
 
@@ -105,7 +102,9 @@ class ParserTest(TestCase):
105
102
 
106
103
  def assert_parse_results(self, cmd_cls: CommandCls, argv: Argv, expected: Expected, msg: str = None) -> Command:
107
104
  cmd = cmd_cls.parse(argv)
108
- self._assert_dict_equal(expected, cmd.ctx.get_parsed(cmd, exclude=EXCLUDE_ACTIONS), msg)
105
+ parsed = cmd.ctx.get_parsed(cmd, exclude=EXCLUDE_ACTIONS)
106
+ if expected != parsed:
107
+ self.fail(msg or f'Expected results ({expected}) != {parsed=}\n{format_dict_diff(expected, parsed)}')
109
108
  return cmd
110
109
 
111
110
  def assert_parse_results_cases(self, cmd_cls: CommandCls, cases: Iterable[Case], msg: str = None):