lockss-pybasic 0.1.0.dev23__tar.gz → 0.2.0.dev2__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.
@@ -3,11 +3,17 @@ Release Notes
3
3
  =============
4
4
 
5
5
  -----
6
- 0.1.0
6
+ 0.2.0
7
7
  -----
8
8
 
9
9
  Released: ?
10
10
 
11
+ -----
12
+ 0.1.0
13
+ -----
14
+
15
+ Released: 2025-07-01
16
+
11
17
  Initial release, including:
12
18
 
13
19
  * ``cliutil``
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lockss-pybasic
3
- Version: 0.1.0.dev23
3
+ Version: 0.2.0.dev2
4
4
  Summary: Basic Python utilities
5
5
  License: BSD-3-Clause
6
6
  Author: Thib Guicherd-Callin
@@ -12,12 +12,9 @@ Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Environment :: Console
13
13
  Classifier: Framework :: Pydantic :: 2
14
14
  Classifier: Intended Audience :: Developers
15
- Classifier: Intended Audience :: System Administrators
16
15
  Classifier: License :: OSI Approved :: BSD License
17
16
  Classifier: Programming Language :: Python
18
17
  Classifier: Topic :: Software Development :: Libraries
19
- Classifier: Topic :: System :: Archiving
20
- Classifier: Topic :: Utilities
21
18
  Requires-Dist: pydantic (>=2.11.0,<3.0.0)
22
19
  Requires-Dist: pydantic-argparse (>=0.10.0,<0.11.0)
23
20
  Requires-Dist: rich-argparse (>=1.7.0,<1.8.0)
@@ -29,8 +26,8 @@ Description-Content-Type: text/x-rst
29
26
  lockss-pybasic
30
27
  ==============
31
28
 
32
- .. |RELEASE| replace:: 0.1.0-dev23
33
- .. |RELEASE_DATE| replace:: ?
29
+ .. |RELEASE| replace:: 0.1.0
30
+ .. |RELEASE_DATE| replace:: 2025-07-01
34
31
 
35
32
  **Latest release:** |RELEASE| (|RELEASE_DATE|)
36
33
 
@@ -2,8 +2,8 @@
2
2
  lockss-pybasic
3
3
  ==============
4
4
 
5
- .. |RELEASE| replace:: 0.1.0-dev23
6
- .. |RELEASE_DATE| replace:: ?
5
+ .. |RELEASE| replace:: 0.1.0
6
+ .. |RELEASE_DATE| replace:: 2025-07-01
7
7
 
8
8
  **Latest release:** |RELEASE| (|RELEASE_DATE|)
9
9
 
@@ -28,7 +28,7 @@
28
28
 
29
29
  [project]
30
30
  name = "lockss-pybasic"
31
- version = "0.1.0-dev23" # Always change in __init__.py, and at release time in README.rst and CHANGELOG.rst
31
+ version = "0.2.0-dev2" # Always change in __init__.py, and at release time in README.rst and CHANGELOG.rst
32
32
  description = "Basic Python utilities"
33
33
  license = { text = "BSD-3-Clause" }
34
34
  readme = "README.rst"
@@ -50,12 +50,9 @@ classifiers = [
50
50
  "Environment :: Console",
51
51
  "Framework :: Pydantic :: 2",
52
52
  "Intended Audience :: Developers",
53
- "Intended Audience :: System Administrators",
54
53
  "License :: OSI Approved :: BSD License",
55
54
  "Programming Language :: Python",
56
55
  "Topic :: Software Development :: Libraries",
57
- "Topic :: System :: Archiving",
58
- "Topic :: Utilities",
59
56
  ]
60
57
 
61
58
  [project.urls]
@@ -36,4 +36,4 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
36
36
  POSSIBILITY OF SUCH DAMAGE.
