10 Commits

Author SHA1 Message Date
d284039f12 Update pre-commit 2025-08-30 20:26:32 -07:00
5fc7a5173d Update tests
Remove python3.8 leftovers
Add prog to create_argparser
Remove metavar usage from BooleanOptionalAction
2025-08-30 20:26:18 -07:00
ecda6fe610 Extract arguments for generic types 2025-08-30 20:07:43 -07:00
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 171 additions and 237 deletions

View File

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

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0 rev: v6.0.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
@ -10,39 +10,39 @@ repos:
- id: name-tests-test - id: name-tests-test
- id: requirements-txt-fixer - id: requirements-txt-fixer
- repo: https://github.com/asottile/setup-cfg-fmt - repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.5.0 rev: v2.8.0
hooks: hooks:
- id: setup-cfg-fmt - id: setup-cfg-fmt
- repo: https://github.com/asottile/reorder-python-imports - repo: https://github.com/asottile/reorder-python-imports
rev: v3.12.0 rev: v3.15.0
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
args: [--py38-plus, --add-import, 'from __future__ import annotations'] args: [--py38-plus, --add-import, 'from __future__ import annotations']
- repo: https://github.com/asottile/add-trailing-comma - repo: https://github.com/asottile/add-trailing-comma
rev: v3.1.0 rev: v3.2.0
hooks: hooks:
- id: add-trailing-comma - id: add-trailing-comma
args: [--py36-plus] args: [--py36-plus]
- repo: https://github.com/asottile/dead - repo: https://github.com/asottile/dead
rev: v1.5.2 rev: v2.1.0
hooks: hooks:
- id: dead - id: dead
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.15.2 rev: v3.20.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py38-plus] args: [--py38-plus]
exclude: tests exclude: tests
- repo: https://github.com/hhatto/autopep8 - repo: https://github.com/hhatto/autopep8
rev: v2.1.0 rev: v2.3.2
hooks: hooks:
- id: autopep8 - id: autopep8
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 7.0.0 rev: 7.3.0
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [flake8-encodings, flake8-warnings, flake8-builtins, flake8-print] additional_dependencies: [flake8-encodings, flake8-warnings, flake8-builtins, flake8-print]
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0 rev: v1.17.1
hooks: hooks:
- id: mypy - id: mypy

View File

