Make most functions work off of Config objects and Values/Namespace objects

Used Generic for better typing
Backported BooleanOptionalAction for tests on Python 3.8
Remove merge support in normalize_config
Add test to validate that the example works
This commit is contained in:
Timmy Welch 2022-12-15 18:53:29 -08:00
parent eb0b6ec0fe
commit 38e42bf3f9
No known key found for this signature in database
5 changed files with 294 additions and 128 deletions

View File

@ -24,27 +24,27 @@ $ python -m settngs --hello lordwelch
Hello lordwelch
$ python -m settngs --hello lordwelch -s
Hello lordwelch
Successfully saved settngs to settngs.json
Successfully saved settings to settings.json
$ python -m settngs
Hello lordwelch
$ python -m settngs -v
Hello lordwelch
merged_namespace.example_verbose=True
merged_namespace.values.example_verbose=True
$ python -m settngs -v -s
Hello lordwelch
Successfully saved settngs to settngs.json
merged_namespace.example_verbose=True
Successfully saved settings to settings.json
merged_namespace.values.example_verbose=True
$ python -m settngs
Hello lordwelch
merged_namespace.example_verbose=True
merged_namespace.values.example_verbose=True
$ python -m settngs --no-verbose
Hello lordwelch
$ python -m settngs --no-verbose -s
Hello lordwelch
Successfully saved settngs to settngs.json
Successfully saved settings to settings.json
$ python -m settngs --hello world --no-verbose -s
Hello world
Successfully saved settngs to settngs.json
Successfully saved settings to settings.json
$ python -m settngs
Hello world
```

View File

@ -4,18 +4,69 @@ import argparse
import json
import logging
import pathlib
import sys
from argparse import Namespace
from collections import defaultdict
from collections.abc import Sequence
from typing import Any
from typing import Callable
from typing import Dict
from typing import NamedTuple
from typing import Generic
from typing import NoReturn
from typing import overload
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
logger = logging.getLogger(__name__)
if sys.version_info < (3, 11): # pragma: no cover
from typing_extensions import NamedTuple
else: # pragma: no cover
from typing import NamedTuple
if sys.version_info < (3, 9): # pragma: no cover
class BooleanOptionalAction(argparse.Action):
def __init__(
self,
option_strings,
dest,
default=None,
type=None, # noqa: A002
choices=None,
required=False,
help=None, # noqa: A002
metavar=None,
):
_option_strings = []
for option_string in option_strings:
_option_strings.append(option_string)
if option_string.startswith('--'):
option_string = '--no-' + option_string[2:]
_option_strings.append(option_string)
if help is not None and default is not None and default is not argparse.SUPPRESS:
help += ' (default: %(default)s)'
super().__init__(
option_strings=_option_strings,
dest=dest,
nargs=0,
default=default,
type=type,
choices=choices,
required=required,
help=help,
metavar=metavar,
)
def __call__(self, parser, namespace, values, option_string=None): # dead: disable
if option_string in self.option_strings:
setattr(namespace, self.dest, not option_string.startswith('--no-'))
else: # pragma: no cover
from argparse import BooleanOptionalAction
class Setting:
def __init__(
@ -84,10 +135,10 @@ class Setting:
'dest': self.internal_name if flag else None,
}
def __str__(self) -> str:
def __str__(self) -> str: # pragma: no cover
return f'Setting({self.argparse_args}, type={self.type}, file={self.file}, cmdline={self.cmdline}, kwargs={self.argparse_kwargs})'
def __repr__(self) -> str:
def __repr__(self) -> str: # pragma: no cover
return self.__str__()
def get_dest(self, prefix: str, names: Sequence[str], dest: str | None) -> tuple[str, str, bool]:
@ -120,17 +171,20 @@ class Setting:
Values = Dict[str, Dict[str, Any]]
Definitions = Dict[str, Dict[str, Setting]]
T = TypeVar('T', Values, Namespace)
class Config(NamedTuple):
values: Values
class Config(NamedTuple, Generic[T]):
values: T
definitions: Definitions
if TYPE_CHECKING:
ArgParser = Union[argparse._MutuallyExclusiveGroup, argparse._ArgumentGroup, argparse.ArgumentParser]
ns = Namespace | Config[T] | None
def get_option(options: Values | argparse.Namespace, setting: Setting) -> tuple[Any, bool]:
def get_option(options: Values | Namespace, setting: Setting) -> tuple[Any, bool]:
"""
Helper function to retrieve the value for a setting and if the value is the default value
@ -146,13 +200,11 @@ def get_option(options: Values | argparse.Namespace, setting: Setting) -> tuple[
def normalize_config(
raw_options: Values | argparse.Namespace,
definitions: Definitions,
config: Config[T],
file: bool = False,
cmdline: bool = False,
defaults: bool = True,
raw_options_2: Values | argparse.Namespace | None = None,
) -> Values:
) -> Config[Values]:
"""
Creates an `OptionValues` dictionary with setting definitions taken from `self.definitions`
and values taken from `raw_options` and `raw_options_2' if defined.
@ -166,27 +218,22 @@ def normalize_config(
defaults: Include default values in the returned dict
raw_options_2: If set, merges non-default values into the returned dict
"""
options: Values = {}
normalized: Values = {}
options, definitions = config
for group_name, group in definitions.items():
group_options = {}
for setting_name, setting in group.items():
if (setting.cmdline and cmdline) or (setting.file and file):
# Ensures the option exists with the default if not already set
value, default = get_option(raw_options, setting)
value, default = get_option(options, setting)
if not default or default and defaults:
group_options[setting_name] = value
# will override with option from raw_options_2 if it is not the default
if raw_options_2 is not None:
value, default = get_option(raw_options_2, setting)
if not default:
group_options[setting_name] = value
options[group_name] = group_options
# options["definitions"] = definitions
return options
normalized[group_name] = group_options
return Config(normalized, definitions)
def parse_file(definitions: Definitions, filename: pathlib.Path) -> tuple[Values, bool]:
def parse_file(definitions: Definitions, filename: pathlib.Path) -> tuple[Config[Values], bool]:
"""
Helper function to read options from a json dictionary from a file
Args:
@ -205,13 +252,13 @@ def parse_file(definitions: Definitions, filename: pathlib.Path) -> tuple[Values
success = False
else:
logger.info('No config file found')
success = False
success = True
return (normalize_config(options, definitions, file=True), success)
return (normalize_config(Config(options, definitions), file=True), success)
def clean_config(
options: Values | argparse.Namespace, definitions: Definitions, file: bool = False, cmdline: bool = False,
config: Config[T], file: bool = False, cmdline: bool = False,
) -> Values:
"""
Normalizes options and then cleans up empty groups and removes 'definitions'
@ -224,20 +271,20 @@ def clean_config(
"""
clean_options = normalize_config(options, definitions, file=file, cmdline=cmdline)
clean_options, definitions = normalize_config(config, file=file, cmdline=cmdline)
for group in list(clean_options.keys()):
if not clean_options[group]:
del clean_options[group]
return clean_options
def defaults(definitions: Definitions) -> Values:
return normalize_config({}, definitions, file=True, cmdline=True)
def defaults(definitions: Definitions) -> Config[Values]:
return normalize_config(Config(Namespace(), definitions), file=True, cmdline=True)
def get_namespace(options: Values, definitions: Definitions, defaults: bool = True) -> argparse.Namespace:
def get_namespace(config: Config[T], defaults: bool = True) -> Config[Namespace]:
"""
Returns an argparse.Namespace object with options in the form "{group_name}_{setting_name}"
Returns an Namespace object with options in the form "{group_name}_{setting_name}"
`options` should already be normalized.
Throws an exception if the internal_name is duplicated
@ -245,8 +292,12 @@ def get_namespace(options: Values, definitions: Definitions, defaults: bool = Tr
options: Normalized options to turn into a Namespace
defaults: Include default values in the returned dict
"""
options = normalize_config(options, definitions, file=True, cmdline=True)
namespace = argparse.Namespace()
if isinstance(config.values, Namespace):
options, definitions = normalize_config(config)
else:
options, definitions = config
namespace = Namespace()
for group_name, group in definitions.items():
for setting_name, setting in group.items():
if hasattr(namespace, setting.internal_name):
@ -255,11 +306,11 @@ def get_namespace(options: Values, definitions: Definitions, defaults: bool = Tr
if not default or default and defaults:
setattr(namespace, setting.internal_name, value)
return namespace
return Config(namespace, definitions)
def save_file(
options: Values | argparse.Namespace, definitions: Definitions, filename: pathlib.Path,
config: Config[T], filename: pathlib.Path,
) -> bool:
"""
Helper function to save options from a json dictionary to a file
@ -267,14 +318,14 @@ def save_file(
options: The options to save to a json dictionary
filename: A pathlib.Path object to save the json dictionary to
"""
file_options = clean_config(options, definitions, file=True)
file_options = clean_config(config, file=True)
if not filename.exists():
filename.parent.mkdir(exist_ok=True, parents=True)
filename.touch()
try:
json_str = json.dumps(file_options, indent=2)
filename.write_text(json_str, encoding='utf-8')
filename.write_text(json_str + '\n', encoding='utf-8')
except Exception:
logger.exception('Failed to save config file: %s', filename)
return False
@ -313,8 +364,8 @@ def parse_cmdline(
description: str,
epilog: str,
args: list[str] | None = None,
namespace: argparse.Namespace | None = None,
) -> Values:
config: Namespace | Config[T] | None = None,
) -> Config[Values]:
"""
Creates an `argparse.ArgumentParser` from cmdline settings in `self.definitions`.
`args` and `namespace` are passed to `argparse.ArgumentParser.parse_args`
@ -323,10 +374,18 @@ def parse_cmdline(
args: Passed to argparse.ArgumentParser.parse
namespace: Passed to argparse.ArgumentParser.parse
"""
namespace = None
if isinstance(config, Config):
if isinstance(config.values, Namespace):
namespace = config.values
else:
namespace = get_namespace(config, defaults=False)[0]
else:
namespace = config
argparser = create_argparser(definitions, description, epilog)
ns = argparser.parse_args(args, namespace=namespace)
return normalize_config(ns, definitions=definitions, cmdline=True, file=True)
return normalize_config(Config(ns, definitions), cmdline=True, file=True)
def parse_config(
@ -335,26 +394,29 @@ def parse_config(
epilog: str,
config_path: pathlib.Path,
args: list[str] | None = None,
) -> tuple[Values, bool]:
) -> tuple[Config[Values], bool]:
file_options, success = parse_file(definitions, config_path)
cmdline_options = parse_cmdline(
definitions, description, epilog, args, get_namespace(file_options, definitions, defaults=False),
definitions, description, epilog, args, get_namespace(file_options, defaults=False),
)
final_options = normalize_config(cmdline_options, definitions=definitions, file=True, cmdline=True)
final_options = normalize_config(cmdline_options, file=True, cmdline=True)
return (final_options, success)
class Manager:
"""docstring for Manager"""
def __init__(self, description: str = '', epilog: str = '', definitions: Definitions | None = None):
def __init__(self, description: str = '', epilog: str = '', definitions: Definitions | Config[T] | None = None):
# This one is never used, it just makes MyPy happy
self.argparser = argparse.ArgumentParser(description=description, epilog=epilog)
self.description = description
self.epilog = epilog
self.definitions: Definitions = defaultdict(lambda: dict(), definitions or {})
if isinstance(definitions, Config):
self.definitions = definitions.definitions
else:
self.definitions = defaultdict(lambda: dict(), definitions or {})
self.exclusive_group = False
self.current_group_name = ''
@ -364,7 +426,7 @@ class Manager:
def add_setting(self, *args: Any, **kwargs: Any) -> None:
"""Takes passes all arguments through to `Setting`, `group` and `exclusive` are already set"""
setting = Setting(*args, group=self.current_group_name, exclusive=self.exclusive_group, **kwargs)
setting = Setting(*args, **kwargs, group=self.current_group_name, exclusive=self.exclusive_group)
self.definitions[self.current_group_name][setting.dest] = setting
def add_group(self, name: str, add_settings: Callable[[Manager], None], exclusive_group: bool = False) -> None:
@ -387,49 +449,64 @@ class Manager:
self.argparser.exit(*args, **kwargs)
raise SystemExit(99)
def defaults(self) -> Values:
def defaults(self) -> Config[Values]:
return defaults(self.definitions)
def clean_config(
self, options: Values | argparse.Namespace, file: bool = False, cmdline: bool = False,
self, options: T | Config[T], file: bool = False, cmdline: bool = False,
) -> Values:
return clean_config(options=options, definitions=self.definitions, file=file, cmdline=cmdline)
if isinstance(options, Config):
config = options
else:
config = Config(options, self.definitions)
return clean_config(config, file=file, cmdline=cmdline)
def normalize_config(
self,
raw_options: Values | argparse.Namespace,
options: T | Config[T],
file: bool = False,
cmdline: bool = False,
defaults: bool = True,
raw_options_2: Values | argparse.Namespace | None = None,
) -> Config:
return Config(
normalize_config(
raw_options=raw_options,
definitions=self.definitions,
file=file,
cmdline=cmdline,
defaults=defaults,
raw_options_2=raw_options_2,
),
self.definitions,
) -> Config[Values]:
if isinstance(options, Config):
config = options
else:
config = Config(options, self.definitions)
return normalize_config(
config=config,
file=file,
cmdline=cmdline,
defaults=defaults,
)
def get_namespace(self, options: Values, defaults: bool = True) -> argparse.Namespace:
return get_namespace(options=options, definitions=self.definitions, defaults=defaults)
@overload
def get_namespace(self, options: Values, defaults: bool = True) -> Namespace:
...
def parse_file(self, filename: pathlib.Path) -> tuple[Values, bool]:
@overload
def get_namespace(self, options: Config[Values], defaults: bool = True) -> Config[Namespace]:
...
def get_namespace(self, options: Values | Config[Values], defaults: bool = True) -> Config[Namespace] | Namespace:
if isinstance(options, Config):
self.definitions = options[1]
return get_namespace(options, defaults=defaults)
else:
return get_namespace(Config(options, self.definitions), defaults=defaults)
def parse_file(self, filename: pathlib.Path) -> tuple[Config[Values], bool]:
return parse_file(filename=filename, definitions=self.definitions)
def save_file(self, options: Values | argparse.Namespace, filename: pathlib.Path) -> bool:
return save_file(options=options, definitions=self.definitions, filename=filename)
def save_file(self, options: T | Config[T], filename: pathlib.Path) -> bool:
if isinstance(options, Config):
return save_file(options, filename=filename)
return save_file(Config(options, self.definitions), filename=filename)
def parse_cmdline(self, args: list[str] | None = None, namespace: argparse.Namespace | None = None) -> Values:
def parse_cmdline(self, args: list[str] | None = None, namespace: ns[T] = None) -> Config[Values]:
return parse_cmdline(self.definitions, self.description, self.epilog, args, namespace)
def parse_config(self, config_path: pathlib.Path, args: list[str] | None = None) -> tuple[Config, bool]:
values, success = parse_config(self.definitions, self.description, self.epilog, config_path, args)
return (Config(values, self.definitions), success)
def parse_config(self, config_path: pathlib.Path, args: list[str] | None = None) -> tuple[Config[Values], bool]:
return parse_config(self.definitions, self.description, self.epilog, config_path, args)
def example(manager: Manager) -> None:
@ -446,11 +523,11 @@ def example(manager: Manager) -> None:
manager.add_setting(
'--verbose', '-v',
default=False,
action=argparse.BooleanOptionalAction,
action=BooleanOptionalAction, # Added in Python 3.9
)
if __name__ == '__main__':
def _main(args: list[str] | None = None) -> None:
settings_path = pathlib.Path('./settings.json')
manager = Manager(description='This is an example', epilog='goodbye!')
@ -459,14 +536,18 @@ if __name__ == '__main__':
file_config, success = manager.parse_file(settings_path)
file_namespace = manager.get_namespace(file_config)
merged_config = manager.parse_cmdline(namespace=file_namespace)
merged_config = manager.parse_cmdline(args=args, namespace=file_namespace)
merged_namespace = manager.get_namespace(merged_config)
print(f'Hello {merged_config["example"]["hello"]}') # noqa: T201
if merged_namespace.example_save:
print(f'Hello {merged_config.values["example"]["hello"]}') # noqa: T201
if merged_namespace.values.example_save:
if manager.save_file(merged_config, settings_path):
print(f'Successfully saved settings to {settings_path}') # noqa: T201
else:
print(f'Failed saving settings to a {settings_path}') # noqa: T201
if merged_namespace.example_verbose:
print(f'{merged_namespace.example_verbose=}') # noqa: T201
if merged_namespace.values.example_verbose:
print(f'{merged_namespace.values.example_verbose=}') # noqa: T201
if __name__ == '__main__':
_main()

View File

@ -18,6 +18,8 @@ classifiers =
[options]
py_modules = settngs
install_requires =
typing-extensions;python_version < '3.11'
python_requires = >=3.8
[options.packages.find]

View File

@ -1,6 +1,63 @@
from __future__ import annotations
import pytest
example: list[tuple[list[str], str, str]] = [
(
[],
'Hello world\n',
'',
),
(
['--hello', 'lordwelch'],
'Hello lordwelch\n',
'',
),
(
['--hello', 'lordwelch', '-s'],
'Hello lordwelch\nSuccessfully saved settings to settings.json\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": false\n }\n}\n',
),
(
[],
'Hello lordwelch\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": false\n }\n}\n',
),
(
['-v'],
'Hello lordwelch\nmerged_namespace.values.example_verbose=True\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": false\n }\n}\n',
),
(
['-v', '-s'],
'Hello lordwelch\nSuccessfully saved settings to settings.json\nmerged_namespace.values.example_verbose=True\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": true\n }\n}\n',
),
(
[],
'Hello lordwelch\nmerged_namespace.values.example_verbose=True\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": true\n }\n}\n',
),
(
['--no-verbose'],
'Hello lordwelch\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": true\n }\n}\n',
),
(
['--no-verbose', '-s'],
'Hello lordwelch\nSuccessfully saved settings to settings.json\n',
'{\n "example": {\n "hello": "lordwelch",\n "verbose": false\n }\n}\n',
),
(
['--hello', 'world', '--no-verbose', '-s'],
'Hello world\nSuccessfully saved settings to settings.json\n',
'{\n "example": {\n "hello": "world",\n "verbose": false\n }\n}\n',
),
(
[],
'Hello world\n',
'{\n "example": {\n "hello": "world",\n "verbose": false\n }\n}\n',
),
]
success = [
(
(

View File

@ -6,6 +6,7 @@ import json
import pytest
import settngs
from testing.settngs import example
from testing.settngs import failure
from testing.settngs import success
@ -22,6 +23,19 @@ def test_settngs_manager():
assert manager is not None and defaults is not None
def test_settngs_manager_config():
manager = settngs.Manager(
definitions=settngs.Config[settngs.Namespace](
settngs.Namespace(),
{'tst': {'test': settngs.Setting('--test', default='hello', group='tst', exclusive=False)}},
),
)
defaults = manager.defaults()
assert manager is not None and defaults is not None
assert defaults.values['tst']['test'] == 'hello'
@pytest.mark.parametrize('arguments, expected', success)
def test_setting_success(arguments, expected):
assert vars(settngs.Setting(*arguments[0], **arguments[1])) == expected
@ -39,25 +53,31 @@ def test_add_setting(settngs_manager):
def test_get_defaults(settngs_manager):
settngs_manager.add_setting('--test', default='hello')
defaults = settngs_manager.defaults()
defaults, _ = settngs_manager.defaults()
assert defaults['']['test'] == 'hello'
def test_get_namespace(settngs_manager):
settngs_manager.add_setting('--test', default='hello')
defaults = settngs_manager.get_namespace(settngs_manager.defaults())
defaults, _ = settngs_manager.get_namespace(settngs_manager.defaults())
assert defaults.test == 'hello'
def test_get_namespace_with_namespace(settngs_manager):
settngs_manager.add_setting('--test', default='hello')
defaults, _ = settngs_manager.get_namespace(argparse.Namespace(test='hello'))
assert defaults.test == 'hello'
def test_get_defaults_group(settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
defaults = settngs_manager.defaults()
defaults, _ = settngs_manager.defaults()
assert defaults['tst']['test'] == 'hello'
def test_get_namespace_group(settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
defaults = settngs_manager.get_namespace(settngs_manager.defaults())
defaults, _ = settngs_manager.get_namespace(settngs_manager.defaults())
assert defaults.tst_test == 'hello'
@ -65,8 +85,8 @@ def test_cmdline_only(settngs_manager):
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))
file_normalized, _ = settngs_manager.normalize_config({}, file=True)
cmdline_normalized, _ = settngs_manager.normalize_config({}, cmdline=True)
file_normalized, _ = settngs_manager.normalize_config(settngs_manager.defaults(), file=True)
cmdline_normalized, _ = settngs_manager.normalize_config(settngs_manager.defaults(), cmdline=True)
assert 'test' in cmdline_normalized['tst']
assert 'test2' not in cmdline_normalized['tst2']
@ -79,13 +99,14 @@ def test_normalize(settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
defaults = settngs_manager.defaults()
defaults['test'] = 'fail' # Not defined in settngs_manager
defaults.values['test'] = 'fail' # Not defined in settngs_manager
defaults_namespace = settngs_manager.get_namespace(defaults)
defaults_namespace.test = 'fail'
defaults_namespace = settngs_manager.get_namespace(settngs_manager.defaults())
defaults_namespace.values.test = 'fail'
normalized, _ = settngs_manager.normalize_config(defaults, file=True)
normalized_namespace = settngs_manager.get_namespace(settngs_manager.normalize_config(defaults, file=True)[0])
normalized_from_namespace = settngs_manager.normalize_config(defaults_namespace, file=True)
normalized_namespace, _ = settngs_manager.get_namespace(normalized_from_namespace)
assert 'test' not in normalized
assert 'tst' in normalized
@ -100,7 +121,7 @@ def test_normalize(settngs_manager):
def test_clean_config(settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello', cmdline=False))
settngs_manager.add_group('tst2', lambda parser: parser.add_setting('--test2', default='hello', file=False))
normalized = settngs_manager.defaults()
normalized, _ = settngs_manager.defaults()
normalized['tst']['test'] = 'success'
normalized['fail'] = 'fail'
@ -114,7 +135,18 @@ def test_clean_config(settngs_manager):
def test_parse_cmdline(settngs_manager, tmp_path):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello', cmdline=True))
normalized = settngs_manager.parse_cmdline(['--test', 'success'])
normalized, _ = settngs_manager.parse_cmdline(['--test', 'success'])
assert 'test' in normalized['tst']
assert normalized['tst']['test'] == 'success'
def test_parse_cmdline_with_namespace(settngs_manager, tmp_path):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello', cmdline=True))
normalized, _ = settngs_manager.parse_cmdline(
['--test', 'success'], namespace=settngs.Config({'tst': {'test': 'fail'}}, settngs_manager.definitions),
)
assert 'test' in normalized['tst']
assert normalized['tst']['test'] == 'success'
@ -128,8 +160,8 @@ def test_parse_file(settngs_manager, tmp_path):
normalized, success = settngs_manager.parse_file(settngs_file)
assert success
assert 'test' in normalized['tst']
assert normalized['tst']['test'] == 'success'
assert 'test' in normalized[0]['tst']
assert normalized[0]['tst']['test'] == 'success'
def test_parse_non_existent_file(settngs_manager, tmp_path):
@ -138,9 +170,9 @@ def test_parse_non_existent_file(settngs_manager, tmp_path):
normalized, success = settngs_manager.parse_file(settngs_file)
assert not success
assert 'test' in normalized['tst']
assert normalized['tst']['test'] == 'hello'
assert success
assert 'test' in normalized[0]['tst']
assert normalized[0]['tst']['test'] == 'hello'
def test_parse_corrupt_file(settngs_manager, tmp_path):
@ -151,54 +183,36 @@ def test_parse_corrupt_file(settngs_manager, tmp_path):
normalized, success = settngs_manager.parse_file(settngs_file)
assert not success
assert 'test' in normalized['tst']
assert normalized['tst']['test'] == 'hello'
assert 'test' in normalized[0]['tst']
assert normalized[0]['tst']['test'] == 'hello'
def test_save_file(settngs_manager, tmp_path):
settngs_file = tmp_path / 'settngs.json'
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello', cmdline=False))
normalized = settngs_manager.defaults()
normalized, _ = settngs_manager.defaults()
normalized['tst']['test'] = 'success'
success = settngs_manager.save_file(normalized, settngs_file)
normalized, success_r = settngs_manager.parse_file(settngs_file)
assert success and success_r
assert 'test' in normalized['tst']
assert normalized['tst']['test'] == 'success'
assert 'test' in normalized[0]['tst']
assert normalized[0]['tst']['test'] == 'success'
def test_save_file_not_seriazable(settngs_manager, tmp_path):
settngs_file = tmp_path / 'settngs.json'
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello', cmdline=False))
normalized = settngs_manager.defaults()
normalized, _ = settngs_manager.defaults()
normalized['tst']['test'] = {'fail'} # Sets are not serializabl
success = settngs_manager.save_file(normalized, settngs_file)
normalized, success_r = settngs_manager.parse_file(settngs_file)
assert not (success and success_r)
assert 'test' in normalized['tst']
assert normalized['tst']['test'] == 'hello'
@pytest.mark.parametrize(
'raw, raw2, expected',
[
({'tst': {'test': 'fail'}}, argparse.Namespace(tst_test='success'), 'success'),
# hello is default so is not used in raw_options_2
({'tst': {'test': 'success'}}, argparse.Namespace(tst_test='hello'), 'success'),
(argparse.Namespace(tst_test='fail'), {'tst': {'test': 'success'}}, 'success'),
(argparse.Namespace(tst_test='success'), {'tst': {'test': 'hello'}}, 'success'),
],
)
def test_normalize_merge(raw, raw2, expected, settngs_manager):
settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello'))
normalized, _ = settngs_manager.normalize_config(raw, file=True, raw_options_2=raw2)
assert normalized['tst']['test'] == expected
assert 'test' in normalized[0]['tst']
assert normalized[0]['tst']['test'] == 'hello'
def test_cli_set(settngs_manager, tmp_path):
@ -251,3 +265,15 @@ def test_cli_explicit_default(settngs_manager, tmp_path):
assert success
assert 'test' in normalized['tst']
assert normalized['tst']['test'] == 'success'
def test_example(capsys, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
settings_file = tmp_path / 'settings.json'
settings_file.touch()
for args, expected_out, expected_file in example:
settngs._main(args)
captured = capsys.readouterr()
assert captured.out == expected_out, args
assert settings_file.read_text() == expected_file, args