|
|
|
@ -10,10 +10,13 @@ import re
|
|
|
|
|
import sys
|
|
|
|
|
import typing
|
|
|
|
|
import warnings
|
|
|
|
|
from argparse import BooleanOptionalAction
|
|
|
|
|
from argparse import Namespace
|
|
|
|
|
from collections import defaultdict
|
|
|
|
|
from collections.abc import Sequence
|
|
|
|
|
from collections.abc import Set
|
|
|
|
|
from enum import Enum
|
|
|
|
|
from types import GenericAlias as types_GenericAlias
|
|
|
|
|
from typing import Any
|
|
|
|
|
from typing import Callable
|
|
|
|
|
from typing import cast
|
|
|
|
@ -21,7 +24,9 @@ from typing import Collection
|
|
|
|
|
from typing import Dict
|
|
|
|
|
from typing import Generic
|
|
|
|
|
from typing import get_args
|
|
|
|
|
from typing import Mapping
|
|
|
|
|
from typing import NoReturn
|
|
|
|
|
from typing import Tuple
|
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
from typing import TypeVar
|
|
|
|
|
from typing import Union
|
|
|
|
@ -36,72 +41,6 @@ else: # pragma: no cover
|
|
|
|
|
from typing import NamedTuple
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if sys.version_info < (3, 9): # pragma: no cover
|
|
|
|
|
from typing import List
|
|
|
|
|
from typing import _GenericAlias as types_GenericAlias
|
|
|
|
|
|
|
|
|
|
def removeprefix(self: str, prefix: str, /) -> str:
|
|
|
|
|
if self.startswith(prefix):
|
|
|
|
|
return self[len(prefix):]
|
|
|
|
|
else:
|
|
|
|
|
return self[:]
|
|
|
|
|
|
|
|
|
|
def get_typing_type(t: type) -> type:
|
|
|
|
|
if t.__module__ == 'builtins':
|
|
|
|
|
if t is NoneType:
|
|
|
|
|
return None
|
|
|
|
|
return getattr(typing, t.__name__.title(), t)
|
|
|
|
|
return t
|
|
|
|
|
|
|
|
|
|
class BooleanOptionalAction(argparse.Action):
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
option_strings,
|
|
|
|
|
dest,
|
|
|
|
|
default=None,
|
|
|
|
|
type=None, # noqa: A002
|
|
|
|
|
choices=None,
|
|
|
|
|
required=False,
|
|
|
|
|
help=None, # noqa: A002
|
|
|
|
|
metavar=None,
|
|
|
|
|
):
|
|
|
|
|
|
|
|
|
|
_option_strings = []
|
|
|
|
|
for option_string in option_strings:
|
|
|
|
|
_option_strings.append(option_string)
|
|
|
|
|
|
|
|
|
|
if option_string.startswith('--'):
|
|
|
|
|
option_string = '--no-' + option_string[2:]
|
|
|
|
|
_option_strings.append(option_string)
|
|
|
|
|
|
|
|
|
|
if help is not None and default is not None and default is not argparse.SUPPRESS:
|
|
|
|
|
help += ' (default: %(default)s)'
|
|
|
|
|
|
|
|
|
|
super().__init__(
|
|
|
|
|
option_strings=_option_strings,
|
|
|
|
|
dest=dest,
|
|
|
|
|
nargs=0,
|
|
|
|
|
default=default,
|
|
|
|
|
type=type,
|
|
|
|
|
choices=choices,
|
|
|
|
|
required=required,
|
|
|
|
|
help=help,
|
|
|
|
|
metavar=metavar,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None): # pragma: no cover dead: disable
|
|
|
|
|
if option_string in self.option_strings:
|
|
|
|
|
setattr(namespace, self.dest, not option_string.startswith('--no-'))
|
|
|
|
|
else: # pragma: no cover
|
|
|
|
|
List = list
|
|
|
|
|
from types import GenericAlias as types_GenericAlias
|
|
|
|
|
from argparse import BooleanOptionalAction
|
|
|
|
|
removeprefix = str.removeprefix
|
|
|
|
|
|
|
|
|
|
def get_typing_type(t: type) -> type | None:
|
|
|
|
|
return None if t is NoneType else t
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _isnamedtupleinstance(x: Any) -> bool: # pragma: no cover
|
|
|
|
|
t = type(x)
|
|
|
|
|
b = t.__bases__
|
|
|
|
@ -244,15 +183,14 @@ class Setting:
|
|
|
|
|
if isinstance(list_type, types_GenericAlias) and issubclass(list_type.__origin__, Collection):
|
|
|
|
|
return list_type, self.default is None
|
|
|
|
|
|
|
|
|
|
# Ensure that generic aliases work for python 3.8
|
|
|
|
|
if list_type is not None:
|
|
|
|
|
list_type = get_typing_type(list_type)
|
|
|
|
|
else:
|
|
|
|
|
list_type = get_typing_type(type(self.default))
|
|
|
|
|
if list_type is NoneType:
|
|
|
|
|
list_type = None
|
|
|
|
|
if list_type is None and self.default is not None:
|
|
|
|
|
list_type = type(self.default)
|
|
|
|
|
|
|
|
|
|
# Default to a list if we don't know what type of collection this is
|
|
|
|
|
if list_type is None or not issubclass(list_type, Collection):
|
|
|
|
|
list_type = List
|
|
|
|
|
if list_type is None or not issubclass(list_type, Collection) or issubclass(list_type, Enum):
|
|
|
|
|
list_type = list
|
|
|
|
|
|
|
|
|
|
# Get the item type (int) in list[int]
|
|
|
|
|
it = get_item_type(self.default)
|
|
|
|
@ -260,7 +198,7 @@ class Setting:
|
|
|
|
|
it = self.type
|
|
|
|
|
|
|
|
|
|
if it is self.__no_type:
|
|
|
|
|
return self._process_type() or List[str], self.default is None
|
|
|
|
|
return self._process_type() or list[str], self.default is None
|
|
|
|
|
|
|
|
|
|
# Try to get the generic alias for this type
|
|
|
|
|
if it is not None:
|
|
|
|
@ -292,7 +230,7 @@ class Setting:
|
|
|
|
|
'store_const': (type(self.const), default_is_none),
|
|
|
|
|
'count': (int, default_is_none),
|
|
|
|
|
'extend': self._guess_collection(),
|
|
|
|
|
'append_const': (List[type(self.const)], default_is_none), # type: ignore[misc]
|
|
|
|
|
'append_const': (list[type(self.const)], default_is_none), # type: ignore[misc]
|
|
|
|
|
'help': (None, default_is_none),
|
|
|
|
|
'version': (None, default_is_none),
|
|
|
|
|
}
|
|
|
|
@ -330,7 +268,7 @@ class Setting:
|
|
|
|
|
|
|
|
|
|
def _guess_type(self) -> tuple[type | str | None, bool]:
|
|
|
|
|
if self.action == 'append':
|
|
|
|
|
return List[self._guess_type_internal()[0]], self.default is None # type: ignore[misc]
|
|
|
|
|
return list[self._guess_type_internal()[0]], self.default is None # type: ignore[misc]
|
|
|
|
|
return self._guess_type_internal()
|
|
|
|
|
|
|
|
|
|
def get_dest(self, prefix: str, names: Sequence[str], dest: str | None) -> tuple[str, str, str, bool]:
|
|
|
|
@ -365,7 +303,7 @@ class Setting:
|
|
|
|
|
return self.argparse_args, self.filter_argparse_kwargs()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TypedNS:
|
|
|
|
|
class TypedNS():
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
raise TypeError('TypedNS cannot be instantiated')
|
|
|
|
|
|
|
|
|
@ -375,7 +313,8 @@ class Group(NamedTuple):
|
|
|
|
|
v: dict[str, Setting]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Values = Dict[str, Dict[str, Any]]
|
|
|
|
|
Values = Mapping[str, Any]
|
|
|
|
|
_values = Dict[str, Dict[str, Any]]
|
|
|
|
|
Definitions = Dict[str, Group]
|
|
|
|
|
|
|
|
|
|
T = TypeVar('T', bound=Union[Values, Namespace, TypedNS])
|
|
|
|
@ -388,7 +327,7 @@ class Config(NamedTuple, Generic[T]):
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
ArgParser = Union[argparse._MutuallyExclusiveGroup, argparse._ArgumentGroup, argparse.ArgumentParser]
|
|
|
|
|
ns = Namespace | TypedNS | Config[T] | None
|
|
|
|
|
ns = Union[TypedNS, Config[T], None]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _type_to_string(t: type | str) -> tuple[str, str]:
|
|
|
|
@ -546,7 +485,7 @@ def get_options(config: Config[T], group: str) -> dict[str, Any]:
|
|
|
|
|
if name in internal_names:
|
|
|
|
|
values[internal_names[name].dest] = value
|
|
|
|
|
else:
|
|
|
|
|
values[removeprefix(name, f'{group}').lstrip('_')] = value
|
|
|
|
|
values[name.removeprefix(f'{group}').lstrip('_')] = value
|
|
|
|
|
return values
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -597,7 +536,7 @@ def normalize_config(
|
|
|
|
|
if not file and not cmdline:
|
|
|
|
|
raise ValueError('Invalid parameters: you must set either file or cmdline to True')
|
|
|
|
|
|
|
|
|
|
normalized: Values = {}
|
|
|
|
|
normalized: dict[str, dict[str, Any]] = {}
|
|
|
|
|
options = config.values
|
|
|
|
|
definitions = _get_internal_definitions(config=config, persistent=persistent)
|
|
|
|
|
for group_name, group in definitions.items():
|
|
|
|
@ -665,7 +604,7 @@ def clean_config(
|
|
|
|
|
persistent: Include unknown keys in persistent groups and unknown groups
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
cleaned, _ = normalize_config(config, file=file, cmdline=cmdline, default=default, persistent=persistent)
|
|
|
|
|
cleaned, _ = cast(Config[_values], normalize_config(config, file=file, cmdline=cmdline, default=default, persistent=persistent))
|
|
|
|
|
for group in list(cleaned.keys()):
|
|
|
|
|
if not cleaned[group]:
|
|
|
|
|
del cleaned[group]
|
|
|
|
@ -1108,6 +1047,32 @@ def persistent_group(manager: Manager) -> None:
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SettngsNS(TypedNS):
|
|
|
|
|
Example_Group__hello: str
|
|
|
|
|
Example_Group__save: bool
|
|
|
|
|
Example_Group__verbose: bool
|
|
|
|
|
|
|
|
|
|
persistent__test: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Example_Group(typing.TypedDict):
|
|
|
|
|
hello: str
|
|
|
|
|
save: bool
|
|
|
|
|
verbose: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class persistent(typing.TypedDict):
|
|
|
|
|
test: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SettngsDict = typing.TypedDict(
|
|
|
|
|
'SettngsDict', {
|
|
|
|
|
'Example Group': Example_Group,
|
|
|
|
|
'persistent': persistent,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _main(args: list[str] | None = None) -> None:
|
|
|
|
|
settings_path = pathlib.Path('./settings.json')
|
|
|
|
|
manager = Manager(description='This is an example', epilog='goodbye!')
|
|
|
|
@ -1115,11 +1080,11 @@ def _main(args: list[str] | None = None) -> None:
|
|
|
|
|
manager.add_group('Example Group', example_group)
|
|
|
|
|
manager.add_persistent_group('persistent', persistent_group)
|
|
|
|
|
|
|
|
|
|
file_config, success = manager.parse_file(settings_path)
|
|
|
|
|
file_config, success = cast(Tuple[Config[SettngsDict], bool], manager.parse_file(settings_path))
|
|
|
|
|
file_namespace = manager.get_namespace(file_config, file=True, cmdline=True)
|
|
|
|
|
|
|
|
|
|
merged_config = manager.parse_cmdline(args=args, config=file_namespace)
|
|
|
|
|
merged_namespace = manager.get_namespace(merged_config, file=True, cmdline=True)
|
|
|
|
|
merged_config = cast(Config[SettngsDict], manager.parse_cmdline(args=args, config=file_namespace))
|
|
|
|
|
merged_namespace = cast(Config[SettngsNS], manager.get_namespace(merged_config, file=True, cmdline=True))
|
|
|
|
|
|
|
|
|
|
print(f'Hello {merged_config.values["Example Group"]["hello"]}') # noqa: T201
|
|
|
|
|
if merged_namespace.values.Example_Group__save:
|
|
|
|
|