6 Commits
0.7.0 ... 0.7.2

Author SHA1 Message Date
101eef56ca Sanitize group names 2023-09-04 18:32:14 -05:00
2f000e12f3 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/add-trailing-comma: v3.0.1 → v3.1.0](https://github.com/asottile/add-trailing-comma/compare/v3.0.1...v3.1.0)
- https://github.com/pre-commit/mirrors-autopep8https://github.com/hhatto/autopep8
- [github.com/hhatto/autopep8: v2.0.2 → v2.0.4](https://github.com/hhatto/autopep8/compare/v2.0.2...v2.0.4)
- [github.com/pre-commit/mirrors-mypy: v1.5.0 → v1.5.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.0...v1.5.1)
2023-09-04 19:22:46 +00:00
0301e1698c Merge pull request #5 from lordwelch/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-08-15 07:43:05 -07:00
6af1e3c562 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/setup-cfg-fmt: v2.3.0 → v2.4.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.3.0...v2.4.0)
- [github.com/asottile/add-trailing-comma: v2.5.1 → v3.0.1](https://github.com/asottile/add-trailing-comma/compare/v2.5.1...v3.0.1)
- [github.com/asottile/pyupgrade: v3.7.0 → v3.10.1](https://github.com/asottile/pyupgrade/compare/v3.7.0...v3.10.1)
- [github.com/PyCQA/flake8: 6.0.0 → 6.1.0](https://github.com/PyCQA/flake8/compare/6.0.0...6.1.0)
- [github.com/pre-commit/mirrors-mypy: v1.3.0 → v1.5.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.3.0...v1.5.0)
2023-08-14 19:13:59 +00:00
fe3bce42fd [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/reorder-python-imports: v3.9.0 → v3.10.0](https://github.com/asottile/reorder-python-imports/compare/v3.9.0...v3.10.0)
- [github.com/asottile/add-trailing-comma: v2.4.0 → v2.5.1](https://github.com/asottile/add-trailing-comma/compare/v2.4.0...v2.5.1)
- [github.com/asottile/dead: v1.5.1 → v1.5.2](https://github.com/asottile/dead/compare/v1.5.1...v1.5.2)
- [github.com/asottile/pyupgrade: v3.4.0 → v3.7.0](https://github.com/asottile/pyupgrade/compare/v3.4.0...v3.7.0)
2023-06-19 19:05:50 +00:00
4c41e6f588 Use a custom class for typing
Add support for nargs to default to list[str]
2023-06-09 15:41:18 -07:00
4 changed files with 65 additions and 44 deletions

View File

@ -10,38 +10,38 @@ 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.3.0 rev: v2.4.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.9.0 rev: v3.10.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: v2.4.0 rev: v3.1.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.1 rev: v1.5.2
hooks: hooks:
- id: dead - id: dead
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.4.0 rev: v3.10.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py38-plus] args: [--py38-plus]
- repo: https://github.com/pre-commit/mirrors-autopep8 - repo: https://github.com/hhatto/autopep8
rev: v2.0.2 rev: v2.0.4
hooks: hooks:
- id: autopep8 - id: autopep8
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 6.0.0 rev: 6.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-length, flake8-print]
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0 rev: v1.5.1
hooks: hooks:
- id: mypy - id: mypy

View File

@ -141,10 +141,9 @@ class Setting:
metavar = dest.upper() metavar = dest.upper()
# If we are not a flag, no '--' or '-' in front # If we are not a flag, no '--' or '-' in front
# we prefix the first name with the group as argparse sets dest to args[0] # we use internal_name as argparse sets dest to args[0]
# I believe internal name may be able to be used here
if not flag: if not flag:
args = tuple((f'{group}_{names[0]}'.lstrip('_'), *names[1:])) args = tuple((self.internal_name, *names[1:]))
self.action = action self.action = action
self.nargs = nargs self.nargs = nargs
@ -190,6 +189,8 @@ class Setting:
def _guess_type(self) -> type | Literal['Any'] | None: def _guess_type(self) -> type | Literal['Any'] | None:
if self.type is None and self.action is None: if self.type is None and self.action is None:
if self.cmdline: if self.cmdline:
if self.nargs in ('+', '*') or isinstance(self.nargs, int) and self.nargs > 1:
return List[str]
return str return str
else: else:
if not self.cmdline and self.default is not None: if not self.cmdline and self.default is not None:
@ -230,6 +231,7 @@ class Setting:
dest_name = None dest_name = None
flag = False flag = False
prefix = sanitize_name(prefix)
for n in names: for n in names:
if n.startswith('--'): if n.startswith('--'):
flag = True flag = True
@ -255,6 +257,11 @@ class Setting:
return self.argparse_args, self.filter_argparse_kwargs() return self.argparse_args, self.filter_argparse_kwargs()
class TypedNS:
def __init__(self) -> None:
raise TypeError('TypedNS cannot be instantiated')
class Group(NamedTuple): class Group(NamedTuple):
persistent: bool persistent: bool
v: dict[str, Setting] v: dict[str, Setting]
@ -263,7 +270,7 @@ class Group(NamedTuple):
Values = Dict[str, Dict[str, Any]] Values = Dict[str, Dict[str, Any]]
Definitions = Dict[str, Group] Definitions = Dict[str, Group]
T = TypeVar('T', Values, Namespace) T = TypeVar('T', bound=Union[Values, Namespace, TypedNS])
class Config(NamedTuple, Generic[T]): class Config(NamedTuple, Generic[T]):
@ -273,12 +280,12 @@ 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 | Config[T] | None ns = Namespace | TypedNS | Config[T] | None
def generate_ns(definitions: Definitions) -> str: def generate_ns(definitions: Definitions) -> str:
imports = ['from __future__ import annotations', 'import typing', 'import settngs'] imports = ['from __future__ import annotations', 'import typing', 'import settngs']
ns = 'class settngs_namespace(settngs.Namespace):\n' ns = 'class settngs_namespace(settngs.TypedNS):\n'
types = [] types = []
for group_name, group in definitions.items(): for group_name, group in definitions.items():
for setting_name, setting in group.v.items(): for setting_name, setting in group.v.items():
@ -288,7 +295,7 @@ def generate_ns(definitions: Definitions) -> str:
type_name = 'Any' type_name = 'Any'
if isinstance(t, str): if isinstance(t, str):
type_name = t type_name = t
elif type(t) == types_GenericAlias: elif isinstance(t, types_GenericAlias):
type_name = str(t) type_name = str(t)
elif isinstance(t, type): elif isinstance(t, type):
type_name = t.__name__ type_name = t.__name__
@ -313,7 +320,7 @@ def sanitize_name(name: str) -> str:
return re.sub('[' + re.escape(' -_,.!@#$%^&*(){}[]\',."<>;:') + ']+', '_', name).strip('_') return re.sub('[' + re.escape(' -_,.!@#$%^&*(){}[]\',."<>;:') + ']+', '_', name).strip('_')
def get_option(options: Values | Namespace, setting: Setting) -> tuple[Any, bool]: def get_option(options: Values | Namespace | TypedNS, setting: Setting) -> tuple[Any, bool]:
""" """
Helper function to retrieve the value for a setting and if the current value is the default value Helper function to retrieve the value for a setting and if the current value is the default value
@ -337,7 +344,7 @@ def get_options(config: Config[T], group: str) -> dict[str, Any]:
group: The name of the group to retrieve group: The name of the group to retrieve
""" """
if isinstance(config[0], dict): if isinstance(config[0], dict):
values = config[0].get(group, {}).copy() values: dict[str, Any] = config[0].get(group, {}).copy()
else: else:
internal_names = {x.internal_name: x for x in config[1][group].v.values()} internal_names = {x.internal_name: x for x in config[1][group].v.values()}
values = {} values = {}
@ -470,11 +477,14 @@ def get_namespace(
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')
if isinstance(config.values, Namespace): options: Values
definitions: Definitions
if isinstance(config.values, dict):
options = config.values
definitions = config.definitions
else:
cfg = normalize_config(config, file=file, cmdline=cmdline, default=default, persistent=persistent) cfg = normalize_config(config, file=file, cmdline=cmdline, default=default, persistent=persistent)
options, definitions = cfg options, definitions = cfg
else:
options, definitions = config
namespace = Namespace() namespace = Namespace()
for group_name, group in definitions.items(): for group_name, group in definitions.items():
@ -573,7 +583,7 @@ def parse_cmdline(
args: Passed to argparse.ArgumentParser.parse_args args: Passed to argparse.ArgumentParser.parse_args
config: The Config or Namespace object to use as a Namespace passed to argparse.ArgumentParser.parse_args config: The Config or Namespace object to use as a Namespace passed to argparse.ArgumentParser.parse_args
""" """
namespace = None namespace: Namespace | TypedNS | None = None
if isinstance(config, Config): if isinstance(config, Config):
if isinstance(config.values, Namespace): if isinstance(config.values, Namespace):
namespace = config.values namespace = config.values
@ -581,6 +591,7 @@ def parse_cmdline(
namespace = get_namespace(config, file=True, cmdline=True, default=False)[0] namespace = get_namespace(config, file=True, cmdline=True, default=False)[0]
else: else:
namespace = config namespace = config
argparser = create_argparser(definitions, description, epilog) argparser = create_argparser(definitions, description, epilog)
ns = argparser.parse_args(args, namespace=namespace) ns = argparser.parse_args(args, namespace=namespace)
@ -866,7 +877,7 @@ 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!')
manager.add_group('example', 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 = manager.parse_file(settings_path)
@ -875,14 +886,14 @@ def _main(args: list[str] | None = None) -> None:
merged_config = manager.parse_cmdline(args=args, config=file_namespace) merged_config = manager.parse_cmdline(args=args, config=file_namespace)
merged_namespace = manager.get_namespace(merged_config, file=True, cmdline=True) merged_namespace = manager.get_namespace(merged_config, file=True, cmdline=True)
print(f'Hello {merged_config.values["example"]["hello"]}') # noqa: T201 print(f'Hello {merged_config.values["Example Group"]["hello"]}') # noqa: T201
if merged_namespace.values.example_save: if merged_namespace.values.Example_Group_save:
if manager.save_file(merged_config, settings_path): if manager.save_file(merged_config, settings_path):
print(f'Successfully saved settings to {settings_path}') # noqa: T201 print(f'Successfully saved settings to {settings_path}') # noqa: T201
else: else:
print(f'Failed saving settings to a {settings_path}') # noqa: T201 print(f'Failed saving settings to a {settings_path}') # noqa: T201
if merged_namespace.values.example_verbose: if merged_namespace.values.Example_Group_verbose:
print(f'{merged_namespace.values.example_verbose=}') # noqa: T201 print(f'{merged_namespace.values.Example_Group_verbose=}') # noqa: T201
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -15,52 +15,52 @@ example: list[tuple[list[str], str, str]] = [
( (
['--hello', 'lordwelch', '-s'], ['--hello', 'lordwelch', '-s'],
'Hello lordwelch\nSuccessfully saved settings to settings.json\n', 'Hello lordwelch\nSuccessfully saved settings to settings.json\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": false\n }\n}\n', '{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": false\n }\n}\n',
), ),
( (
[], [],
'Hello lordwelch\n', 'Hello lordwelch\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": false\n }\n}\n', '{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": false\n }\n}\n',
), ),
( (
['-v'], ['-v'],
'Hello lordwelch\nmerged_namespace.values.example_verbose=True\n', 'Hello lordwelch\nmerged_namespace.values.Example_Group_verbose=True\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": false\n }\n}\n', '{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": false\n }\n}\n',
), ),
( (
['-v', '-s'], ['-v', '-s'],
'Hello lordwelch\nSuccessfully saved settings to settings.json\nmerged_namespace.values.example_verbose=True\n', 'Hello lordwelch\nSuccessfully saved settings to settings.json\nmerged_namespace.values.Example_Group_verbose=True\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false\n }\n}\n', '{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false\n }\n}\n',
), ),
( (
[], [],
'Hello lordwelch\nmerged_namespace.values.example_verbose=True\n', 'Hello lordwelch\nmerged_namespace.values.Example_Group_verbose=True\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false\n }\n}\n', '{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false\n }\n}\n',
), ),
( (
['manual settings.json'], ['manual settings.json'],
'Hello lordwelch\nmerged_namespace.values.example_verbose=True\n', 'Hello lordwelch\nmerged_namespace.values.Example_Group_verbose=True\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n', '{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n',
), ),
( (
['--no-verbose', '-t'], ['--no-verbose', '-t'],
'Hello lordwelch\n', 'Hello lordwelch\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n', '{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n',
), ),
( (
['--no-verbose', '-s', '-t'], ['--no-verbose', '-s', '-t'],
'Hello lordwelch\nSuccessfully saved settings to settings.json\n', 'Hello lordwelch\nSuccessfully saved settings to settings.json\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": true,\n "hello": "world"\n }\n}\n', '{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": false\n },\n "persistent": {\n "test": true,\n "hello": "world"\n }\n}\n',
), ),
( (
['--hello', 'world', '--no-verbose', '--no-test', '-s'], ['--hello', 'world', '--no-verbose', '--no-test', '-s'],
'Hello world\nSuccessfully saved settings to settings.json\n', 'Hello world\nSuccessfully saved settings to settings.json\n',
'{\n "example": {\n "hello": "world",\n "verbose": false\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n', '{\n "Example Group": {\n "hello": "world",\n "verbose": false\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n',
), ),
( (
[], [],
'Hello world\n', 'Hello world\n',
'{\n "example": {\n "hello": "world",\n "verbose": false\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n', '{\n "Example Group": {\n "hello": "world",\n "verbose": false\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n',
), ),
] ]
success = [ success = [

View File

@ -19,7 +19,7 @@ from testing.settngs import success
if sys.version_info < (3, 9): # pragma: no cover if sys.version_info < (3, 9): # pragma: no cover
from typing import List from typing import List
else: else: # pragma: no cover
List = list List = list
@ -81,6 +81,11 @@ class TestValues:
defaults, _ = settngs_manager.defaults() defaults, _ = settngs_manager.defaults()
assert defaults['tst']['test'] == 'hello' assert defaults['tst']['test'] == 'hello'
def test_get_defaults_group_space(self, settngs_manager):
settngs_manager.add_group('Testing tst', lambda parser: parser.add_setting('--test', default='hello'))
defaults, _ = settngs_manager.defaults()
assert defaults['Testing tst']['test'] == 'hello'
def test_cmdline_only(self, settngs_manager): def test_cmdline_only(self, settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello', file=False)) settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello', file=False))
settngs_manager.add_group('tst2', lambda parser: parser.add_setting('--test2', default='hello', cmdline=False)) settngs_manager.add_group('tst2', lambda parser: parser.add_setting('--test2', default='hello', cmdline=False))
@ -161,6 +166,11 @@ class TestNamespace:
defaults, _ = settngs_manager.get_namespace(settngs_manager.defaults(), file=True, cmdline=True) defaults, _ = settngs_manager.get_namespace(settngs_manager.defaults(), file=True, cmdline=True)
assert defaults.tst_test == 'hello' assert defaults.tst_test == 'hello'
def test_get_defaults_group_space(self, settngs_manager):
settngs_manager.add_group('Testing tst', lambda parser: parser.add_setting('--test', default='hello'))
defaults, _ = settngs_manager.get_namespace(settngs_manager.defaults(), file=True, cmdline=True)
assert defaults.Testing_tst_test == 'hello'
def test_cmdline_only(self, settngs_manager): def test_cmdline_only(self, settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello', file=False)) settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello', file=False))
settngs_manager.add_group('tst2', lambda parser: parser.add_setting('--test2', default='hello', cmdline=False)) settngs_manager.add_group('tst2', lambda parser: parser.add_setting('--test2', default='hello', cmdline=False))
@ -541,7 +551,7 @@ import settngs
if typ == 'tests.settngs_test.test_type': if typ == 'tests.settngs_test.test_type':
src += 'import tests.settngs_test\n' src += 'import tests.settngs_test\n'
src += ''' src += '''
class settngs_namespace(settngs.Namespace): class settngs_namespace(settngs.TypedNS):
''' '''
if typ is None: if typ is None:
src += ' ...\n' src += ' ...\n'
@ -564,7 +574,7 @@ def test_example(capsys, tmp_path, monkeypatch):
for args, expected_out, expected_file in example: for args, expected_out, expected_file in example:
if args == ['manual settings.json']: if args == ['manual settings.json']:
settings_file.unlink() settings_file.unlink()
settings_file.write_text('{\n "example": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n') settings_file.write_text('{\n "Example Group": {\n "hello": "lordwelch",\n "verbose": true\n },\n "persistent": {\n "test": false,\n "hello": "world"\n }\n}\n')
i += 1 i += 1
continue continue
else: else: