22 Commits
0.9.1 ... 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
73552e0dd2 Improve Generic list and set typing 2024-05-19 14:11:18 -07:00
58daa4b274 Fix deduplication 2024-05-18 16:46:44 -07:00
43f6bf1eac Improve type detection 2024-05-18 15:49:32 -07:00
eca7be0c51 Set optional None types 2024-05-18 13:03:50 -07:00
709aec31f0 Merge branch 'pre-commit-ci-update-config' 2024-04-29 19:12:57 -07:00
a3eb2f8e31 Fix metavar deprecation on BooleanOptionAction 2024-04-29 19:12:48 -07:00
20dd942784 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0)
- [github.com/pre-commit/mirrors-mypy: v1.9.0 → v1.10.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.9.0...v1.10.0)
2024-04-29 17:23:54 +00:00
3f9cfbb8b4 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.15.1 → v3.15.2](https://github.com/asottile/pyupgrade/compare/v3.15.1...v3.15.2)
- [github.com/hhatto/autopep8: v2.0.4 → v2.1.0](https://github.com/hhatto/autopep8/compare/v2.0.4...v2.1.0)
- [github.com/pre-commit/mirrors-mypy: v1.8.0 → v1.9.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.8.0...v1.9.0)
2024-03-25 17:17:17 +00:00
c588fc891e Support type generation for dicts an addition to namespaces 2024-02-22 19:15:47 -08:00
1ce6079285 Fix pre-commit 2024-02-22 19:14:45 -08:00
7c748f6815 Merge branch 'pre-commit-ci-update-config' 2024-02-22 14:44:51 -08:00
8d5b30546e Improve type guessing for generic Sequence types 2024-02-22 14:42:07 -08:00
cebca481fc [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.15.0 → v3.15.1](https://github.com/asottile/pyupgrade/compare/v3.15.0...v3.15.1)
2024-02-19 17:21:12 +00:00
dd8cd1188e [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/PyCQA/flake8: 6.1.0 → 7.0.0](https://github.com/PyCQA/flake8/compare/6.1.0...7.0.0)
- [github.com/pre-commit/mirrors-mypy: v1.7.0 → v1.8.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.0...v1.8.0)
2024-01-08 17:17:39 +00:00
d30e73a679 Do not add duplicate settings when generating a namespace 2023-12-17 18:26:57 -08:00
5 changed files with 465 additions and 215 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.5.0 rev: v4.6.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
@ -14,7 +14,7 @@ repos:
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.13.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']
@ -28,20 +28,21 @@ repos:
hooks: hooks:
- id: dead - id: dead
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.15.0 rev: v3.16.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py38-plus] args: [--py38-plus]
exclude: tests
- repo: https://github.com/hhatto/autopep8 - repo: https://github.com/hhatto/autopep8
rev: v2.0.4 rev: v2.3.1
hooks: hooks:
- id: autopep8 - id: autopep8
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 6.1.0 rev: 7.1.0
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [flake8-encodings, flake8-warnings, flake8-builtins, flake8-length, 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.7.0 rev: v1.10.0
hooks: hooks:
- id: mypy - id: mypy

View File

@ -2,86 +2,57 @@ from __future__ import annotations
import argparse import argparse
import copy import copy
import inspect
import json import json
import logging import logging
import pathlib import pathlib
import re import re
import sys import sys
import typing import typing
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 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 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 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
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
NoneType = type(None)
if sys.version_info < (3, 11): # pragma: no cover if sys.version_info < (3, 11): # pragma: no cover
from typing_extensions import NamedTuple from typing_extensions import NamedTuple
else: # pragma: no cover else: # pragma: no cover
from typing import NamedTuple from typing import NamedTuple
if sys.version_info < (3, 9): # pragma: no cover def _isnamedtupleinstance(x: Any) -> bool: # pragma: no cover
from typing import List t = type(x)
from typing import _GenericAlias as types_GenericAlias b = t.__bases__
def removeprefix(self: str, prefix: str, /) -> str: if len(b) != 1 or b[0] != tuple:
if self.startswith(prefix): return False
return self[len(prefix):]
else:
return self[:]
class BooleanOptionalAction(argparse.Action): f = getattr(t, '_fields', None)
def __init__( if not isinstance(f, tuple):
self, return False
option_strings,
dest,
default=None,
type=None, # noqa: A002
choices=None,
required=False,
help=None, # noqa: A002
metavar=None,
):
_option_strings = [] return all(isinstance(n, str) for n in f)
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): # 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
class Setting: class Setting:
@ -137,12 +108,13 @@ class Setting:
raise ValueError('names must be specified') raise ValueError('names must be specified')
# We prefix the destination name used by argparse so that there are no conflicts # We prefix the destination name used by argparse so that there are no conflicts
# Argument names will still cause an exception if there is a conflict e.g. if '-f' is defined twice # Argument names will still cause an exception if there is a conflict e.g. if '-f' is defined twice
self.internal_name, setting_name, dest, self.flag = self.get_dest(group, names, dest) self.internal_name, self.setting_name, dest, self.flag = self.get_dest(group, names, dest)
args: Sequence[str] = names args: Sequence[str] = names
# We then also set the metavar so that '--config' in the group runtime shows as 'CONFIG' instead of 'RUNTIME_CONFIG' # We then also set the metavar so that '--config' in the group runtime shows as 'CONFIG' instead of 'RUNTIME_CONFIG'
if not metavar and action not in ('store_true', 'store_false', 'count'): if not metavar and action not in ('store_true', 'store_false', 'count', 'help', 'version'):
metavar = dest.upper() if not callable(action) or 'metavar' in inspect.signature(action).parameters.keys():
metavar = dest.upper()
# If we are not a flag, no '--' or '-' in front # If we are not a flag, no '--' or '-' in front
# we use internal_name as argparse sets dest to args[0] # we use internal_name as argparse sets dest to args[0]
@ -159,7 +131,6 @@ class Setting:
self.help = help self.help = help
self.metavar = metavar self.metavar = metavar
self.dest = dest self.dest = dest
self.setting_name = setting_name
self.cmdline = cmdline self.cmdline = cmdline
self.file = file self.file = file
self.argparse_args = args self.argparse_args = args
@ -191,47 +162,114 @@ class Setting:
return NotImplemented return NotImplemented
return self.__dict__ == other.__dict__ return self.__dict__ == other.__dict__
def _guess_type(self) -> type | str | None: __no_type = object()
if self.type is None and self.action is None:
if self.cmdline:
if self.nargs in ('+', '*') or isinstance(self.nargs, int) and self.nargs > 1:
return List[str]
return str
else:
if not self.cmdline and self.default is not None:
return type(self.default)
return 'Any'
def _guess_collection(self) -> tuple[type | str | None, bool]:
def get_item_type(x: Any) -> type | None:
if x is None or not isinstance(x, (Set, Sequence)) or len(x) == 0:
t = self._process_type() # Specifically this is needed when using the extend action
if typing.get_args(t): # We need the item type not the type of the collection
return typing.get_args(t)[0] # type: ignore[no-any-return]
# Return None so that we get the default
return None if t is None else self.__no_type # type: ignore[return-value]
if isinstance(x, Set):
return type(next(iter(x)))
return type(x[0])
try:
list_type = self._process_type()
# if the type is a generic alias than return it immediately
if isinstance(list_type, types_GenericAlias) and issubclass(list_type.__origin__, Collection):
return list_type, self.default is None
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) or issubclass(list_type, Enum):
list_type = list
# Get the item type (int) in list[int]
it = get_item_type(self.default)
if isinstance(self.type, type):
it = self.type
if it is self.__no_type:
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:
try:
ret = cast(type, list_type[it]), self.default is None # type: ignore[index]
return ret
except Exception:
...
# Fall back to list[str] if anything fails
return list_type[str], self.default is None # type: ignore[index]
except Exception:
return None, self.default is None
def _process_type(self) -> type | None:
if self.type is None:
return None
if isinstance(self.type, type): if isinstance(self.type, type):
return self.type return self.type
if self.type is not None: return typing.get_type_hints(self.type).get('return', None) # type: ignore[no-any-return]
type_hints = typing.get_type_hints(self.type)
if 'return' in type_hints:
t: type | str = type_hints['return']
return t
if self.default is not None:
return type(self.default)
return 'Any'
if self.action in ('store_true', 'store_false', BooleanOptionalAction): def _guess_type_internal(self) -> tuple[type | str | None, bool]:
return bool default_is_none = self.default is None
__action_to_type = {
'store_true': (bool, False),
'store_false': (bool, False),
BooleanOptionalAction: (bool, default_is_none),
'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]
'help': (None, default_is_none),
'version': (None, default_is_none),
}
if self.action in ('store_const',): # Process standard actions
return type(self.const) if self.action in __action_to_type:
return __action_to_type[self.action]
if self.action in ('count',): # nargs > 1 is always a list
return int if self.nargs in ('+', '*') or isinstance(self.nargs, int) and self.nargs > 1:
return self._guess_collection()
if self.action in ('append', 'extend'): # Process the type argument
return List[str] type_type = self._process_type()
if type_type is not None:
return type_type, default_is_none
if self.action in ('append_const',): # Check if a default value was given.
return list # list[type(self.const)] if self.default is not None:
if not isinstance(self.default, str) and not _isnamedtupleinstance(self.default) and isinstance(self.default, (Set, Sequence)):
return self._guess_collection()
# The type argument will convert this if it is a string. We only get here if type is a function without type hints
if not (isinstance(self.default, str) and self.type is not None):
return type(self.default), default_is_none
if self.action in ('help', 'version'): # There is no way to detemine the type from an action
return None if callable(self.action):
return 'Any' return 'Any', default_is_none
# Finally if this is a commandline argument it will default to a string
if self.cmdline and self.type is None:
return str, default_is_none
# For file only settings it will default to Any
return 'Any', default_is_none
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 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]:
setting_name = None setting_name = None
@ -265,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')
@ -275,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])
@ -288,48 +327,62 @@ 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 generate_ns(definitions: Definitions) -> str: def _type_to_string(t: type | str) -> tuple[str, str]:
initial_imports = ['from __future__ import annotations', '', 'import settngs', ''] type_name = 'Any'
import_needed = ''
# Take a string as is
if isinstance(t, str):
type_name = t
# Handle generic aliases eg dict[str, str] instead of dict
elif isinstance(t, types_GenericAlias):
if not get_args(t):
t = t.__origin__.__name__
type_name = str(t)
# Handle standard type objects
elif isinstance(t, type):
type_name = t.__name__
# 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
if type_name == 'Any':
type_name = 'typing.Any'
return type_name, import_needed
def generate_ns(definitions: Definitions) -> tuple[str, str]:
initial_imports = ['from __future__ import annotations', '', 'import settngs']
imports: Sequence[str] | set[str] imports: Sequence[str] | set[str]
imports = set() imports = set()
attributes = [] attributes = []
used_attributes: set[str] = set()
for group in definitions.values(): for group in definitions.values():
for setting in group.v.values(): for setting in group.v.values():
t = setting._guess_type() t, noneable = setting._guess_type()
if t is None: if t is None:
continue continue
# Default to any type_name, import_needed = _type_to_string(t)
type_name = 'Any' imports.add(import_needed)
# Take a string as is if noneable and type_name not in ('typing.Any', 'None'):
if isinstance(t, str): attribute = f' {setting.internal_name}: {type_name} | None'
type_name = t else:
# Handle generic aliases eg dict[str, str] instead of dict attribute = f' {setting.internal_name}: {type_name}'
elif isinstance(t, types_GenericAlias): if setting.internal_name not in used_attributes:
type_name = str(t) used_attributes.add(setting.internal_name)
# Handle standard type objects attributes.append(attribute)
elif isinstance(t, type):
type_name = t.__name__
# Builtin types don't need an import
if t.__module__ != 'builtins':
imports.add(f'import {t.__module__}')
# Use the full imported name
type_name = t.__module__ + '.' + type_name
# Expand Any to typing.Any
if type_name == 'Any':
type_name = 'typing.Any'
attributes.append(f' {setting.internal_name}: {type_name}')
# Add a blank line between groups # Add a blank line between groups
if attributes and attributes[-1] != '': if attributes and attributes[-1] != '':
attributes.append('') attributes.append('')
ns = 'class settngs_namespace(settngs.TypedNS):\n' ns = 'class SettngsNS(settngs.TypedNS):\n'
# Add a '...' expression if there are no attributes # Add a '...' expression if there are no attributes
if not attributes or all(x == '' for x in attributes): if not attributes or all(x == '' for x in attributes):
ns += ' ...\n' ns += ' ...\n'
@ -340,10 +393,58 @@ def generate_ns(definitions: Definitions) -> str:
initial_imports.append('import typing') initial_imports.append('import typing')
# Remove the possible duplicate typing import # Remove the possible duplicate typing import
imports = sorted(list(imports - {'import typing'})) imports = sorted(imports - {'import typing', ''})
# Merge the imports the ns class definition and the attributes # Merge the imports the ns class definition and the attributes
return '\n'.join(initial_imports + imports) + '\n\n\n' + ns + '\n'.join(attributes) return '\n'.join(initial_imports + imports), ns + '\n'.join(attributes)
def generate_dict(definitions: Definitions) -> tuple[str, str]:
initial_imports = ['from __future__ import annotations', '', 'import typing']
imports: Sequence[str] | set[str]
imports = set()
groups_are_identifiers = all(n.isidentifier() for n in definitions.keys())
classes = []
for group_name, group in definitions.items():
attributes = []
used_attributes: set[str] = set()
for setting in group.v.values():
t, no_default = setting._guess_type()
if t is None:
continue
type_name, import_needed = _type_to_string(t)
imports.add(import_needed)
if no_default and type_name not in ('typing.Any', 'None'):
attribute = f' {setting.dest}: {type_name} | None'
else:
attribute = f' {setting.dest}: {type_name}'
if setting.dest not in used_attributes:
used_attributes.add(setting.dest)
attributes.append(attribute)
if not attributes or all(x == '' for x in attributes):
attributes = [' ...']
classes.append(
f'class {sanitize_name(group_name)}(typing.TypedDict):\n'
+ '\n'.join(attributes) + '\n\n',
)
# Remove the possible duplicate typing import
imports = sorted(list(imports - {'import typing', ''}))
if groups_are_identifiers:
ns = '\nclass SettngsDict(typing.TypedDict):\n'
ns += '\n'.join(f' {n}: {sanitize_name(n)}' for n in definitions.keys())
else:
ns = '\nSettngsDict = typing.TypedDict(\n'
ns += " 'SettngsDict', {\n"
for n in definitions.keys():
ns += f' {n!r}: {sanitize_name(n)},\n'
ns += ' },\n'
ns += ')\n'
# Merge the imports the ns class definition and the attributes
return '\n'.join(initial_imports + imports), '\n'.join(classes) + ns + '\n'
def sanitize_name(name: str) -> str: def sanitize_name(name: str) -> str:
@ -384,7 +485,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
@ -435,7 +536,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():
@ -503,7 +604,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]
@ -597,26 +698,35 @@ def create_argparser(definitions: Definitions, description: str, epilog: str) ->
argparser = argparse.ArgumentParser( argparser = argparse.ArgumentParser(
description=description, epilog=epilog, formatter_class=argparse.RawTextHelpFormatter, description=description, epilog=epilog, formatter_class=argparse.RawTextHelpFormatter,
) )
for group in definitions.values():
for setting in group.v.values():
if setting.cmdline:
argparse_args, argparse_kwargs = setting.to_argparse()
current_group: ArgParser = argparser
if setting.group:
if setting.group not in groups:
if setting.exclusive:
groups[setting.group] = argparser.add_argument_group(
setting.group,
).add_mutually_exclusive_group()
else:
groups[setting.group] = argparser.add_argument_group(setting.group)
# Hard coded exception for positional arguments def get_current_group(setting: Setting) -> ArgParser:
# Ensures that the option shows at the top of the help output
if 'runtime' in setting.group.casefold() and setting.nargs == '*' and not setting.flag: if not setting.group:
current_group = argparser return argparser
else:
current_group = groups[setting.group] # Hard coded exception for positional arguments
# Ensures that the option shows at the top of the help output
if 'runtime' in setting.group.casefold() and setting.nargs == '*' and not setting.flag:
return argparser
if setting.group not in groups:
if setting.exclusive:
groups[setting.group] = argparser.add_argument_group(
setting.group,
).add_mutually_exclusive_group()
else:
groups[setting.group] = argparser.add_argument_group(setting.group)
return groups[setting.group]
with warnings.catch_warnings():
warnings.filterwarnings('ignore', message="'metavar", category=DeprecationWarning, module='argparse')
for group in definitions.values():
for setting in group.v.values():
if not setting.cmdline:
continue
argparse_args, argparse_kwargs = setting.to_argparse()
current_group: ArgParser = get_current_group(setting)
current_group.add_argument(*argparse_args, **argparse_kwargs) current_group.add_argument(*argparse_args, **argparse_kwargs)
return argparser return argparser
@ -705,9 +815,12 @@ class Manager:
return Config(c, self.definitions) return Config(c, self.definitions)
return c return c
def generate_ns(self) -> str: def generate_ns(self) -> tuple[str, str]:
return generate_ns(self.definitions) return generate_ns(self.definitions)
def generate_dict(self) -> tuple[str, str]:
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)
@ -921,6 +1034,7 @@ 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
) )
@ -933,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: 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!')
@ -940,11 +1080,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

@ -19,7 +19,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 +31,7 @@ exclude =
settngs = py.typed settngs = py.typed
[tox:tox] [tox:tox]
envlist = py3.8,py3.9,py3.10,py3.11,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

@ -6,7 +6,6 @@ import json
import pathlib import pathlib
import sys import sys
from collections import defaultdict from collections import defaultdict
from textwrap import dedent
from typing import Generator from typing import Generator
import pytest import pytest
@ -20,6 +19,7 @@ from testing.settngs import success
if sys.version_info >= (3, 10): # pragma: no cover if sys.version_info >= (3, 10): # pragma: no cover
List = list List = list
Set = set
help_output = '''\ help_output = '''\
usage: __main__.py [-h] [TEST ...] usage: __main__.py [-h] [TEST ...]
@ -31,6 +31,7 @@ options:
''' '''
elif sys.version_info < (3, 9): # pragma: no cover elif sys.version_info < (3, 9): # pragma: no cover
from typing import List from typing import List
from typing import Set
help_output = '''\ help_output = '''\
usage: __main__.py [-h] [TEST [TEST ...]] usage: __main__.py [-h] [TEST [TEST ...]]
@ -43,6 +44,7 @@ optional arguments:
else: # pragma: no cover else: # pragma: no cover
List = list List = list
Set = set
help_output = '''\ help_output = '''\
usage: __main__.py [-h] [TEST ...] usage: __main__.py [-h] [TEST ...]
@ -256,7 +258,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)
@ -619,6 +621,18 @@ def _typed_function(something: str) -> test_type: # pragma: no cover
return test_type() return test_type()
def _typed_list_generic_function(something: test_type) -> List[test_type]: # pragma: no cover
return [test_type()]
def _typed_list_function() -> List: # type: ignore[type-arg] # pragma: no cover
return []
def _typed_set_function() -> Set: # type: ignore[type-arg] # pragma: no cover
return set()
def _untyped_function(something): def _untyped_function(something):
... ...
@ -645,88 +659,183 @@ class _customAction(argparse.Action): # pragma: no cover
help=help, help=help,
) )
def __call__(self, parser, namespace, values, option_string=None): def __call__(self, parser, namespace, values, option_string=None): # pragma: no cover
setattr(namespace, self.dest, 'Something') setattr(namespace, self.dest, 'Something')
types = ( types = (
(settngs.Setting('-t', '--test'), str), (0, settngs.Setting('-t', '--test'), str, True),
(settngs.Setting('-t', '--test', cmdline=False), 'Any'), (1, settngs.Setting('-t', '--test', cmdline=False), 'Any', True),
(settngs.Setting('-t', '--test', default=1, file=True, cmdline=False), int), (2, settngs.Setting('-t', '--test', default=1, file=True, cmdline=False), int, False),
(settngs.Setting('-t', '--test', action='count'), int), (3, settngs.Setting('-t', '--test', default='test'), str, False),
(settngs.Setting('-t', '--test', action='append'), List[str]), (4, settngs.Setting('-t', '--test', default='test', file=True, cmdline=False), str, False),
(settngs.Setting('-t', '--test', action='extend'), List[str]), (5, settngs.Setting('-t', '--test', action='count'), int, True),
(settngs.Setting('-t', '--test', action='store_const', const=1), int), (6, settngs.Setting('-t', '--test', action='append'), List[str], True),
(settngs.Setting('-t', '--test', action='append_const', const=1), list), (7, settngs.Setting('-t', '--test', action='extend'), List[str], True),
(settngs.Setting('-t', '--test', action='store_true'), bool), (8, settngs.Setting('-t', '--test', nargs='+'), List[str], True),
(settngs.Setting('-t', '--test', action='store_false'), bool), (9, settngs.Setting('-t', '--test', action='store_const', const=1), int, True),
(settngs.Setting('-t', '--test', action=settngs.BooleanOptionalAction), bool), (10, settngs.Setting('-t', '--test', action='append_const', const=1), List[int], True),
(settngs.Setting('-t', '--test', action=_customAction), 'Any'), (11, settngs.Setting('-t', '--test', action='store_true'), bool, False),
(settngs.Setting('-t', '--test', action='help'), None), (12, settngs.Setting('-t', '--test', action='store_false'), bool, False),
(settngs.Setting('-t', '--test', action='version'), None), (13, settngs.Setting('-t', '--test', action=settngs.BooleanOptionalAction), bool, True),
(settngs.Setting('-t', '--test', type=int), int), (14, settngs.Setting('-t', '--test', action=_customAction), 'Any', True),
(settngs.Setting('-t', '--test', type=_typed_function), test_type), (15, settngs.Setting('-t', '--test', action='help'), None, True),
(settngs.Setting('-t', '--test', type=_untyped_function, default=1), int), (16, settngs.Setting('-t', '--test', action='version'), None, True),
(settngs.Setting('-t', '--test', type=_untyped_function), 'Any'), (17, settngs.Setting('-t', '--test', type=int), int, True),
(18, settngs.Setting('-t', '--test', type=int, nargs='+'), List[int], True),
(19, settngs.Setting('-t', '--test', type=_typed_function), test_type, True),
(20, settngs.Setting('-t', '--test', type=_untyped_function, default=1), int, False),
(21, settngs.Setting('-t', '--test', type=_untyped_function, default=[1]), List[int], False),
(22, settngs.Setting('-t', '--test', type=_untyped_function), 'Any', True),
(23, settngs.Setting('-t', '--test', type=_untyped_function, default={1}), Set[int], False),
(24, settngs.Setting('-t', '--test', action='append', type=int), List[int], True),
(25, settngs.Setting('-t', '--test', action='extend', type=int, nargs=2), List[int], True),
(26, settngs.Setting('-t', '--test', action='append', type=int, nargs=2), List[List[int]], True),
(27, settngs.Setting('-t', '--test', action='extend', nargs='+'), List[str], True),
(28, settngs.Setting('-t', '--test', action='extend', type=_typed_list_generic_function), List[test_type], True),
(29, settngs.Setting('-t', '--test', action='extend', type=_typed_list_function), List, True),
(30, settngs.Setting('-t', '--test', action='extend', type=_typed_set_function), Set, True),
) )
@pytest.mark.parametrize('setting,typ', types) @pytest.mark.parametrize('num,setting,typ,noneable_expected', types)
def test_guess_type(setting, typ): def test_guess_type(num, setting, typ, noneable_expected):
guessed_type = setting._guess_type() x = setting._guess_type()
guessed_type, noneable = x
assert guessed_type == typ assert guessed_type == typ
assert noneable == noneable_expected
expected_src = '''from __future__ import annotations
import settngs
{extra_imports}
class SettngsNS(settngs.TypedNS):
test__test: {typ}
'''
no_type_expected_src = '''from __future__ import annotations
import settngs
class SettngsNS(settngs.TypedNS):
...
'''
settings = ( settings = (
(lambda parser: parser.add_setting('-t', '--test'), 'str'), (0, lambda parser: parser.add_setting('-t', '--test'), expected_src.format(extra_imports='', typ='str | None')),
(lambda parser: parser.add_setting('-t', '--test', cmdline=False), 'typing.Any'), (1, lambda parser: parser.add_setting('-t', '--test', cmdline=False), expected_src.format(extra_imports='import typing\n', typ='typing.Any')),
(lambda parser: parser.add_setting('-t', '--test', default=1, file=True, cmdline=False), 'int'), (2, lambda parser: parser.add_setting('-t', '--test', default=1, file=True, cmdline=False), expected_src.format(extra_imports='', typ='int')),
(lambda parser: parser.add_setting('-t', '--test', action='count'), 'int'), (3, lambda parser: parser.add_setting('-t', '--test', default='test'), expected_src.format(extra_imports='', typ='str')),
(lambda parser: parser.add_setting('-t', '--test', action='append'), List[str]), (4, lambda parser: parser.add_setting('-t', '--test', default='test', file=True, cmdline=False), expected_src.format(extra_imports='', typ='str')),
(lambda parser: parser.add_setting('-t', '--test', action='extend'), List[str]), (5, lambda parser: parser.add_setting('-t', '--test', action='count'), expected_src.format(extra_imports='', typ='int | None')),
(lambda parser: parser.add_setting('-t', '--test', action='store_const', const=1), 'int'), (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')),
(lambda parser: parser.add_setting('-t', '--test', action='append_const', const=1), 'list'), (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')),
(lambda parser: parser.add_setting('-t', '--test', action='store_true'), 'bool'), (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')),
(lambda parser: parser.add_setting('-t', '--test', action='store_false'), 'bool'), (9, lambda parser: parser.add_setting('-t', '--test', action='store_const', const=1), expected_src.format(extra_imports='', typ='int | None')),
(lambda parser: parser.add_setting('-t', '--test', action=settngs.BooleanOptionalAction), 'bool'), (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')),
(lambda parser: parser.add_setting('-t', '--test', action=_customAction), 'typing.Any'), (11, lambda parser: parser.add_setting('-t', '--test', action='store_true'), expected_src.format(extra_imports='', typ='bool')),
(lambda parser: parser.add_setting('-t', '--test', action='help'), None), (12, lambda parser: parser.add_setting('-t', '--test', action='store_false'), expected_src.format(extra_imports='', typ='bool')),
(lambda parser: parser.add_setting('-t', '--test', action='version'), None), (13, lambda parser: parser.add_setting('-t', '--test', action=settngs.BooleanOptionalAction), expected_src.format(extra_imports='', typ='bool | None')),
(lambda parser: parser.add_setting('-t', '--test', type=int), 'int'), (14, lambda parser: parser.add_setting('-t', '--test', action=_customAction), expected_src.format(extra_imports='import typing\n', typ='typing.Any')),
(lambda parser: parser.add_setting('-t', '--test', nargs='+'), List[str]), (15, lambda parser: parser.add_setting('-t', '--test', action='help'), no_type_expected_src),
(lambda parser: parser.add_setting('-t', '--test', type=_typed_function), 'tests.settngs_test.test_type'), (16, lambda parser: parser.add_setting('-t', '--test', action='version'), no_type_expected_src),
(lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default=1), 'int'), (17, lambda parser: parser.add_setting('-t', '--test', type=int), expected_src.format(extra_imports='', typ='int | None')),
(lambda parser: parser.add_setting('-t', '--test', type=_untyped_function), 'typing.Any'), (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')),
(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')),
(20, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default=1), expected_src.format(extra_imports='', typ='int')),
(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]}')),
(22, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function), expected_src.format(extra_imports='import typing\n', typ='typing.Any')),
(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]}')),
(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')),
(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')),
(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')),
(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')),
(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')),
(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')),
(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')),
) )
@pytest.mark.parametrize('set_options,typ', settings) @pytest.mark.parametrize('num,set_options,expected', settings)
def test_generate_ns(settngs_manager, set_options, typ): def test_generate_ns(settngs_manager, num, set_options, expected):
settngs_manager.add_group('test', set_options) settngs_manager.add_group('test', set_options)
src = dedent('''\ imports, types = settngs_manager.generate_ns()
from __future__ import annotations generated_src = '\n\n\n'.join((imports, types))
import settngs assert generated_src == expected
''')
if 'typing.' in str(typ): ast.parse(generated_src)
src += '\nimport typing'
if typ == 'tests.settngs_test.test_type':
src += '\nimport tests.settngs_test'
src += dedent('''
class settngs_namespace(settngs.TypedNS): expected_src_dict = '''from __future__ import annotations
''')
if typ is None:
src += ' ...\n'
else:
src += f' {settngs_manager.definitions["test"].v["test"].internal_name}: {typ}\n'
generated_src = settngs_manager.generate_ns() import typing
{extra_imports}
assert generated_src == src class test(typing.TypedDict):
test: {typ}
class SettngsDict(typing.TypedDict):
test: test
'''
no_type_expected_src_dict = '''from __future__ import annotations
import typing
class test(typing.TypedDict):
...
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):
settngs_manager.add_group('test', set_options)
imports, types = settngs_manager.generate_dict()
generated_src = '\n\n\n'.join((imports, types))
assert generated_src == expected
ast.parse(generated_src) ast.parse(generated_src)