@ -10,10 +10,13 @@ import re
import sys import sys
import typing import typing
import warnings import warnings
from argparse import BooleanOptionalAction
from argparse import Namespace from argparse import Namespace
from collections import defaultdict from collections import defaultdict
from collections.abc import Sequence from collections.abc import Sequence
from collections.abc import Set from collections.abc import Set
from enum import Enum
from types import GenericAlias as types_GenericAlias
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
@ -21,7 +24,9 @@ from typing import Collection
from typing import Dict from typing import Dict
from typing import Generic from typing import Generic
from typing import get_args from typing import get_args
from typing import Mapping
from typing import NoReturn from typing import NoReturn
from typing import Tuple
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
@ -36,72 +41,6 @@ else: # pragma: no cover
from typing import NamedTuple 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 def _isnamedtupleinstance(x: Any) -> bool: # pragma: no cover
t = type(x) t = type(x)
b = t.__bases__ b = t.__bases__
@ -244,15 +183,14 @@ class Setting:
if isinstance(list_type, types_GenericAlias) and issubclass(list_type.__origin__, Collection): if isinstance(list_type, types_GenericAlias) and issubclass(list_type.__origin__, Collection):
return list_type, self.default is None return list_type, self.default is None
# Ensure that generic aliases work for python 3.8 if list_type is NoneType:
if list_type is not None: list_type = None
list_type = get_typing_type(list_type) if list_type is None and self.default is not None:
else: list_type = type(self.default)
list_type = get_typing_type(type(self.default))
# Default to a list if we don't know what type of collection this is # 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): if list_type is None or not issubclass(list_type, Collection) or issubclass(list_type, Enum):
list_type = List list_type = list
# Get the item type (int) in list[int] # Get the item type (int) in list[int]
it = get_item_type(self.default) it = get_item_type(self.default)
@ -260,7 +198,7 @@ class Setting:
it = self.type it = self.type
if it is self.__no_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 # Try to get the generic alias for this type
if it is not None: if it is not None:
@ -271,7 +209,7 @@ class Setting:
... ...
# Fall back to list[str] if anything fails # Fall back to list[str] if anything fails
return list_type[str], self.default is None # type: ignore[index] return cast(type, list_type[str]), self.default is None # type: ignore[index]
except Exception: except Exception:
return None, self.default is None return None, self.default is None
@ -281,7 +219,7 @@ class Setting:
if isinstance(self.type, type): if isinstance(self.type, type):
return self.type return self.type
return typing.get_type_hints(self.type).get('return', None) # type: ignore[no-any-return] return cast(dict[str, type], typing.get_type_hints(self.type)).get('return', None)
def _guess_type_internal(self) -> tuple[type | str | None, bool]: def _guess_type_internal(self) -> tuple[type | str | None, bool]:
default_is_none = self.default is None default_is_none = self.default is None
@ -292,7 +230,7 @@ class Setting:
'store_const': (type(self.const), default_is_none), 'store_const': (type(self.const), default_is_none),
'count': (int, default_is_none), 'count': (int, default_is_none),
'extend': self._guess_collection(), 'extend': self._guess_collection(),
'append_const': (List[type(self.const)], default_is_none), # type: ignore[misc] 'append_const': (cast(type, list[type(self.const)]), default_is_none), # type: ignore[misc]
'help': (None, default_is_none), 'help': (None, default_is_none),
'version': (None, default_is_none), 'version': (None, default_is_none),
} }
@ -330,7 +268,7 @@ class Setting:
def _guess_type(self) -> tuple[type | str | None, bool]: def _guess_type(self) -> tuple[type | str | None, bool]:
if self.action == 'append': if self.action == 'append':
return List[self._guess_type_internal()[0]], self.default is None # type: ignore[misc] return cast(type, list[self._guess_type_internal()[0]]), self.default is None # type: ignore[misc]
return self._guess_type_internal() return self._guess_type_internal()
def get_dest(self, prefix: str, names: Sequence[str], dest: str | None) -> tuple[str, str, str, bool]: 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() return self.argparse_args, self.filter_argparse_kwargs()
class TypedNS: class TypedNS():
def __init__(self) -> None: def __init__(self) -> None:
raise TypeError('TypedNS cannot be instantiated') raise TypeError('TypedNS cannot be instantiated')
@ -375,7 +313,8 @@ class Group(NamedTuple):
v: dict[str, Setting] v: dict[str, Setting]
Values = Dict[str, Dict[str, Any]] Values = Mapping[str, Any]
_values = Dict[str, Dict[str, Any]]
Definitions = Dict[str, Group] Definitions = Dict[str, Group]
T = TypeVar('T', bound=Union[Values, Namespace, TypedNS]) T = TypeVar('T', bound=Union[Values, Namespace, TypedNS])
@ -388,7 +327,18 @@ class Config(NamedTuple, Generic[T]):
if TYPE_CHECKING: if TYPE_CHECKING:
ArgParser = Union[argparse._MutuallyExclusiveGroup, argparse._ArgumentGroup, argparse.ArgumentParser] ArgParser = Union[argparse._MutuallyExclusiveGroup, argparse._ArgumentGroup, argparse.ArgumentParser]
ns = Namespace | TypedNS | Config[T] | None ns = Union[TypedNS, Config[T], None]
def _get_import(t: type) -> tuple[str, str]:
type_name = t.__name__
import_needed = ''
# Builtin types don't need an import
if t.__module__ != 'builtins':
import_needed = f'import {t.__module__}'
# Use the full imported name
type_name = t.__module__ + '.' + type_name
return type_name, import_needed
def _type_to_string(t: type | str) -> tuple[str, str]: def _type_to_string(t: type | str) -> tuple[str, str]:
@ -399,22 +349,23 @@ def _type_to_string(t: type | str) -> tuple[str, str]:
type_name = t type_name = t
# Handle generic aliases eg dict[str, str] instead of dict # Handle generic aliases eg dict[str, str] instead of dict
elif isinstance(t, types_GenericAlias): elif isinstance(t, types_GenericAlias):
if not get_args(t): args = get_args(t)
if args:
import_needed = ''
for arg in args:
_, arg_import = _get_import(arg)
import_needed += '\n' + arg_import
else:
t = t.__origin__.__name__ t = t.__origin__.__name__
type_name = str(t) type_name = str(t)
# Handle standard type objects # Handle standard type objects
elif isinstance(t, type): elif isinstance(t, type):
type_name = t.__name__ type_name, import_needed = _get_import(t)
# Builtin types don't need an import
if t.__module__ != 'builtins':
import_needed = f'import {t.__module__}'
# Use the full imported name
type_name = t.__module__ + '.' + type_name
# Expand Any to typing.Any # Expand Any to typing.Any
if type_name == 'Any': if type_name == 'Any':
type_name = 'typing.Any' type_name = 'typing.Any'
return type_name, import_needed return type_name.strip(), import_needed.strip()
def generate_ns(definitions: Definitions) -> tuple[str, str]: def generate_ns(definitions: Definitions) -> tuple[str, str]:
@ -546,7 +497,7 @@ def get_options(config: Config[T], group: str) -> dict[str, Any]:
if name in internal_names: if name in internal_names:
values[internal_names[name].dest] = value values[internal_names[name].dest] = value
else: else:
values[removeprefix(name, f'{group}').lstrip('_')] = value values[name.removeprefix(f'{group}').lstrip('_')] = value
return values return values
@ -597,7 +548,7 @@ def normalize_config(
if not file and not cmdline: if not file and not cmdline:
raise ValueError('Invalid parameters: you must set either file or cmdline to True') raise ValueError('Invalid parameters: you must set either file or cmdline to True')
normalized: Values = {} normalized: dict[str, dict[str, Any]] = {}
options = config.values options = config.values
definitions = _get_internal_definitions(config=config, persistent=persistent) definitions = _get_internal_definitions(config=config, persistent=persistent)
for group_name, group in definitions.items(): for group_name, group in definitions.items():
@ -665,7 +616,7 @@ def clean_config(
persistent: Include unknown keys in persistent groups and unknown groups 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()): for group in list(cleaned.keys()):
if not cleaned[group]: if not cleaned[group]:
del cleaned[group] del cleaned[group]
@ -753,11 +704,12 @@ def save_file(
return True return True
def create_argparser(definitions: Definitions, description: str, epilog: str) -> argparse.ArgumentParser: def create_argparser(definitions: Definitions, description: str, epilog: str, *, prog: str | None = None) -> argparse.ArgumentParser:
"""Creates an :class:`argparse.ArgumentParser` from all cmdline settings""" """Creates an :class:`argparse.ArgumentParser` from all cmdline settings"""
groups: dict[str, ArgParser] = {} groups: dict[str, ArgParser] = {}
argparser = argparse.ArgumentParser( argparser = argparse.ArgumentParser(
description=description, epilog=epilog, formatter_class=argparse.RawTextHelpFormatter, description=description, epilog=epilog, formatter_class=argparse.RawTextHelpFormatter,
prog=prog,
) )
def get_current_group(setting: Setting) -> ArgParser: def get_current_group(setting: Setting) -> ArgParser:
@ -798,6 +750,8 @@ def parse_cmdline(
epilog: str, epilog: str,
args: list[str] | None = None, args: list[str] | None = None,
config: ns[T] = None, config: ns[T] = None,
*,
prog: str | None = None,
) -> Config[Values]: ) -> Config[Values]:
""" """
Creates an `argparse.ArgumentParser` from cmdline settings in `self.definitions`. Creates an `argparse.ArgumentParser` from cmdline settings in `self.definitions`.
@ -819,7 +773,7 @@ def parse_cmdline(
else: else:
namespace = config namespace = config
argparser = create_argparser(definitions, description, epilog) argparser = create_argparser(definitions, description, epilog, prog=prog)
ns = argparser.parse_args(args, namespace=namespace) ns = argparser.parse_args(args, namespace=namespace)
return normalize_config(Config(ns, definitions), cmdline=True, file=True) return normalize_config(Config(ns, definitions), cmdline=True, file=True)
@ -856,11 +810,12 @@ def parse_config(
class Manager: class Manager:
"""docstring for Manager""" """docstring for Manager"""
def __init__(self, description: str = '', epilog: str = '', definitions: Definitions | Config[T] | None = None): def __init__(self, description: str = '', epilog: str = '', definitions: Definitions | Config[T] | None = None, *, prog: str | None = None):
# This one is never used, it just makes MyPy happy # This one is never used, it just makes MyPy happy
self.argparser = argparse.ArgumentParser(description=description, epilog=epilog) self.argparser = argparse.ArgumentParser(description=description, epilog=epilog)
self.description = description self.description = description
self.epilog = epilog self.epilog = epilog
self.prog = prog
self.definitions: Definitions self.definitions: Definitions
if isinstance(definitions, Config): if isinstance(definitions, Config):
@ -883,7 +838,7 @@ class Manager:
return generate_dict(self.definitions) return generate_dict(self.definitions)
def create_argparser(self) -> None: def create_argparser(self) -> None:
self.argparser = create_argparser(self.definitions, self.description, self.epilog) self.argparser = create_argparser(self.definitions, self.description, self.epilog, prog=self.prog)
def add_setting(self, *args: Any, **kwargs: Any) -> None: def add_setting(self, *args: Any, **kwargs: Any) -> None:
"""Passes all arguments through to `Setting`, `group` and `exclusive` are already set""" """Passes all arguments through to `Setting`, `group` and `exclusive` are already set"""
@ -1095,7 +1050,6 @@ def example_group(manager: Manager) -> None:
manager.add_setting( manager.add_setting(
'--verbose', '-v', '--verbose', '-v',
default=False, default=False,
metavar='nothing',
action=BooleanOptionalAction, # Added in Python 3.9 action=BooleanOptionalAction, # Added in Python 3.9
) )
@ -1108,6 +1062,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: def _main(args: list[str] | None = None) -> None:
settings_path = pathlib.Path('./settings.json') settings_path = pathlib.Path('./settings.json')
manager = Manager(description='This is an example', epilog='goodbye!') manager = Manager(description='This is an example', epilog='goodbye!')
@ -1115,11 +1095,11 @@ def _main(args: list[str] | None = None) -> None:
manager.add_group('Example Group', example_group) manager.add_group('Example Group', example_group)
manager.add_persistent_group('persistent', persistent_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) file_namespace = manager.get_namespace(file_config, file=True, cmdline=True)
merged_config = manager.parse_cmdline(args=args, config=file_namespace) merged_config = cast(Config[SettngsDict], manager.parse_cmdline(args=args, config=file_namespace))
merged_namespace = manager.get_namespace(merged_config, file=True, cmdline=True) 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 print(f'Hello {merged_config.values["Example Group"]["hello"]}') # noqa: T201
if merged_namespace.values.Example_Group__save: if merged_namespace.values.Example_Group__save:

View File

@ -9,7 +9,6 @@ author_email = timmy@narnian.us
license = MIT license = MIT
license_files = LICENSE license_files = LICENSE
classifiers = classifiers =
License :: OSI Approved :: MIT License
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: CPython
@ -19,7 +18,7 @@ classifiers =
packages = find: packages = find:
install_requires = install_requires =
typing-extensions>=4.3.0;python_version < '3.11' typing-extensions>=4.3.0;python_version < '3.11'
python_requires = >=3.8 python_requires = >=3.9
include_package_data = True include_package_data = True
[options.packages.find] [options.packages.find]
@ -31,7 +30,7 @@ exclude =
settngs = py.typed settngs = py.typed
[tox:tox] [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] [testenv]
deps = -rrequirements-dev.txt deps = -rrequirements-dev.txt

View File

@ -2,11 +2,13 @@ from __future__ import annotations
import argparse import argparse
import ast import ast
from enum import Enum, auto
import json import json
import pathlib import pathlib
import sys import sys
from collections import defaultdict from collections import defaultdict
from typing import Generator from typing import Generator
from typing import NamedTuple
import pytest import pytest
@ -29,19 +31,6 @@ positional arguments:
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
''' '''
elif sys.version_info < (3, 9): # pragma: no cover
from typing import List
from typing import Set
help_output = '''\
usage: __main__.py [-h] [TEST [TEST ...]]
positional arguments:
TEST
optional arguments:
-h, --help show this help message and exit
'''
else: # pragma: no cover else: # pragma: no cover
List = list List = list
Set = set Set = set
@ -126,12 +115,14 @@ def test_exclusive_group(settngs_manager):
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test2', default='hello'), exclusive_group=True) settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test2', default='hello'), exclusive_group=True)
settngs_manager.create_argparser() settngs_manager.create_argparser()
args = settngs_manager.argparser.parse_args(['--test', 'never', '--test2', 'never']) args = settngs_manager.argparser.parse_args(['__main__.py', '--test', 'never', '--test2', 'never'])
def test_files_group(capsys, settngs_manager): def test_files_group(capsys, settngs_manager):
settngs_manager.add_group('runtime', lambda parser: parser.add_setting('test', default='hello', nargs='*')) settngs_manager.add_group('runtime', lambda parser: parser.add_setting('test', default='hello', nargs='*'))
settngs_manager.prog = '__main__.py'
settngs_manager.create_argparser() settngs_manager.create_argparser()
settngs_manager.prog = '__main__.py'
settngs_manager.argparser.print_help() settngs_manager.argparser.print_help()
captured = capsys.readouterr() captured = capsys.readouterr()
assert captured.out == help_output assert captured.out == help_output
@ -139,7 +130,9 @@ def test_files_group(capsys, settngs_manager):
def test_setting_without_group(capsys, settngs_manager): def test_setting_without_group(capsys, settngs_manager):
settngs_manager.add_setting('test', default='hello', nargs='*') settngs_manager.add_setting('test', default='hello', nargs='*')
settngs_manager.prog = '__main__.py'
settngs_manager.create_argparser() settngs_manager.create_argparser()
settngs_manager.prog = '__main__.py'
settngs_manager.argparser.print_help() settngs_manager.argparser.print_help()
captured = capsys.readouterr() captured = capsys.readouterr()
assert captured.out == help_output assert captured.out == help_output
@ -258,7 +251,7 @@ class TestValues:
# manager_unknown.add_persistent_group('persistent', lambda parser: parser.add_setting('--world', default='world')) # manager_unknown.add_persistent_group('persistent', lambda parser: parser.add_setting('--world', default='world'))
defaults = manager.defaults() 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 defaults.values['persistent']['hello'] = 'success' # Group is not defined in manager_unknown, should stay
normalized, _ = manager_unknown.normalize_config(defaults.values, file=True) normalized, _ = manager_unknown.normalize_config(defaults.values, file=True)
@ -613,6 +606,10 @@ def test_adding_to_existing_persistent_group(settngs_manager: settngs.Manager, t
assert default_to_regular(settngs_manager.definitions) == default_to_regular(settngs_manager2.definitions) assert default_to_regular(settngs_manager.definitions) == default_to_regular(settngs_manager2.definitions)
class test_enum(Enum):
test = auto()
class test_type(int): class test_type(int):
... ...
@ -673,28 +670,31 @@ types = (
(6, settngs.Setting('-t', '--test', action='append'), List[str], True), (6, settngs.Setting('-t', '--test', action='append'), List[str], True),
(7, settngs.Setting('-t', '--test', action='extend'), List[str], True), (7, settngs.Setting('-t', '--test', action='extend'), List[str], True),
(8, settngs.Setting('-t', '--test', nargs='+'), List[str], True), (8, settngs.Setting('-t', '--test', nargs='+'), List[str], True),
(9, settngs.Setting('-t', '--test', action='store_const', const=1), int, True), (9, settngs.Setting('-t', '--test', nargs='+', type=pathlib.Path), List[pathlib.Path], True),
(10, settngs.Setting('-t', '--test', action='append_const', const=1), List[int], True), (10, settngs.Setting('-t', '--test', nargs='+', type=test_enum), List[test_enum], True),
(11, settngs.Setting('-t', '--test', action='store_true'), bool, False), (11, settngs.Setting('-t', '--test', action='store_const', const=1), int, True),
(12, settngs.Setting('-t', '--test', action='store_false'), bool, False), (12, settngs.Setting('-t', '--test', action='append_const', const=1), List[int], True),
(13, settngs.Setting('-t', '--test', action=settngs.BooleanOptionalAction), bool, True), (13, settngs.Setting('-t', '--test', action='store_true'), bool, False),
(14, settngs.Setting('-t', '--test', action=_customAction), 'Any', True), (14, settngs.Setting('-t', '--test', action='store_false'), bool, False),
(15, settngs.Setting('-t', '--test', action='help'), None, True), (15, settngs.Setting('-t', '--test', action=argparse.BooleanOptionalAction), bool, True),
(16, settngs.Setting('-t', '--test', action='version'), None, True), (16, settngs.Setting('-t', '--test', action=_customAction), 'Any', True),
(17, settngs.Setting('-t', '--test', type=int), int, True), (17, settngs.Setting('-t', '--test', type=test_enum), test_enum, True),
(18, settngs.Setting('-t', '--test', type=int, nargs='+'), List[int], True), (18, settngs.Setting('-t', '--test', type=int), int, True),
(19, settngs.Setting('-t', '--test', type=_typed_function), test_type, True), (19, settngs.Setting('-t', '--test', type=int, nargs='+'), List[int], True),
(20, settngs.Setting('-t', '--test', type=_untyped_function, default=1), int, False), (20, settngs.Setting('-t', '--test', type=_typed_function), test_type, True),
(21, settngs.Setting('-t', '--test', type=_untyped_function, default=[1]), List[int], False), (21, settngs.Setting('-t', '--test', type=_untyped_function, default=1), int, False),
(22, settngs.Setting('-t', '--test', type=_untyped_function), 'Any', True), (22, settngs.Setting('-t', '--test', type=_untyped_function, default=[1]), List[int], False),
(23, settngs.Setting('-t', '--test', type=_untyped_function, default={1}), Set[int], False), (23, settngs.Setting('-t', '--test', type=_untyped_function), 'Any', True),
(24, settngs.Setting('-t', '--test', action='append', type=int), List[int], True), (24, settngs.Setting('-t', '--test', type=_untyped_function, default={1}), Set[int], False),
(25, settngs.Setting('-t', '--test', action='extend', type=int, nargs=2), List[int], True), (25, settngs.Setting('-t', '--test', action='append', type=int), List[int], True),
(26, settngs.Setting('-t', '--test', action='append', type=int, nargs=2), List[List[int]], True), (26, settngs.Setting('-t', '--test', action='extend', type=int, nargs=2), List[int], True),
(27, settngs.Setting('-t', '--test', action='extend', nargs='+'), List[str], True), (27, settngs.Setting('-t', '--test', action='append', type=int, nargs=2), List[List[int]], True),
(28, settngs.Setting('-t', '--test', action='extend', type=_typed_list_generic_function), List[test_type], True), (28, settngs.Setting('-t', '--test', action='extend', nargs='+'), List[str], True),
(29, settngs.Setting('-t', '--test', action='extend', type=_typed_list_function), List, True), (29, settngs.Setting('-t', '--test', action='extend', type=_typed_list_generic_function), List[test_type], True),
(30, settngs.Setting('-t', '--test', action='extend', type=_typed_set_function), Set, True), (30, settngs.Setting('-t', '--test', action='extend', type=_typed_list_function), List, True),
(31, settngs.Setting('-t', '--test', action='extend', type=_typed_set_function), Set, True),
(32, settngs.Setting('-t', '--test', action='help'), None, True),
(33, settngs.Setting('-t', '--test', action='version'), None, True),
) )
@ -706,6 +706,11 @@ def test_guess_type(num, setting, typ, noneable_expected):
assert noneable == noneable_expected assert noneable == noneable_expected
class TypeResult(NamedTuple):
extra_imports: str
typ: str
expected_src = '''from __future__ import annotations expected_src = '''from __future__ import annotations
import settngs import settngs
@ -714,46 +719,40 @@ import settngs
class SettngsNS(settngs.TypedNS): class SettngsNS(settngs.TypedNS):
test__test: {typ} test__test: {typ}
''' '''
no_type_expected_src = '''from __future__ import annotations
import settngs
class SettngsNS(settngs.TypedNS):
...
'''
settings = ( settings = (
(0, lambda parser: parser.add_setting('-t', '--test'), expected_src.format(extra_imports='', typ='str | None')), (0, lambda parser: parser.add_setting('-t', '--test'), TypeResult(extra_imports='', typ='str | None')),
(1, lambda parser: parser.add_setting('-t', '--test', cmdline=False), expected_src.format(extra_imports='import typing\n', typ='typing.Any')), (1, lambda parser: parser.add_setting('-t', '--test', cmdline=False), TypeResult(extra_imports='import typing\n', typ='typing.Any')),
(2, lambda parser: parser.add_setting('-t', '--test', default=1, file=True, cmdline=False), expected_src.format(extra_imports='', typ='int')), (2, lambda parser: parser.add_setting('-t', '--test', default=1, file=True, cmdline=False), TypeResult(extra_imports='', typ='int')),
(3, lambda parser: parser.add_setting('-t', '--test', default='test'), expected_src.format(extra_imports='', typ='str')), (3, lambda parser: parser.add_setting('-t', '--test', default='test'), TypeResult(extra_imports='', typ='str')),
(4, lambda parser: parser.add_setting('-t', '--test', default='test', file=True, cmdline=False), expected_src.format(extra_imports='', typ='str')), (4, lambda parser: parser.add_setting('-t', '--test', default='test', file=True, cmdline=False), TypeResult(extra_imports='', typ='str')),
(5, lambda parser: parser.add_setting('-t', '--test', action='count'), expected_src.format(extra_imports='', typ='int | None')), (5, lambda parser: parser.add_setting('-t', '--test', action='count'), TypeResult(extra_imports='', typ='int | None')),
(6, lambda parser: parser.add_setting('-t', '--test', action='append'), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[str]} | None')), (6, lambda parser: parser.add_setting('-t', '--test', action='append'), TypeResult(extra_imports='', typ=f'{List[str]} | None')),
(7, lambda parser: parser.add_setting('-t', '--test', action='extend'), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[str]} | None')), (7, lambda parser: parser.add_setting('-t', '--test', action='extend'), TypeResult(extra_imports='', typ=f'{List[str]} | None')),
(8, lambda parser: parser.add_setting('-t', '--test', nargs='+'), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[str]} | None')), (8, lambda parser: parser.add_setting('-t', '--test', nargs='+'), TypeResult(extra_imports='', typ=f'{List[str]} | None')),
(9, lambda parser: parser.add_setting('-t', '--test', action='store_const', const=1), expected_src.format(extra_imports='', typ='int | None')), (9, lambda parser: parser.add_setting('-t', '--test', nargs='+', type=pathlib.Path), TypeResult(extra_imports='import pathlib._local\n' if sys.version_info[:2] == (3, 13) else 'import pathlib\n', typ=f'{List[pathlib.Path]} | None')),
(10, lambda parser: parser.add_setting('-t', '--test', action='append_const', const=1), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[int]} | None')), (10, lambda parser: parser.add_setting('-t', '--test', nargs='+', type=test_enum), TypeResult(extra_imports='import tests.settngs_test\n', typ=f'{List[test_enum]} | None')),
(11, lambda parser: parser.add_setting('-t', '--test', action='store_true'), expected_src.format(extra_imports='', typ='bool')), (11, lambda parser: parser.add_setting('-t', '--test', action='store_const', const=1), TypeResult(extra_imports='', typ='int | None')),
(12, lambda parser: parser.add_setting('-t', '--test', action='store_false'), expected_src.format(extra_imports='', typ='bool')), (12, lambda parser: parser.add_setting('-t', '--test', action='append_const', const=1), TypeResult(extra_imports='', typ=f'{List[int]} | None')),
(13, lambda parser: parser.add_setting('-t', '--test', action=settngs.BooleanOptionalAction), expected_src.format(extra_imports='', typ='bool | None')), (13, lambda parser: parser.add_setting('-t', '--test', action='store_true'), TypeResult(extra_imports='', typ='bool')),
(14, lambda parser: parser.add_setting('-t', '--test', action=_customAction), expected_src.format(extra_imports='import typing\n', typ='typing.Any')), (14, lambda parser: parser.add_setting('-t', '--test', action='store_false'), TypeResult(extra_imports='', typ='bool')),
(15, lambda parser: parser.add_setting('-t', '--test', action='help'), no_type_expected_src), (15, lambda parser: parser.add_setting('-t', '--test', action=argparse.BooleanOptionalAction), TypeResult(extra_imports='', typ='bool | None')),
(16, lambda parser: parser.add_setting('-t', '--test', action='version'), no_type_expected_src), (16, lambda parser: parser.add_setting('-t', '--test', action=_customAction), TypeResult(extra_imports='import typing\n', typ='typing.Any')),
(17, lambda parser: parser.add_setting('-t', '--test', type=int), expected_src.format(extra_imports='', typ='int | None')), (17, lambda parser: parser.add_setting('-t', '--test', type=test_enum), TypeResult(extra_imports='import tests.settngs_test\n', typ='tests.settngs_test.test_enum | None')),
(18, lambda parser: parser.add_setting('-t', '--test', type=int, nargs='+'), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[int]} | None')), (18, lambda parser: parser.add_setting('-t', '--test', type=int), TypeResult(extra_imports='', typ='int | None')),
(19, lambda parser: parser.add_setting('-t', '--test', type=_typed_function), expected_src.format(extra_imports='import tests.settngs_test\n', typ='tests.settngs_test.test_type | None')), (19, lambda parser: parser.add_setting('-t', '--test', type=int, nargs='+'), TypeResult(extra_imports='', typ=f'{List[int]} | None')),
(20, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default=1), expected_src.format(extra_imports='', typ='int')), (20, lambda parser: parser.add_setting('-t', '--test', type=_typed_function), TypeResult(extra_imports='import tests.settngs_test\n', typ='tests.settngs_test.test_type | None')),
(21, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default=[1]), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[int]}')), (21, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default=1), TypeResult(extra_imports='', typ='int')),
(22, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function), expected_src.format(extra_imports='import typing\n', typ='typing.Any')), (22, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default=[1]), TypeResult(extra_imports='', typ=f'{List[int]}')),
(23, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default={1}), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{Set[int]}')), (23, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function), TypeResult(extra_imports='import typing\n', typ='typing.Any')),
(24, lambda parser: parser.add_setting('-t', '--test', action='append', type=int), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[int]} | None')), (24, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default={1}), TypeResult(extra_imports='', typ=f'{Set[int]}')),
(25, lambda parser: parser.add_setting('-t', '--test', action='extend', type=int, nargs=2), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[int]} | None')), (25, lambda parser: parser.add_setting('-t', '--test', action='append', type=int), TypeResult(extra_imports='', typ=f'{List[int]} | None')),
(26, lambda parser: parser.add_setting('-t', '--test', action='append', type=int, nargs=2), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[List[int]]} | None')), (26, lambda parser: parser.add_setting('-t', '--test', action='extend', type=int, nargs=2), TypeResult(extra_imports='', typ=f'{List[int]} | None')),
(27, lambda parser: parser.add_setting('-t', '--test', action='extend', nargs='+'), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[str]} | None')), (27, lambda parser: parser.add_setting('-t', '--test', action='append', type=int, nargs=2), TypeResult(extra_imports='', typ=f'{List[List[int]]} | None')),
(28, lambda parser: parser.add_setting('-t', '--test', action='extend', type=_typed_list_generic_function), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ=f'{List[test_type]} | None')), (28, lambda parser: parser.add_setting('-t', '--test', action='extend', nargs='+'), TypeResult(extra_imports='', typ=f'{List[str]} | None')),
(29, lambda parser: parser.add_setting('-t', '--test', action='extend', type=_typed_list_function), expected_src.format(extra_imports='', typ=f'{settngs._type_to_string(List)[0]} | None')), (29, lambda parser: parser.add_setting('-t', '--test', action='extend', type=_typed_list_generic_function), TypeResult(extra_imports='import tests.settngs_test\n', typ=f'{List[test_type]} | None')),
(30, lambda parser: parser.add_setting('-t', '--test', action='extend', type=_typed_set_function), expected_src.format(extra_imports='', typ=f'{settngs._type_to_string(Set)[0]} | None')), (30, lambda parser: parser.add_setting('-t', '--test', action='extend', type=_typed_list_function), TypeResult(extra_imports='', typ=f'{settngs._type_to_string(List)[0]} | None')),
(31, lambda parser: parser.add_setting('-t', '--test', action='extend', type=_typed_set_function), TypeResult(extra_imports='', typ=f'{settngs._type_to_string(Set)[0]} | None')),
) )
@ -764,14 +763,13 @@ def test_generate_ns(settngs_manager, num, set_options, expected):
imports, types = settngs_manager.generate_ns() imports, types = settngs_manager.generate_ns()
generated_src = '\n\n\n'.join((imports, types)) generated_src = '\n\n\n'.join((imports, types))
assert generated_src == expected assert generated_src == expected_src.format(**expected._asdict())
ast.parse(generated_src) ast.parse(generated_src)
expected_src_dict = '''from __future__ import annotations expected_src_dict = '''from __future__ import annotations
import typing
{extra_imports} {extra_imports}
class test(typing.TypedDict): class test(typing.TypedDict):
@ -781,61 +779,18 @@ class test(typing.TypedDict):
class SettngsDict(typing.TypedDict): class SettngsDict(typing.TypedDict):
test: test test: test
''' '''
no_type_expected_src_dict = '''from __future__ import annotations
import typing
class test(typing.TypedDict): @pytest.mark.parametrize('num,set_options,expected', settings)
...
class SettngsDict(typing.TypedDict):
test: test
'''
settings_dict = (
(0, lambda parser: parser.add_setting('-t', '--test'), expected_src_dict.format(extra_imports='', typ='str | None')),
(1, lambda parser: parser.add_setting('-t', '--test', cmdline=False), expected_src_dict.format(extra_imports='', typ='typing.Any')),
(2, lambda parser: parser.add_setting('-t', '--test', default=1, file=True, cmdline=False), expected_src_dict.format(extra_imports='', typ='int')),
(3, lambda parser: parser.add_setting('-t', '--test', default='test'), expected_src_dict.format(extra_imports='', typ='str')),
(4, lambda parser: parser.add_setting('-t', '--test', default='test', file=True, cmdline=False), expected_src_dict.format(extra_imports='', typ='str')),
(5, lambda parser: parser.add_setting('-t', '--test', action='count'), expected_src_dict.format(extra_imports='', typ='int | None')),
(6, lambda parser: parser.add_setting('-t', '--test', action='append'), expected_src_dict.format(extra_imports='', typ=f'{List[str]} | None')),
(7, lambda parser: parser.add_setting('-t', '--test', action='extend'), expected_src_dict.format(extra_imports='', typ=f'{List[str]} | None')),
(8, lambda parser: parser.add_setting('-t', '--test', nargs='+'), expected_src_dict.format(extra_imports='', typ=f'{List[str]} | None')),
(9, lambda parser: parser.add_setting('-t', '--test', action='store_const', const=1), expected_src_dict.format(extra_imports='', typ='int | None')),
(10, lambda parser: parser.add_setting('-t', '--test', action='append_const', const=1), expected_src_dict.format(extra_imports='', typ=f'{List[int]} | None')),
(11, lambda parser: parser.add_setting('-t', '--test', action='store_true'), expected_src_dict.format(extra_imports='', typ='bool')),
(12, lambda parser: parser.add_setting('-t', '--test', action='store_false'), expected_src_dict.format(extra_imports='', typ='bool')),
(13, lambda parser: parser.add_setting('-t', '--test', action=settngs.BooleanOptionalAction), expected_src_dict.format(extra_imports='', typ='bool | None')),
(14, lambda parser: parser.add_setting('-t', '--test', action=_customAction), expected_src_dict.format(extra_imports='', typ='typing.Any')),
(15, lambda parser: parser.add_setting('-t', '--test', action='help'), no_type_expected_src_dict),
(16, lambda parser: parser.add_setting('-t', '--test', action='version'), no_type_expected_src_dict),
(17, lambda parser: parser.add_setting('-t', '--test', type=int), expected_src_dict.format(extra_imports='', typ='int | None')),
(18, lambda parser: parser.add_setting('-t', '--test', type=int, nargs='+'), expected_src_dict.format(extra_imports='', typ=f'{List[int]} | None')),
(19, lambda parser: parser.add_setting('-t', '--test', type=_typed_function), expected_src_dict.format(extra_imports='import tests.settngs_test\n', typ=f'{test_type.__module__}.{test_type.__name__} | None')),
(20, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default=1), expected_src_dict.format(extra_imports='', typ='int')),
(21, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default=[1]), expected_src_dict.format(extra_imports='', typ=f'{List[int]}')),
(22, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function), expected_src_dict.format(extra_imports='', typ='typing.Any')),
(23, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default={1}), expected_src_dict.format(extra_imports='', typ=f'{Set[int]}')),
(24, lambda parser: parser.add_setting('-t', '--test', action='append', type=int), expected_src_dict.format(extra_imports='', typ=f'{List[int]} | None')),
(25, lambda parser: parser.add_setting('-t', '--test', action='extend', type=int, nargs=2), expected_src_dict.format(extra_imports='', typ=f'{List[int]} | None')),
(26, lambda parser: parser.add_setting('-t', '--test', action='append', type=int, nargs=2), expected_src_dict.format(extra_imports='', typ=f'{List[List[int]]} | None')),
(27, lambda parser: parser.add_setting('-t', '--test', action='extend', nargs='+'), expected_src_dict.format(extra_imports='', typ=f'{List[str]} | None')),
(28, lambda parser: parser.add_setting('-t', '--test', action='extend', type=_typed_list_generic_function), expected_src_dict.format(extra_imports='', typ=f'{List[test_type]} | None')),
(29, lambda parser: parser.add_setting('-t', '--test', action='extend', type=_typed_list_function), expected_src_dict.format(extra_imports='', typ=f'{settngs._type_to_string(List)[0]} | None')),
(30, lambda parser: parser.add_setting('-t', '--test', action='extend', type=_typed_set_function), expected_src_dict.format(extra_imports='', typ=f'{settngs._type_to_string(Set)[0]} | None')),
)
@pytest.mark.parametrize('num,set_options,expected', settings_dict)
def test_generate_dict(settngs_manager, num, set_options, expected): def test_generate_dict(settngs_manager, num, set_options, expected):
settngs_manager.add_group('test', set_options) settngs_manager.add_group('test', set_options)
imports, types = settngs_manager.generate_dict() imports, types = settngs_manager.generate_dict()
generated_src = '\n\n\n'.join((imports, types)) generated_src = '\n\n\n'.join((imports, types))
assert generated_src == expected if 'import typing' not in expected.extra_imports:
expected = TypeResult('import typing\n' + expected.extra_imports, expected.typ)
assert generated_src == expected_src_dict.format(**expected._asdict())
ast.parse(generated_src) ast.parse(generated_src)