12 Commits

Author SHA1 Message Date
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
fc2a175e5b Fix normalization of settings using a custom dest 2023-12-17 16:09:41 -08:00
4 changed files with 308 additions and 113 deletions

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@ -28,20 +28,20 @@ repos:
hooks:
- id: dead
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
rev: v3.15.2
hooks:
- id: pyupgrade
args: [--py38-plus]
- repo: https://github.com/hhatto/autopep8
rev: v2.0.4
rev: v2.1.0
hooks:
- id: autopep8
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
rev: 7.0.0
hooks:
- 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
rev: v1.7.0
rev: v1.10.0
hooks:
- id: mypy

View File

@ -2,17 +2,20 @@ from __future__ import annotations
import argparse
import copy
import inspect
import json
import logging
import pathlib
import re
import sys
import typing
import warnings
from argparse import Namespace
from collections import defaultdict
from collections.abc import Sequence
from typing import Any
from typing import Callable
from typing import cast
from typing import Dict
from typing import Generic
from typing import NoReturn
@ -84,6 +87,20 @@ else: # pragma: no cover
removeprefix = str.removeprefix
def _isnamedtupleinstance(x: Any) -> bool:
t = type(x)
b = t.__bases__
if len(b) != 1 or b[0] != tuple:
return False
f = getattr(t, '_fields', None)
if not isinstance(f, tuple):
return False
return all(isinstance(n, str) for n in f)
class Setting:
def __init__(
self,
@ -137,11 +154,12 @@ class Setting:
raise ValueError('names must be specified')
# 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
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
# 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'):
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
@ -159,7 +177,6 @@ class Setting:
self.help = help
self.metavar = metavar
self.dest = dest
self.setting_name = setting_name
self.cmdline = cmdline
self.file = file
self.argparse_args = args
@ -199,6 +216,11 @@ class Setting:
return str
else:
if not self.cmdline and self.default is not None:
if not isinstance(self.default, str) and not _isnamedtupleinstance(self.default) and isinstance(self.default, Sequence) and self.default and self.default[0]:
try:
return cast(type, type(self.default)[type(self.default[0])])
except Exception:
...
return type(self.default)
return 'Any'
@ -211,6 +233,11 @@ class Setting:
t: type | str = type_hints['return']
return t
if self.default is not None:
if not isinstance(self.default, str) and not _isnamedtupleinstance(self.default) and isinstance(self.default, Sequence) and self.default and self.default[0]:
try:
return cast(type, type(self.default)[type(self.default[0])])
except Exception:
...
return type(self.default)
return 'Any'
@ -291,8 +318,8 @@ if TYPE_CHECKING:
ns = Namespace | TypedNS | Config[T] | None
def generate_ns(definitions: Definitions) -> str:
initial_imports = ['from __future__ import annotations', '', 'import settngs', '']
def generate_ns(definitions: Definitions) -> tuple[str, str]:
initial_imports = ['from __future__ import annotations', '', 'import settngs']
imports: Sequence[str] | set[str]
imports = set()
@ -324,12 +351,14 @@ def generate_ns(definitions: Definitions) -> str:
if type_name == 'Any':
type_name = 'typing.Any'
attributes.append(f' {setting.internal_name}: {type_name}')
attribute = f' {setting.internal_name}: {type_name}'
if attribute not in attributes:
attributes.append(attribute)
# Add a blank line between groups
if attributes and attributes[-1] != '':
attributes.append('')
ns = 'class settngs_namespace(settngs.TypedNS):\n'
ns = 'class SettngsNS(settngs.TypedNS):\n'
# Add a '...' expression if there are no attributes
if not attributes or all(x == '' for x in attributes):
ns += ' ...\n'
@ -343,7 +372,69 @@ def generate_ns(definitions: Definitions) -> str:
imports = sorted(list(imports - {'import typing'}))
# 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 = []
for setting in group.v.values():
t = setting._guess_type()
if t is None:
continue
# Default to any
type_name = 'Any'
# 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):
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':
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'
attribute = f' {setting.dest}: {type_name}'
if attribute not in attributes:
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:
@ -448,13 +539,13 @@ def normalize_config(
value, is_default = get_option(options, setting)
if not is_default or default:
# User has set a custom value or has requested the default value
group_options[setting_name] = value
elif setting_name in group_options:
group_options[setting.dest] = value
elif setting.dest in group_options:
# default values have been requested to be removed
del group_options[setting_name]
elif setting_name in group_options:
del group_options[setting.dest]
elif setting.dest in group_options:
# Setting type (file or cmdline) has not been requested and should be removed for persistent groups
del group_options[setting_name]
del group_options[setting.dest]
normalized[group_name] = group_options
return Config(normalized, config.definitions)
@ -597,12 +688,17 @@ def create_argparser(definitions: Definitions, description: str, epilog: str) ->
argparser = argparse.ArgumentParser(
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:
def get_current_group(setting: Setting) -> ArgParser:
if not setting.group:
return argparser
# 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(
@ -610,13 +706,17 @@ def create_argparser(definitions: Definitions, description: str, epilog: str) ->
).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)
# 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:
current_group = argparser
else:
current_group = groups[setting.group]
current_group.add_argument(*argparse_args, **argparse_kwargs)
return argparser
@ -705,9 +805,12 @@ class Manager:
return Config(c, self.definitions)
return c
def generate_ns(self) -> str:
def generate_ns(self) -> tuple[str, str]:
return generate_ns(self.definitions)
def generate_dict(self) -> tuple[str, str]:
return generate_dict(self.definitions)
def create_argparser(self) -> None:
self.argparser = create_argparser(self.definitions, self.description, self.epilog)
@ -921,6 +1024,7 @@ def example_group(manager: Manager) -> None:
manager.add_setting(
'--verbose', '-v',
default=False,
metavar='nothing',
action=BooleanOptionalAction, # Added in Python 3.9
)

View File

@ -31,7 +31,7 @@ exclude =
settngs = py.typed
[tox:tox]
envlist = py3.8,py3.9,py3.10,py3.11,pypy3
envlist = py3.8,py3.9,py3.10,py3.11,py3.12,pypy3
[testenv]
deps = -rrequirements-dev.txt

View File

@ -6,7 +6,6 @@ import json
import pathlib
import sys
from collections import defaultdict
from textwrap import dedent
from typing import Generator
import pytest
@ -18,7 +17,18 @@ from testing.settngs import failure
from testing.settngs import success
if sys.version_info < (3, 9): # pragma: no cover
if sys.version_info >= (3, 10): # pragma: no cover
List = list
help_output = '''\
usage: __main__.py [-h] [TEST ...]
positional arguments:
TEST
options:
-h, --help show this help message and exit
'''
elif sys.version_info < (3, 9): # pragma: no cover
from typing import List
help_output = '''\
usage: __main__.py [-h] [TEST [TEST ...]]
@ -42,17 +52,6 @@ optional arguments:
-h, --help show this help message and exit
'''
if sys.version_info >= (3, 10): # pragma: no cover
help_output = '''\
usage: __main__.py [-h] [TEST ...]
positional arguments:
TEST
options:
-h, --help show this help message and exit
'''
@pytest.fixture
def settngs_manager() -> Generator[settngs.Manager, None, None]:
@ -210,6 +209,24 @@ class TestValues:
assert non_defaults_normalized.values['tst'] == {'test': 'world'}
assert non_defaults_normalized.values['tst_persistent'] == {'test': 'world'}
def test_normalize_dest(self, settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test2', dest='test', default='hello'))
settngs_manager.add_persistent_group('tst_persistent', lambda parser: parser.add_setting('--test', default='hello'))
defaults = settngs_manager.defaults()
defaults_normalized = settngs_manager.normalize_config(defaults, file=True, default=False)
assert defaults_normalized.values['tst'] == {}
assert defaults_normalized.values['tst_persistent'] == {}
non_defaults = settngs_manager.defaults()
non_defaults.values['tst']['test'] = 'world'
non_defaults.values['tst_persistent']['test'] = 'world'
non_defaults_normalized = settngs_manager.normalize_config(non_defaults, file=True, default=False)
assert non_defaults_normalized.values['tst'] == {'test': 'world'}
assert non_defaults_normalized.values['tst_persistent'] == {'test': 'world'}
def test_normalize(self, settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
settngs_manager.add_persistent_group('persistent', lambda parser: parser.add_setting('--world', default='world'))
@ -317,6 +334,23 @@ class TestNamespace:
assert non_defaults_normalized.values.tst__test == 'world'
assert non_defaults_normalized.values.tst_persistent__test == 'world'
def test_normalize_dest(self, settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test2', dest='test', default='hello'))
settngs_manager.add_persistent_group('tst_persistent', lambda parser: parser.add_setting('--test', default='hello'))
defaults = settngs_manager.defaults()
defaults_normalized = settngs_manager.get_namespace(settngs_manager.normalize_config(defaults, file=True, default=False), file=True, default=False)
assert defaults_normalized.values.__dict__ == {}
non_defaults = settngs_manager.get_namespace(settngs_manager.defaults(), file=True, cmdline=True)
non_defaults.values.tst__test = 'world'
non_defaults.values.tst_persistent__test = 'world'
non_defaults_normalized = settngs_manager.get_namespace(settngs_manager.normalize_config(non_defaults, file=True, default=False), file=True, default=False)
assert non_defaults_normalized.values.tst__test == 'world'
assert non_defaults_normalized.values.tst_persistent__test == 'world'
def test_normalize(self, settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
settngs_manager.add_persistent_group('persistent', lambda parser: parser.add_setting('--world', default='world'))
@ -333,7 +367,7 @@ class TestNamespace:
assert normalized.persistent__hello == 'success'
assert normalized.persistent__world == 'world'
def test_normalize_unknown(self, settngs_manager):
def test_normalize_unknown_group(self, settngs_manager):
manager = settngs.Manager()
manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
manager.add_persistent_group('persistent', lambda parser: parser.add_setting('--world', default='world'))
@ -615,83 +649,140 @@ class _customAction(argparse.Action): # pragma: no cover
types = (
(settngs.Setting('-t', '--test'), str),
(settngs.Setting('-t', '--test', cmdline=False), 'Any'),
(settngs.Setting('-t', '--test', default=1, file=True, cmdline=False), int),
(settngs.Setting('-t', '--test', action='count'), int),
(settngs.Setting('-t', '--test', action='append'), List[str]),
(settngs.Setting('-t', '--test', action='extend'), List[str]),
(settngs.Setting('-t', '--test', action='store_const', const=1), int),
(settngs.Setting('-t', '--test', action='append_const', const=1), list),
(settngs.Setting('-t', '--test', action='store_true'), bool),
(settngs.Setting('-t', '--test', action='store_false'), bool),
(settngs.Setting('-t', '--test', action=settngs.BooleanOptionalAction), bool),
(settngs.Setting('-t', '--test', action=_customAction), 'Any'),
(settngs.Setting('-t', '--test', action='help'), None),
(settngs.Setting('-t', '--test', action='version'), None),
(settngs.Setting('-t', '--test', type=int), int),
(settngs.Setting('-t', '--test', type=_typed_function), test_type),
(settngs.Setting('-t', '--test', type=_untyped_function, default=1), int),
(settngs.Setting('-t', '--test', type=_untyped_function), 'Any'),
(0, settngs.Setting('-t', '--test'), str),
(1, settngs.Setting('-t', '--test', cmdline=False), 'Any'),
(2, settngs.Setting('-t', '--test', default=1, file=True, cmdline=False), int),
(3, settngs.Setting('-t', '--test', action='count'), int),
(4, settngs.Setting('-t', '--test', action='append'), List[str]),
(5, settngs.Setting('-t', '--test', action='extend'), List[str]),
(6, settngs.Setting('-t', '--test', nargs='+'), List[str]),
(7, settngs.Setting('-t', '--test', action='store_const', const=1), int),
(8, settngs.Setting('-t', '--test', action='append_const', const=1), list),
(9, settngs.Setting('-t', '--test', action='store_true'), bool),
(10, settngs.Setting('-t', '--test', action='store_false'), bool),
(11, settngs.Setting('-t', '--test', action=settngs.BooleanOptionalAction), bool),
(12, settngs.Setting('-t', '--test', action=_customAction), 'Any'),
(13, settngs.Setting('-t', '--test', action='help'), None),
(14, settngs.Setting('-t', '--test', action='version'), None),
(15, settngs.Setting('-t', '--test', type=int), int),
(16, settngs.Setting('-t', '--test', type=_typed_function), test_type),
(17, settngs.Setting('-t', '--test', type=_untyped_function, default=1), int),
(18, settngs.Setting('-t', '--test', type=_untyped_function), 'Any'),
)
@pytest.mark.parametrize('setting,typ', types)
def test_guess_type(setting, typ):
@pytest.mark.parametrize('num,setting,typ', types)
def test_guess_type(num, setting, typ):
guessed_type = setting._guess_type()
assert guessed_type == typ
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 = (
(lambda parser: parser.add_setting('-t', '--test'), 'str'),
(lambda parser: parser.add_setting('-t', '--test', cmdline=False), 'typing.Any'),
(lambda parser: parser.add_setting('-t', '--test', default=1, file=True, cmdline=False), 'int'),
(lambda parser: parser.add_setting('-t', '--test', action='count'), 'int'),
(lambda parser: parser.add_setting('-t', '--test', action='append'), List[str]),
(lambda parser: parser.add_setting('-t', '--test', action='extend'), List[str]),
(lambda parser: parser.add_setting('-t', '--test', action='store_const', const=1), 'int'),
(lambda parser: parser.add_setting('-t', '--test', action='append_const', const=1), 'list'),
(lambda parser: parser.add_setting('-t', '--test', action='store_true'), 'bool'),
(lambda parser: parser.add_setting('-t', '--test', action='store_false'), 'bool'),
(lambda parser: parser.add_setting('-t', '--test', action=settngs.BooleanOptionalAction), 'bool'),
(lambda parser: parser.add_setting('-t', '--test', action=_customAction), 'typing.Any'),
(lambda parser: parser.add_setting('-t', '--test', action='help'), None),
(lambda parser: parser.add_setting('-t', '--test', action='version'), None),
(lambda parser: parser.add_setting('-t', '--test', type=int), 'int'),
(lambda parser: parser.add_setting('-t', '--test', nargs='+'), List[str]),
(lambda parser: parser.add_setting('-t', '--test', type=_typed_function), 'tests.settngs_test.test_type'),
(lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default=1), 'int'),
(lambda parser: parser.add_setting('-t', '--test', type=_untyped_function), 'typing.Any'),
(0, lambda parser: parser.add_setting('-t', '--test'), expected_src.format(extra_imports='', typ='str')),
(1, lambda parser: parser.add_setting('-t', '--test', cmdline=False), expected_src.format(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')),
(3, lambda parser: parser.add_setting('-t', '--test', action='count'), expected_src.format(extra_imports='', typ='int')),
(4, 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='typing.List[str]' if sys.version_info < (3, 9) else 'list[str]')),
(5, 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='typing.List[str]' if sys.version_info < (3, 9) else 'list[str]')),
(6, lambda parser: parser.add_setting('-t', '--test', nargs='+'), expected_src.format(extra_imports='import typing\n' if sys.version_info < (3, 9) else '', typ='typing.List[str]' if sys.version_info < (3, 9) else 'list[str]')),
(7, lambda parser: parser.add_setting('-t', '--test', action='store_const', const=1), expected_src.format(extra_imports='', typ='int')),
(8, lambda parser: parser.add_setting('-t', '--test', action='append_const', const=1), expected_src.format(extra_imports='', typ='list')),
(9, lambda parser: parser.add_setting('-t', '--test', action='store_true'), expected_src.format(extra_imports='', typ='bool')),
(10, lambda parser: parser.add_setting('-t', '--test', action='store_false'), expected_src.format(extra_imports='', typ='bool')),
(11, lambda parser: parser.add_setting('-t', '--test', action=settngs.BooleanOptionalAction), expected_src.format(extra_imports='', typ='bool')),
(12, lambda parser: parser.add_setting('-t', '--test', action=_customAction), expected_src.format(extra_imports='import typing\n', typ='typing.Any')),
(13, lambda parser: parser.add_setting('-t', '--test', action='help'), no_type_expected_src),
(14, lambda parser: parser.add_setting('-t', '--test', action='version'), no_type_expected_src),
(15, lambda parser: parser.add_setting('-t', '--test', type=int), expected_src.format(extra_imports='', typ='int')),
(16, 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')),
(17, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default=1), expected_src.format(extra_imports='', typ='int')),
(18, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function), expected_src.format(extra_imports='import typing\n', typ='typing.Any')),
)
@pytest.mark.parametrize('set_options,typ', settings)
def test_generate_ns(settngs_manager, set_options, typ):
@pytest.mark.parametrize('num,set_options,expected', settings)
def test_generate_ns(settngs_manager, num, set_options, expected):
settngs_manager.add_group('test', set_options)
src = dedent('''\
from __future__ import annotations
imports, types = settngs_manager.generate_ns()
generated_src = '\n\n\n'.join((imports, types))
import settngs
''')
assert generated_src == expected
if 'typing.' in str(typ):
src += '\nimport typing'
if typ == 'tests.settngs_test.test_type':
src += '\nimport tests.settngs_test'
src += dedent('''
ast.parse(generated_src)
class settngs_namespace(settngs.TypedNS):
''')
if typ is None:
src += ' ...\n'
else:
src += f' {settngs_manager.definitions["test"].v["test"].internal_name}: {typ}\n'
expected_src_dict = '''from __future__ import annotations
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')),
(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', action='count'), expected_src_dict.format(extra_imports='', typ='int')),
(4, lambda parser: parser.add_setting('-t', '--test', action='append'), expected_src_dict.format(extra_imports='' if sys.version_info < (3, 9) else '', typ='typing.List[str]' if sys.version_info < (3, 9) else 'list[str]')),
(5, lambda parser: parser.add_setting('-t', '--test', action='extend'), expected_src_dict.format(extra_imports='' if sys.version_info < (3, 9) else '', typ='typing.List[str]' if sys.version_info < (3, 9) else 'list[str]')),
(6, lambda parser: parser.add_setting('-t', '--test', nargs='+'), expected_src_dict.format(extra_imports='' if sys.version_info < (3, 9) else '', typ='typing.List[str]' if sys.version_info < (3, 9) else 'list[str]')),
(7, lambda parser: parser.add_setting('-t', '--test', action='store_const', const=1), expected_src_dict.format(extra_imports='', typ='int')),
(8, lambda parser: parser.add_setting('-t', '--test', action='append_const', const=1), expected_src_dict.format(extra_imports='', typ='list')),
(9, lambda parser: parser.add_setting('-t', '--test', action='store_true'), expected_src_dict.format(extra_imports='', typ='bool')),
(10, lambda parser: parser.add_setting('-t', '--test', action='store_false'), expected_src_dict.format(extra_imports='', typ='bool')),
(11, lambda parser: parser.add_setting('-t', '--test', action=settngs.BooleanOptionalAction), expected_src_dict.format(extra_imports='', typ='bool')),
(12, lambda parser: parser.add_setting('-t', '--test', action=_customAction), expected_src_dict.format(extra_imports='', typ='typing.Any')),
(13, lambda parser: parser.add_setting('-t', '--test', action='help'), no_type_expected_src_dict),
(14, lambda parser: parser.add_setting('-t', '--test', action='version'), no_type_expected_src_dict),
(15, lambda parser: parser.add_setting('-t', '--test', type=int), expected_src_dict.format(extra_imports='', typ='int')),
(16, lambda parser: parser.add_setting('-t', '--test', type=_typed_function), expected_src_dict.format(extra_imports='import tests.settngs_test\n', typ='tests.settngs_test.test_type')),
(17, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function, default=1), expected_src_dict.format(extra_imports='', typ='int')),
(18, lambda parser: parser.add_setting('-t', '--test', type=_untyped_function), expected_src_dict.format(extra_imports='', typ='typing.Any')),
)
@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)