37
37
  '''.strip()
38
38
 
39
- __version__ = '0.1.0-dev23'
39
+ __version__ = '0.2.0-dev2'
@@ -129,22 +129,25 @@ class BaseCli(Generic[BaseModelT]):
129
129
 
130
130
  def dispatch(self) -> None:
131
131
  """
132
- Dispatches from the first field ``x_y_z`` in ``self.args`` that is a
132
+ Dispatches from the first field ``x_y_z`` in ``self._args`` that is a
133
133
  command (i.e. whose value derives from ``BaseModel``) to a method
134
134
  called ``_x_y_z``.
135
135
  """
136
- field_names = self._args.__class__.__fields__.keys()
136
+ self._dispatch_recursive(self._args, [])
137
+
138
+ def _dispatch_recursive(self, base_model: BaseModel, subcommands: list[str]) -> None:
139
+ field_names = base_model.__class__.__fields__.keys()
137
140
  for field_name in field_names:
138
- field_value = getattr(self._args, field_name)
141
+ field_value = getattr(base_model, field_name)
139
142
  if issubclass(type(field_value), BaseModel):
140
- func = getattr(self, f'_{field_name}')
141
- if callable(func):
142
- func(field_value)
143
- else:
144
- self._parser.exit(1, f'internal error: no _{field_name} callable for the {field_name} command')
145
- break
143
+ self._dispatch_recursive(field_value, [*subcommands, field_name])
144
+ return
145
+ func_name = ''.join(f'_{sub}' for sub in subcommands)
146
+ func = getattr(self, func_name)
147
+ if callable(func):
148
+ func(base_model) # FIXME?
146
149
  else:
147
- self._parser.error(f'unknown command; expected one of {', '.join(field_names)}')
150
+ self._parser.exit(1, f'internal error: no {func_name} callable for the {" ".join(sub for sub in subcommands)} command')
148
151
 
149
152
  def _initialize_rich_argparse(self) -> None:
150
153
  """
@@ -176,7 +179,7 @@ class BaseCli(Generic[BaseModelT]):
176
179
  })
177
180
 
178
181
 
179
- def at_most_one_from_enum(model_cls, values: Dict[str, Any], enum_cls) -> Dict[str, Any]:
182
+ def at_most_one_from_enum(model_cls: type[BaseModel], values: Dict[str, Any], enum_cls) -> Dict[str, Any]:
180
183
  """
181
184
  Among the fields of a Pydantic-Argparse model whose ``Field`` definition is
182
185
  tagged with the ``enum`` keyword set to the given ``Enum`` type, ensures
@@ -192,7 +195,7 @@ def at_most_one_from_enum(model_cls, values: Dict[str, Any], enum_cls) -> Dict[s
192
195
  enum_names = [field_name for field_name, model_field in model_cls.__fields__.items() if model_field.field_info.extra.get('enum') == enum_cls]
193
196
  ret = [field_name for field_name in enum_names if values.get(field_name)]
194
197
  if (length := len(ret)) > 1:
195
- raise ValueError(f'at most one of {', '.join([option_name(enum_name) for enum_name in enum_names])} is allowed, got {length} ({', '.join([option_name(enum_name) for enum_name in ret])})')
198
+ raise ValueError(f'at most one of {', '.join([option_name(model_cls, enum_name) for enum_name in enum_names])} is allowed, got {length} ({', '.join([option_name(enum_name) for enum_name in ret])})')
196
199
  return values
197
200
 
198
201
 
@@ -216,25 +219,29 @@ def get_from_enum(model_inst, enum_cls, default=None):
216
219
  return default
217
220
 
218
221
 
219
- def at_most_one(values: Dict[str, Any], *names: str):
222
+ def at_most_one(model_cls: type[BaseModel], values: Dict[str, Any], *names: str):
220
223
  if (length := _matchy_length(values, *names)) > 1:
221
- raise ValueError(f'at most one of {', '.join([option_name(name) for name in names])} is allowed, got {length}')
224
+ raise ValueError(f'at most one of {', '.join([option_name(model_cls, name) for name in names])} is allowed, got {length}')
222
225
  return values
223
226
 
224
227
 
225
- def exactly_one(values: Dict[str, Any], *names: str):
228
+ def exactly_one(model_cls: type[BaseModel], values: Dict[str, Any], *names: str):
226
229
  if (length := _matchy_length(values, *names)) != 1:
227
- raise ValueError(f'exactly one of {', '.join([option_name(name) for name in names])} is required, got {length}')
230
+ raise ValueError(f'exactly one of {', '.join([option_name(model_cls, name) for name in names])} is required, got {length}')
228
231
  return values
229
232
 
230
233
 
231
- def one_or_more(values: Dict[str, Any], *names: str):
234
+ def one_or_more(model_cls: type[BaseModel], values: Dict[str, Any], *names: str):
232
235
  if _matchy_length(values, *names) == 0:
233
- raise ValueError(f'one or more of {', '.join([option_name(name) for name in names])} is required')
236
+ raise ValueError(f'one or more of {', '.join([option_name(model_cls, name) for name in names])} is required')
234
237
  return values
235
238
 
236
239
 
237
- def option_name(name: str) -> str:
240
+ def option_name(model_cls: type[BaseModel], name: str) -> str:
241
+ if (info := model_cls.__fields__.get(name)) is None:
242
+ raise RuntimeError(f'invalid name: {name}')
243
+ if alias := info.alias:
244
+ name = alias
238
245
  return f'{('-' if len(name) == 1 else '--')}{name.replace('_', '-')}'
239
246
 
240
247