lockss-pybasic 0.2.0.dev1__tar.gz → 0.2.0.dev3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lockss-pybasic
3
- Version: 0.2.0.dev1
3
+ Version: 0.2.0.dev3
4
4
  Summary: Basic Python utilities
5
5
  License: BSD-3-Clause
6
6
  Author: Thib Guicherd-Callin
@@ -28,7 +28,7 @@
28
28
 
29
29
  [project]
30
30
  name = "lockss-pybasic"
31
- version = "0.2.0-dev1" # Always change in __init__.py, and at release time in README.rst and CHANGELOG.rst
31
+ version = "0.2.0-dev3" # 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"
@@ -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.2.0-dev1'
39
+ __version__ = '0.2.0-dev3'
@@ -34,47 +34,50 @@ Command line utilities.
34
34
 
35
35
  from collections.abc import Callable
36
36
  import sys
37
- from typing import Any, Dict, Generic, Optional, TypeVar
37
+ from typing import Any, ClassVar, Dict, Generic, Optional, TypeVar, TYPE_CHECKING
38
38
 
39
- from pydantic.v1 import BaseModel
39
+ if TYPE_CHECKING:
40
+ from _typeshed import SupportsWrite
41
+
42
+ from pydantic.v1 import BaseModel, PrivateAttr, create_model
43
+ from pydantic.v1.fields import FieldInfo
40
44
  from pydantic_argparse import ArgumentParser
41
45
  from pydantic_argparse.argparse.actions import SubParsersAction
42
46
  from rich_argparse import RichHelpFormatter
43
47
 
44
48
 
45
- class ActionCommand(Callable, BaseModel):
46
- """
47
- Base class for a pydantic-argparse style command.
48
- """
49
- pass
49
+ class StringCommand(BaseModel):
50
50
 
51
+ def display(self, file: SupportsWrite[str]=sys.stdout) -> None:
52
+ print(getattr(self, 'display_string'), file=file)
51
53
 
52
- class StringCommand(ActionCommand):
53
- """
54
- A pydantic-argparse style command that prints a string.
54
+ @staticmethod
55
+ def make(model_name: str, option_name: str, description: str, display_string: str):
56
+ return create_model(model_name,
57
+ __base__=StringCommand,
58
+ **{option_name: (Optional[bool], FieldInfo(False, description=description)),
59
+ display_string: PrivateAttr(display_string)})
55
60
 
56
- Example of use:
57
61
 
58
- .. code-block:: python
62
+ class CopyrightCommand(StringCommand):
59
63
 
60
- class MyCliModel(BaseModel):
61
- copyright: Optional[StringCommand.type(my_copyright_string)] = Field(description=COPYRIGHT_DESCRIPTION)
64
+ @staticmethod
65
+ def make(display_string: str):
66
+ return StringCommand.make('CopyrightCommand', 'copyright', 'print the copyright and exit', display_string)
62
67
 
63
- See also the convenience constants ``COPYRIGHT_DESCRIPTION``,
64
- ``LICENSE_DESCRIPTION``, and ``VERSION_DESCRIPTION``.
65
- """
68
+
69
+ class LicenseCommand(StringCommand):
66
70
 
67
71
  @staticmethod
68
- def type(display_str: str):
69
- class _StringCommand(StringCommand):
70
- def __call__(self, file=sys.stdout, **kwargs):
71
- print(display_str, file=file)
72
- return _StringCommand
72
+ def make(display_string: str):
73
+ return StringCommand.make('LicenseCommand', 'license', 'print the software license and exit', display_string)
73
74
 
74
75
 
75
- COPYRIGHT_DESCRIPTION = 'print the copyright and exit'
76
- LICENSE_DESCRIPTION = 'print the software license and exit'
77
- VERSION_DESCRIPTION = 'print the version number and exit'
76
+ class VersionCommand(StringCommand):
77
+
78
+ @staticmethod
79
+ def make(display_string: str):
80
+ return StringCommand.make('VersionCommand', 'version', 'print the version number and exit', display_string)
78
81
 
79
82
 
