7 Commits
0.10.2 ... main

Author SHA1 Message Date
cfc55b7366 Drop python 3.8 2025-05-01 17:13:07 -07:00
6b158ca495 Handle lists of Enum 2025-05-01 17:01:34 -07:00
3b0ae0f24a [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.15.2 → v3.16.0](https://github.com/asottile/pyupgrade/compare/v3.15.2...v3.16.0)
- [github.com/hhatto/autopep8: v2.2.0 → v2.3.1](https://github.com/hhatto/autopep8/compare/v2.2.0...v2.3.1)
- [github.com/PyCQA/flake8: 7.0.0 → 7.1.0](https://github.com/PyCQA/flake8/compare/7.0.0...7.1.0)
2024-06-24 17:18:37 +00:00
f1735c879d Don't inherit from argparse Namespace 2024-06-06 19:33:07 -07:00
afdc11eb6f [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/reorder-python-imports: v3.12.0 → v3.13.0](https://github.com/asottile/reorder-python-imports/compare/v3.12.0...v3.13.0)
- [github.com/hhatto/autopep8: v2.1.1 → v2.2.0](https://github.com/hhatto/autopep8/compare/v2.1.1...v2.2.0)
2024-06-03 17:17:48 +00:00
9b71af1c75 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/hhatto/autopep8: v2.1.0 → v2.1.1](https://github.com/hhatto/autopep8/compare/v2.1.0...v2.1.1)
2024-05-27 17:15:51 +00:00
8d772c6513 Improve types 2024-05-20 15:09:33 -07:00
5 changed files with 58 additions and 93 deletions

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python_version: ['3.8', '3.9','3.10','3.11']
python_version: ['3.9','3.10','3.11','3.13']
os: [ubuntu-latest]
steps:

View File

@ -14,7 +14,7 @@ repos:
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/asottile/reorder-python-imports
rev: v3.12.0
rev: v3.13.0
hooks:
- id: reorder-python-imports
args: [--py38-plus, --add-import, 'from __future__ import annotations']
@ -28,17 +28,17 @@ repos:
hooks:
- id: dead
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.2
rev: v3.16.0
hooks:
- id: pyupgrade
args: [--py38-plus]
exclude: tests
- repo: https://github.com/hhatto/autopep8
rev: v2.1.0
rev: v2.3.1
hooks:
- id: autopep8
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
rev: 7.1.0
hooks:
- id: flake8
additional_dependencies: [flake8-encodings, flake8-warnings, flake8-builtins, flake8-print]

View File

@ -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:

View File

@ -19,7 +19,7 @@ classifiers =
packages = find:
install_requires =
typing-extensions>=4.3.0;python_version < '3.11'
python_requires = >=3.8
python_requires = >=3.9
include_package_data = True
[options.packages.find]
@ -31,7 +31,7 @@ exclude =
settngs = py.typed
[tox:tox]
envlist = py3.8,py3.9,py3.10,py3.11,py3.12,pypy3
envlist = py3.9,py3.10,py3.11,py3.12,py3.13,pypy3
[testenv]
deps = -rrequirements-dev.txt

View File

@ -258,7 +258,7 @@ class TestValues:
# manager_unknown.add_persistent_group('persistent', lambda parser: parser.add_setting('--world', default='world'))
defaults = manager.defaults()
defaults.values['test'] = 'fail' # type: ignore[assignment] # Not defined in manager, should be removed
defaults.values['test'] = 'fail' # type: ignore[index] # Not defined in manager, should be removed
defaults.values['persistent']['hello'] = 'success' # Group is not defined in manager_unknown, should stay
normalized, _ = manager_unknown.normalize_config(defaults.values, file=True)