cmdkit 2.7.7__tar.gz → 2.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cmdkit-2.7.7 → cmdkit-2.8.0}/PKG-INFO +15 -14
- cmdkit-2.8.0/pyproject.toml +77 -0
- cmdkit-2.8.0/pyproject.toml.orig +71 -0
- {cmdkit-2.7.7 → cmdkit-2.8.0}/src/cmdkit/__init__.py +4 -1
- {cmdkit-2.7.7 → cmdkit-2.8.0}/src/cmdkit/app.py +56 -0
- {cmdkit-2.7.7 → cmdkit-2.8.0}/src/cmdkit/config.py +107 -5
- cmdkit-2.7.7/pyproject.toml +0 -66
- {cmdkit-2.7.7 → cmdkit-2.8.0}/LICENSE +0 -0
- {cmdkit-2.7.7 → cmdkit-2.8.0}/README.rst +0 -0
- {cmdkit-2.7.7 → cmdkit-2.8.0}/src/cmdkit/ansi.py +0 -0
- {cmdkit-2.7.7 → cmdkit-2.8.0}/src/cmdkit/cli.py +0 -0
- {cmdkit-2.7.7 → cmdkit-2.8.0}/src/cmdkit/contrib/__init__.py +0 -0
- {cmdkit-2.7.7 → cmdkit-2.8.0}/src/cmdkit/contrib/builder.py +0 -0
- {cmdkit-2.7.7 → cmdkit-2.8.0}/src/cmdkit/logging.py +0 -0
- {cmdkit-2.7.7 → cmdkit-2.8.0}/src/cmdkit/namespace.py +0 -0
- {cmdkit-2.7.7 → cmdkit-2.8.0}/src/cmdkit/platform.py +0 -0
- {cmdkit-2.7.7 → cmdkit-2.8.0}/src/cmdkit/py.typed +0 -0
|
@@ -1,29 +1,31 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: cmdkit
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.0
|
|
4
4
|
Summary: A command-line utility toolkit for Python.
|
|
5
|
-
Home-page: https://cmdkit.readthedocs.io
|
|
6
|
-
License: Apache-2.0
|
|
7
5
|
Keywords: command-line,utility,toolkit
|
|
8
6
|
Author: Geoffrey Lentner
|
|
9
|
-
Author-email: glentner@purdue.edu
|
|
10
|
-
|
|
7
|
+
Author-email: Geoffrey Lentner <glentner@purdue.edu>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
11
10
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
-
Classifier:
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
13
12
|
Classifier: Programming Language :: Python :: 3
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.9
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
-
Classifier:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
Requires-
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
20
|
+
Requires-Dist: tomli>=2.1.0,<3.0.0 ; extra == 'toml'
|
|
21
|
+
Requires-Dist: tomli-w>=1.1.0,<2.0.0 ; extra == 'toml'
|
|
22
|
+
Requires-Dist: pyyaml>=6.0.1,<7.0.0 ; extra == 'yaml'
|
|
23
|
+
Requires-Python: >=3.9, <4.0
|
|
24
|
+
Project-URL: Homepage, https://cmdkit.readthedocs.io
|
|
25
25
|
Project-URL: Documentation, https://cmdkit.readthedocs.io
|
|
26
26
|
Project-URL: Repository, https://github.com/glentner/cmdkit
|
|
27
|
+
Provides-Extra: toml
|
|
28
|
+
Provides-Extra: yaml
|
|
27
29
|
Description-Content-Type: text/x-rst
|
|
28
30
|
|
|
29
31
|
CmdKit
|
|
@@ -92,4 +94,3 @@ Contributions are welcome in the form of suggestions for additional features, pu
|
|
|
92
94
|
new features or bug fixes, etc. If you find bugs or have questions, open an *Issue* here. If and
|
|
93
95
|
when the project grows, a code of conduct will be provided along side a more comprehensive set of
|
|
94
96
|
guidelines for contributing; until then, just be nice.
|
|
95
|
-
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cmdkit"
|
|
3
|
+
version = "2.8.0"
|
|
4
|
+
description = "A command-line utility toolkit for Python."
|
|
5
|
+
readme = "README.rst"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
keywords = [
|
|
9
|
+
"command-line",
|
|
10
|
+
"utility",
|
|
11
|
+
"toolkit",
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 5 - Production/Stable",
|
|
15
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.9",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"License :: OSI Approved :: Apache Software License",
|
|
24
|
+
]
|
|
25
|
+
requires-python = ">=3.9,<4.0"
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[[project.authors]]
|
|
29
|
+
name = "Geoffrey Lentner"
|
|
30
|
+
email = "glentner@purdue.edu"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
toml = [
|
|
34
|
+
"tomli>=2.1.0,<3.0.0",
|
|
35
|
+
"tomli-w>=1.1.0,<2.0.0",
|
|
36
|
+
]
|
|
37
|
+
yaml = ["pyyaml>=6.0.1,<7.0.0"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://cmdkit.readthedocs.io"
|
|
41
|
+
Documentation = "https://cmdkit.readthedocs.io"
|
|
42
|
+
Repository = "https://github.com/glentner/cmdkit"
|
|
43
|
+
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = [
|
|
46
|
+
"tomli>=2.1.0,<3.0.0",
|
|
47
|
+
"tomli-w>=1.1.0,<2.0.0",
|
|
48
|
+
"pyyaml>=6.0.1,<7.0.0",
|
|
49
|
+
"pytest>=7.4.4,<8.0.0",
|
|
50
|
+
"hypothesis>=6.92.6,<7.0.0",
|
|
51
|
+
"sphinx-autobuild>=2021.3.14,<2022.0.0",
|
|
52
|
+
]
|
|
53
|
+
docs = [
|
|
54
|
+
"sphinx>=7.2.6,<8.0.0",
|
|
55
|
+
"sphinx-sitemap>=2.5.1,<3.0.0",
|
|
56
|
+
"sphinx-autobuild>=2021.3.14,<2022.0.0",
|
|
57
|
+
"sphinx-copybutton>=0.5.2,<0.6.0",
|
|
58
|
+
"sphinxext-opengraph>=0.9.1,<0.10.0",
|
|
59
|
+
"sphinxcontrib-details-directive>=0.1.0,<0.2.0",
|
|
60
|
+
"sphinx-toolbox>=3.5.0,<4.0.0",
|
|
61
|
+
"pydata-sphinx-theme>=0.15.1,<0.16.0",
|
|
62
|
+
"enum-tools[sphinx]>=0.10.0,<0.11.0",
|
|
63
|
+
"docutils>=0.21.1,<0.22.0",
|
|
64
|
+
"setuptools<82",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[build-system]
|
|
68
|
+
requires = ["uv_build>=0.13.7,<0.14.0"]
|
|
69
|
+
build-backend = "uv_build"
|
|
70
|
+
|
|
71
|
+
[tool.pytest.ini_options]
|
|
72
|
+
addopts = ["--strict-markers"]
|
|
73
|
+
markers = [
|
|
74
|
+
"unit: Unit tests are short, interface driven tests on discrete components.",
|
|
75
|
+
"integration: Integration tests are often longer and deal with the interaction between systems.",
|
|
76
|
+
"parameterize: Place holder for parameterized tests (not a real type).",
|
|
77
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cmdkit"
|
|
3
|
+
version = "2.8.0"
|
|
4
|
+
description = "A command-line utility toolkit for Python."
|
|
5
|
+
readme = "README.rst"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Geoffrey Lentner", email = "glentner@purdue.edu" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["command-line", "utility", "toolkit"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 5 - Production/Stable",
|
|
14
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.9",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
"License :: OSI Approved :: Apache Software License",
|
|
23
|
+
]
|
|
24
|
+
requires-python = ">=3.9,<4.0"
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
toml = ["tomli>=2.1.0,<3.0.0", "tomli-w>=1.1.0,<2.0.0"]
|
|
29
|
+
yaml = ["pyyaml>=6.0.1,<7.0.0"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://cmdkit.readthedocs.io"
|
|
33
|
+
Documentation = "https://cmdkit.readthedocs.io"
|
|
34
|
+
Repository = "https://github.com/glentner/cmdkit"
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"tomli>=2.1.0,<3.0.0",
|
|
39
|
+
"tomli-w>=1.1.0,<2.0.0",
|
|
40
|
+
"pyyaml>=6.0.1,<7.0.0",
|
|
41
|
+
"pytest>=7.4.4,<8.0.0",
|
|
42
|
+
"hypothesis>=6.92.6,<7.0.0",
|
|
43
|
+
"sphinx-autobuild>=2021.3.14,<2022.0.0",
|
|
44
|
+
]
|
|
45
|
+
docs = [
|
|
46
|
+
"sphinx>=7.2.6,<8.0.0",
|
|
47
|
+
"sphinx-sitemap>=2.5.1,<3.0.0",
|
|
48
|
+
"sphinx-autobuild>=2021.3.14,<2022.0.0",
|
|
49
|
+
"sphinx-copybutton>=0.5.2,<0.6.0",
|
|
50
|
+
"sphinxext-opengraph>=0.9.1,<0.10.0",
|
|
51
|
+
"sphinxcontrib-details-directive>=0.1.0,<0.2.0",
|
|
52
|
+
"sphinx-toolbox>=3.5.0,<4.0.0",
|
|
53
|
+
"pydata-sphinx-theme>=0.15.1,<0.16.0",
|
|
54
|
+
"enum-tools[sphinx]>=0.10.0,<0.11.0",
|
|
55
|
+
"docutils>=0.21.1,<0.22.0",
|
|
56
|
+
# sphinxcontrib-details-directive imports pkg_resources, which setuptools 82 removed;
|
|
57
|
+
# a uv environment holds only what is declared, so setuptools is named here
|
|
58
|
+
"setuptools<82",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[build-system]
|
|
62
|
+
requires = ["uv_build>=0.13.7,<0.14.0"]
|
|
63
|
+
build-backend = "uv_build"
|
|
64
|
+
|
|
65
|
+
[tool.pytest.ini_options]
|
|
66
|
+
addopts = ["--strict-markers", ]
|
|
67
|
+
markers = [
|
|
68
|
+
"unit: Unit tests are short, interface driven tests on discrete components.",
|
|
69
|
+
"integration: Integration tests are often longer and deal with the interaction between systems.",
|
|
70
|
+
"parameterize: Place holder for parameterized tests (not a real type).",
|
|
71
|
+
]
|
|
@@ -30,4 +30,7 @@ __all__ = [
|
|
|
30
30
|
__version__ = get_version('cmdkit')
|
|
31
31
|
|
|
32
32
|
# null-handler for library interface
|
|
33
|
-
|
|
33
|
+
_null_handler = NullHandler()
|
|
34
|
+
_null_handler._cmdkit_library_handler = True
|
|
35
|
+
Logger.with_name(__name__).addHandler(_null_handler)
|
|
36
|
+
del _null_handler
|
|
@@ -10,6 +10,7 @@ from typing import List, Dict, Callable, NamedTuple, Type, TypeVar
|
|
|
10
10
|
|
|
11
11
|
# standard libs
|
|
12
12
|
import abc
|
|
13
|
+
import sys
|
|
13
14
|
import logging
|
|
14
15
|
|
|
15
16
|
# internal libs
|
|
@@ -27,6 +28,41 @@ TAppGrp = TypeVar('TAppGrp', bound='ApplicationGroup')
|
|
|
27
28
|
log = logging.getLogger(__name__)
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
def _hidden_by_null_handler(logger: logging.Logger, level: int, msg: object) -> bool:
|
|
32
|
+
"""
|
|
33
|
+
True if the :class:`logging.NullHandler` cmdkit attaches to its logger at import is the only
|
|
34
|
+
reason Python's last-resort handler did not print a record with `msg` at `level` from `logger`.
|
|
35
|
+
|
|
36
|
+
This follows :meth:`logging.Logger.handle` and :meth:`logging.Logger.callHandlers` in order,
|
|
37
|
+
and is false wherever Python would print nothing anyway: the logger is not enabled for `level`
|
|
38
|
+
(a disabled logger, a logger level above `level`, or :func:`logging.disable`), the logger's
|
|
39
|
+
own filters reject the record, or :data:`logging.lastResort` is unset or above `level`.
|
|
40
|
+
Otherwise the logger and its ancestors are searched for as long as they propagate. Any handler
|
|
41
|
+
found, whatever its level, other than cmdkit's own (recognised by the mark set on it where the
|
|
42
|
+
package attaches it) means the application has configured logging: false. That includes a
|
|
43
|
+
:class:`logging.NullHandler` the application adds to the `cmdkit` logger itself to silence it.
|
|
44
|
+
If no handler is found at all, Python's last resort has already printed the record: also
|
|
45
|
+
false, so the message appears only once.
|
|
46
|
+
"""
|
|
47
|
+
if not logger.isEnabledFor(level):
|
|
48
|
+
return False
|
|
49
|
+
if not logger.filter(logger.makeRecord(logger.name, level, '(unknown file)', 0, msg, (), None)):
|
|
50
|
+
return False
|
|
51
|
+
if logging.lastResort is None or level < logging.lastResort.level:
|
|
52
|
+
return False
|
|
53
|
+
found = False
|
|
54
|
+
current = logger
|
|
55
|
+
while current is not None:
|
|
56
|
+
for handler in current.handlers:
|
|
57
|
+
if getattr(handler, '_cmdkit_library_handler', False) is not True:
|
|
58
|
+
return False
|
|
59
|
+
found = True
|
|
60
|
+
if not current.propagate:
|
|
61
|
+
break
|
|
62
|
+
current = current.parent
|
|
63
|
+
return found
|
|
64
|
+
|
|
65
|
+
|
|
30
66
|
# NOTE: In next major release this will be made into an Enum
|
|
31
67
|
class ExitStatus(NamedTuple):
|
|
32
68
|
"""Collection of exit status values."""
|
|
@@ -96,8 +132,22 @@ class Application(abc.ABC):
|
|
|
96
132
|
Entry-point for application.
|
|
97
133
|
This is a try-except block that handles standard scenarios.
|
|
98
134
|
|
|
135
|
+
An :class:`~cmdkit.cli.ArgumentError` is passed to :data:`~Application.log_critical`
|
|
136
|
+
and ``exit_status.bad_argument`` is returned. If ``log_critical`` is the default and the
|
|
137
|
+
:class:`~logging.NullHandler` cmdkit attaches to its own logger is the only reason
|
|
138
|
+
Python's last-resort handler did not print the record (no handler of the application's
|
|
139
|
+
is found, and the ``cmdkit.app`` logger is neither silenced nor filtering the record
|
|
140
|
+
out), the message is also written as is to ``sys.stderr``, followed by a newline.
|
|
141
|
+
Where no handler is found at all, Python's last resort prints the record itself, and
|
|
142
|
+
that line stands alone. A :class:`~logging.NullHandler` the application adds to the
|
|
143
|
+
``cmdkit`` logger is a handler of the application's, and keeps the message silent.
|
|
144
|
+
Without a ``sys.stderr`` (it is ``None``) the message is skipped, and a failure to
|
|
145
|
+
write it (a closed stream or a broken pipe) is ignored, so ``main`` still returns
|
|
146
|
+
``exit_status.bad_argument``.
|
|
147
|
+
|
|
99
148
|
See Also:
|
|
100
149
|
:data:`~Application.exceptions`
|
|
150
|
+
:data:`~Application.log_critical`
|
|
101
151
|
"""
|
|
102
152
|
try:
|
|
103
153
|
if not cmdline:
|
|
@@ -124,6 +174,12 @@ class Application(abc.ABC):
|
|
|
124
174
|
|
|
125
175
|
except ArgumentError as error:
|
|
126
176
|
cls.log_critical(error)
|
|
177
|
+
if cls.log_critical == log.critical and _hidden_by_null_handler(log, logging.CRITICAL, error):
|
|
178
|
+
if sys.stderr is not None:
|
|
179
|
+
try:
|
|
180
|
+
print(error, file=sys.stderr)
|
|
181
|
+
except (OSError, ValueError):
|
|
182
|
+
pass
|
|
127
183
|
return exit_status.bad_argument
|
|
128
184
|
|
|
129
185
|
except KeyboardInterrupt:
|
|
@@ -10,7 +10,7 @@ local files and your environment.
|
|
|
10
10
|
|
|
11
11
|
# type annotations
|
|
12
12
|
from __future__ import annotations
|
|
13
|
-
from typing import Tuple, List, Dict, TypeVar, Callable, Union, Any
|
|
13
|
+
from typing import Tuple, List, Dict, TypeVar, Callable, Union, Any, Mapping
|
|
14
14
|
|
|
15
15
|
# standard libs
|
|
16
16
|
from collections import Counter
|
|
@@ -33,6 +33,85 @@ class ConfigurationError(Exception):
|
|
|
33
33
|
"""Exception specific to configuration errors."""
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
_MISSING = object()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _variant_siblings(key: Any) -> List[str]:
|
|
40
|
+
"""The other keys that are a variant of a name `key` is a variant of."""
|
|
41
|
+
if not isinstance(key, str):
|
|
42
|
+
return []
|
|
43
|
+
names = [key]
|
|
44
|
+
if key.endswith('_env'):
|
|
45
|
+
names.append(key[:-4])
|
|
46
|
+
if key.endswith('_eval'):
|
|
47
|
+
names.append(key[:-5])
|
|
48
|
+
return [other for name in names for other in (name, f'{name}_env', f'{name}_eval') if other != key]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _drop_displaced_variants(target: dict, new: Mapping) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Remove from `target` every variant of a name that `new` sets a different variant of,
|
|
54
|
+
at every level where the depth-first update will descend.
|
|
55
|
+
"""
|
|
56
|
+
for key, value in new.items():
|
|
57
|
+
for other in _variant_siblings(key):
|
|
58
|
+
if other in target and other not in new:
|
|
59
|
+
del target[other]
|
|
60
|
+
if isinstance(value, dict) and isinstance(target.get(key), dict):
|
|
61
|
+
_drop_displaced_variants(target[key], value)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _apply_layer(target: NSCoreMixin, new: Mapping) -> None:
|
|
65
|
+
"""Depth-first update of `target` with `new`, where the variants `new` sets displace the others."""
|
|
66
|
+
_drop_displaced_variants(target, new)
|
|
67
|
+
NSCoreMixin.update(target, new)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _value_at(mapping: Any, path: Tuple[Any, ...]) -> Any:
|
|
71
|
+
"""The value at `path` through nested mappings, or `_MISSING`."""
|
|
72
|
+
for key in path:
|
|
73
|
+
if not isinstance(mapping, dict) or key not in mapping:
|
|
74
|
+
return _MISSING
|
|
75
|
+
mapping = mapping[key]
|
|
76
|
+
return mapping
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _lacks(mapping: Any, path: Tuple[Any, ...]) -> bool:
|
|
80
|
+
"""Whether item access along `path` raises `KeyError`: a mapping on the way lacks the next key."""
|
|
81
|
+
for key in path:
|
|
82
|
+
if not isinstance(mapping, dict):
|
|
83
|
+
return False
|
|
84
|
+
if key not in mapping:
|
|
85
|
+
return True
|
|
86
|
+
mapping = mapping[key]
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _depth_held(mapping: Any, path: Tuple[Any, ...]) -> int:
|
|
91
|
+
"""How many leading keys of `path` are held through nested mappings."""
|
|
92
|
+
for depth, key in enumerate(path):
|
|
93
|
+
if not isinstance(mapping, dict) or key not in mapping:
|
|
94
|
+
return depth
|
|
95
|
+
mapping = mapping[key]
|
|
96
|
+
return len(path)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _displaced(sources: List[Mapping], path: Tuple[Any, ...]) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
Whether a source sets another variant of a key on `path` in that key's place,
|
|
102
|
+
and not the key itself, above a source that holds `path` down to that key.
|
|
103
|
+
"""
|
|
104
|
+
held = set()
|
|
105
|
+
for source in sources:
|
|
106
|
+
for depth, key in enumerate(path):
|
|
107
|
+
parent = _value_at(source, path[:depth])
|
|
108
|
+
if depth in held and isinstance(parent, dict) and key not in parent \
|
|
109
|
+
and any(other in parent for other in _variant_siblings(key)):
|
|
110
|
+
return True
|
|
111
|
+
held.update(range(_depth_held(source, path)))
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
|
|
36
115
|
class Configuration(NSCoreMixin):
|
|
37
116
|
"""
|
|
38
117
|
An ordered collection of `Namespace` dictionaries.
|
|
@@ -54,6 +133,24 @@ class Configuration(NSCoreMixin):
|
|
|
54
133
|
|
|
55
134
|
>>> conf.namespaces.one.x
|
|
56
135
|
1
|
|
136
|
+
|
|
137
|
+
A parameter `name` and its `name_env` and `name_eval` variants are one key across
|
|
138
|
+
the layers: the highest layer that sets any of them decides, and only the variants
|
|
139
|
+
it sets remain in the merged view. Within a single layer, two variants of one name
|
|
140
|
+
are still ambiguous.
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
>>> conf = Configuration(one=Namespace({'db': {'password': 'secret'}}),
|
|
144
|
+
... two=Namespace({'db': {'password_eval': 'echo other'}}))
|
|
145
|
+
|
|
146
|
+
>>> dict(conf)
|
|
147
|
+
{'db': Namespace({'password_eval': 'echo other'})}
|
|
148
|
+
|
|
149
|
+
>>> conf.db.password
|
|
150
|
+
'other'
|
|
151
|
+
|
|
152
|
+
>>> conf.which('db', 'password_eval')
|
|
153
|
+
'two'
|
|
57
154
|
"""
|
|
58
155
|
|
|
59
156
|
local: Namespace # NOTE: used to track changes to the 'self'
|
|
@@ -89,7 +186,7 @@ class Configuration(NSCoreMixin):
|
|
|
89
186
|
for name, mapping in others.items():
|
|
90
187
|
if name != '_':
|
|
91
188
|
self.namespaces[name] = Namespace(mapping)
|
|
92
|
-
|
|
189
|
+
_apply_layer(self, self.namespaces[name])
|
|
93
190
|
else:
|
|
94
191
|
self.local.update(mapping)
|
|
95
192
|
|
|
@@ -180,6 +277,8 @@ class Configuration(NSCoreMixin):
|
|
|
180
277
|
Namespace({'x': 1, 'y': 3})
|
|
181
278
|
"""
|
|
182
279
|
namespaces = Namespace({**self.namespaces, '_': self.local})
|
|
280
|
+
if _lacks(self, path) and _displaced(list(namespaces.values()), path):
|
|
281
|
+
raise KeyError(f'Not found: {path}')
|
|
183
282
|
for label in reversed(list(namespaces.keys())):
|
|
184
283
|
try:
|
|
185
284
|
sub = namespaces[label]
|
|
@@ -231,7 +330,7 @@ class Configuration(NSCoreMixin):
|
|
|
231
330
|
|
|
232
331
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
233
332
|
"""Intercept parameter assignment."""
|
|
234
|
-
if name in self:
|
|
333
|
+
if name in self or any(other in self for other in _variant_siblings(name)):
|
|
235
334
|
self.update({name: value})
|
|
236
335
|
else:
|
|
237
336
|
super().__setattr__(name, value)
|
|
@@ -247,6 +346,9 @@ class Configuration(NSCoreMixin):
|
|
|
247
346
|
Instead, overrides have been implemented to capture these changes in a `local` namespace.
|
|
248
347
|
If you ask :func:`which` namespace a parameter has come from, and it was an in-place change,
|
|
249
348
|
it will be considered a member of the "_" namespace.
|
|
349
|
+
An in-place change follows the same rule as the layers for `_env` and `_eval`
|
|
350
|
+
variants: setting one variant of a name replaces any other variant of that name,
|
|
351
|
+
both in the "_" namespace and in the merged view.
|
|
250
352
|
|
|
251
353
|
Example:
|
|
252
354
|
>>> conf = Configuration(a=Namespace(x=1))
|
|
@@ -268,8 +370,8 @@ class Configuration(NSCoreMixin):
|
|
|
268
370
|
>>> dict(conf)
|
|
269
371
|
{'x': 2, 'y': 3}
|
|
270
372
|
"""
|
|
271
|
-
self.local
|
|
272
|
-
|
|
373
|
+
_apply_layer(self.local, dict(*args, **kwargs))
|
|
374
|
+
_apply_layer(self, dict(*args, **kwargs))
|
|
273
375
|
|
|
274
376
|
def pop(self, *args, **kwargs) -> Any:
|
|
275
377
|
"""
|
cmdkit-2.7.7/pyproject.toml
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
[tool.poetry]
|
|
2
|
-
name = "cmdkit"
|
|
3
|
-
version = "2.7.7"
|
|
4
|
-
description = "A command-line utility toolkit for Python."
|
|
5
|
-
readme = "README.rst"
|
|
6
|
-
license = "Apache-2.0"
|
|
7
|
-
homepage = "https://cmdkit.readthedocs.io"
|
|
8
|
-
documentation = "https://cmdkit.readthedocs.io"
|
|
9
|
-
repository = "https://github.com/glentner/cmdkit"
|
|
10
|
-
authors = [
|
|
11
|
-
"Geoffrey Lentner <glentner@purdue.edu>"
|
|
12
|
-
]
|
|
13
|
-
keywords = ["command-line", "utility", "toolkit"]
|
|
14
|
-
classifiers = [
|
|
15
|
-
"Development Status :: 5 - Production/Stable",
|
|
16
|
-
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
17
|
-
"Programming Language :: Python :: 3",
|
|
18
|
-
"Programming Language :: Python :: 3.9",
|
|
19
|
-
"Programming Language :: Python :: 3.10",
|
|
20
|
-
"Programming Language :: Python :: 3.11",
|
|
21
|
-
"Programming Language :: Python :: 3.12",
|
|
22
|
-
"Programming Language :: Python :: 3.13",
|
|
23
|
-
"License :: OSI Approved :: Apache Software License",
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
[tool.poetry.dependencies]
|
|
27
|
-
python = ">=3.9,<4.0"
|
|
28
|
-
tomli = {version = "^2.1.0", optional = true}
|
|
29
|
-
tomli-w = {version = "^1.1.0", optional = true}
|
|
30
|
-
pyyaml = {version = "^6.0.1", optional = true}
|
|
31
|
-
|
|
32
|
-
[tool.poetry.extras]
|
|
33
|
-
toml = ['tomli', 'tomli-w']
|
|
34
|
-
yaml = ['pyyaml']
|
|
35
|
-
|
|
36
|
-
[tool.poetry.group.docs.dependencies]
|
|
37
|
-
sphinx = "^7.2.6"
|
|
38
|
-
sphinx-sitemap = "^2.5.1"
|
|
39
|
-
sphinx-autobuild = "^2021.3.14"
|
|
40
|
-
sphinx-copybutton = "^0.5.2"
|
|
41
|
-
sphinxext-opengraph = "^0.9.1"
|
|
42
|
-
sphinxcontrib-details-directive = "^0.1.0"
|
|
43
|
-
sphinx-toolbox = "^3.5.0"
|
|
44
|
-
pydata-sphinx-theme = "^0.15.1"
|
|
45
|
-
enum-tools = {extras = ["sphinx"], version = "^0.10.0"}
|
|
46
|
-
docutils = "^0.21.1" # NOTE: https://github.com/python-poetry/poetry/issues/9293
|
|
47
|
-
|
|
48
|
-
[tool.poetry.group.dev.dependencies]
|
|
49
|
-
tomli = "^2.1.0"
|
|
50
|
-
tomli-w = "^1.1.0"
|
|
51
|
-
pyyaml = "^6.0.1"
|
|
52
|
-
pytest = "^7.4.4"
|
|
53
|
-
hypothesis = "^6.92.6"
|
|
54
|
-
sphinx-autobuild = "^2021.3.14"
|
|
55
|
-
|
|
56
|
-
[build-system]
|
|
57
|
-
requires = ["poetry-core>=1.0.0"]
|
|
58
|
-
build-backend = "poetry.core.masonry.api"
|
|
59
|
-
|
|
60
|
-
[tool.pytest.ini_options]
|
|
61
|
-
addopts = ["--strict-markers", ]
|
|
62
|
-
markers = [
|
|
63
|
-
"unit: Unit tests are short, interface driven tests on discrete components.",
|
|
64
|
-
"integration: Integration tests are often longer and deal with the interaction between systems.",
|
|
65
|
-
"parameterize: Place holder for parameterized tests (not a real type).",
|
|
66
|
-
]
|
|
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
|