80
83
  BaseModelT = TypeVar('BaseModelT', bound=BaseModel)
@@ -147,7 +150,7 @@ class BaseCli(Generic[BaseModelT]):
147
150
  if callable(func):
148
151
  func(base_model) # FIXME?
149
152
  else:
150
- self._parser.exit(1, f'internal error: no {func_name} callable for the {" ".join(sub for sub in subcommands)}')
153
+ self._parser.exit(1, f'internal error: no {func_name} callable for the {" ".join(sub for sub in subcommands)} command')
151
154
 
152
155
  def _initialize_rich_argparse(self) -> None:
153
156
  """
@@ -179,7 +182,7 @@ class BaseCli(Generic[BaseModelT]):
179
182
  })
180
183
 
181
184
 
182
- def at_most_one_from_enum(model_cls, values: Dict[str, Any], enum_cls) -> Dict[str, Any]:
185
+ def at_most_one_from_enum(model_cls: type[BaseModel], values: Dict[str, Any], enum_cls) -> Dict[str, Any]:
183
186
  """
184
187
  Among the fields of a Pydantic-Argparse model whose ``Field`` definition is
185
188
  tagged with the ``enum`` keyword set to the given ``Enum`` type, ensures
@@ -195,7 +198,7 @@ def at_most_one_from_enum(model_cls, values: Dict[str, Any], enum_cls) -> Dict[s
195
198
  enum_names = [field_name for field_name, model_field in model_cls.__fields__.items() if model_field.field_info.extra.get('enum') == enum_cls]
196
199
  ret = [field_name for field_name in enum_names if values.get(field_name)]
197
200
  if (length := len(ret)) > 1:
198
- 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])})')
201
+ raise ValueError(f'at most one of {', '.join([option_name(model_cls, enum_name) for enum_name in enum_names])} allowed; got {length} ({', '.join([option_name(enum_name) for enum_name in ret])})')
199
202
  return values
200
203
 
201
204
 
@@ -219,25 +222,29 @@ def get_from_enum(model_inst, enum_cls, default=None):
219
222
  return default
220
223
 
221
224
 
222
- def at_most_one(values: Dict[str, Any], *names: str):
225
+ def at_most_one(model_cls: type[BaseModel], values: Dict[str, Any], *names: str):
223
226
  if (length := _matchy_length(values, *names)) > 1:
224
- raise ValueError(f'at most one of {', '.join([option_name(name) for name in names])} is allowed, got {length}')
227
+ raise ValueError(f'at most one of {', '.join([option_name(model_cls, name) for name in names])} allowed; got {length}')
225
228
  return values
226
229
 
227
230
 
228
- def exactly_one(values: Dict[str, Any], *names: str):
231
+ def exactly_one(model_cls: type[BaseModel], values: Dict[str, Any], *names: str):
229
232
  if (length := _matchy_length(values, *names)) != 1:
230
- raise ValueError(f'exactly one of {', '.join([option_name(name) for name in names])} is required, got {length}')
233
+ raise ValueError(f'exactly one of {', '.join([option_name(model_cls, name) for name in names])} required; got {length}')
231
234
  return values
232
235
 
233
236
 
234
- def one_or_more(values: Dict[str, Any], *names: str):
237
+ def one_or_more(model_cls: type[BaseModel], values: Dict[str, Any], *names: str):
235
238
  if _matchy_length(values, *names) == 0:
236
- raise ValueError(f'one or more of {', '.join([option_name(name) for name in names])} is required')
239
+ raise ValueError(f'one or more of {', '.join([option_name(model_cls, name) for name in names])} required')
237
240
  return values
238
241
 
239
242
 
240
- def option_name(name: str) -> str:
243
+ def option_name(model_cls: type[BaseModel], name: str) -> str:
244
+ if (info := model_cls.__fields__.get(name)) is None:
245
+ raise RuntimeError(f'invalid name: {name}')
246
+ if alias := info.alias:
247
+ name = alias
241
248
  return f'{('-' if len(name) == 1 else '--')}{name.replace('_', '-')}'
242
249
 
243
250