diff --git a/settngs/__init__.py b/settngs/__init__.py index 986c684..03868b1 100644 --- a/settngs/__init__.py +++ b/settngs/__init__.py @@ -294,6 +294,9 @@ def normalize_config( persistent: Include unknown keys in persistent groups """ + if not file and not cmdline: + raise ValueError('Invalid parameters: you must set either file or cmdline to True') + normalized: Values = {} options, definitions = config for group_name, group in definitions.items(): @@ -366,7 +369,9 @@ def defaults(definitions: Definitions) -> Config[Values]: return normalize_config(Config(Namespace(), definitions), file=True, cmdline=True) -def get_namespace(config: Config[T], defaults: bool = True, persistent: bool = True) -> Config[Namespace]: +def get_namespace( + config: Config[T], file: bool = False, cmdline: bool = False, defaults: bool = True, persistent: bool = True, +) -> Config[Namespace]: """ Returns an Namespace object with options in the form "{group_name}_{setting_name}" `options` should already be normalized. @@ -378,33 +383,36 @@ def get_namespace(config: Config[T], defaults: bool = True, persistent: bool = T persistent: Include unknown keys in persistent groups """ + if not file and not cmdline: + raise ValueError('Invalid parameters: you must set either file or cmdline to True') + if isinstance(config.values, Namespace): - options, definitions = normalize_config(config, True, True, defaults=defaults, persistent=persistent) + options, definitions = normalize_config(config, file, cmdline, defaults=defaults, persistent=persistent) else: options, definitions = config namespace = Namespace() for group_name, group in definitions.items(): + + group_options = get_options(config, group_name) if group.persistent and persistent: - group_options = get_options(config, group_name) for name, value in group_options.items(): if name in group.v: - internal_name, default = group.v[name].internal_name, group.v[name].default == value + setting_file, setting_cmdline = group.v[name].file, group.v[name].cmdline + value, default = get_option(options, group.v[name]) + internal_name = group.v[name].internal_name else: + setting_file = setting_cmdline = True internal_name, default = f'{group_name}_' + sanitize_name(name), None - if hasattr(namespace, internal_name): - raise Exception(f'Duplicate internal name: {internal_name}') - - if not default or default and defaults: + if ((setting_cmdline and cmdline) or (setting_file and file)) and (not default or (default and defaults)): setattr(namespace, internal_name, value) - else: - for setting_name, setting in group.v.items(): - if hasattr(namespace, setting.internal_name): - raise Exception(f'Duplicate internal name: {setting.internal_name}') + for setting_name, setting in group.v.items(): + if (setting.cmdline and cmdline) or (setting.file and file): value, default = get_option(options, setting) - if not default or default and defaults: + if not default or (default and defaults): + # User has set a custom value or has requested the default value setattr(namespace, setting.internal_name, value) return Config(namespace, definitions) @@ -479,7 +487,7 @@ def parse_cmdline( if isinstance(config.values, Namespace): namespace = config.values else: - namespace = get_namespace(config, defaults=False)[0] + namespace = get_namespace(config, file=True, cmdline=True, defaults=False)[0] else: namespace = config argparser = create_argparser(definitions, description, epilog) @@ -497,7 +505,7 @@ def parse_config( ) -> tuple[Config[Values], bool]: file_options, success = parse_file(definitions, config_path) cmdline_options = parse_cmdline( - definitions, description, epilog, args, get_namespace(file_options, defaults=False), + definitions, description, epilog, args, get_namespace(file_options, file=True, cmdline=True, defaults=False), ) final_options = normalize_config(cmdline_options, file=True, cmdline=True) @@ -605,19 +613,41 @@ class Manager: ) @overload - def get_namespace(self, options: Values, defaults: bool = True) -> Namespace: + def get_namespace( + self, + options: Values, + file: bool = False, + cmdline: bool = False, + defaults: bool = True, + persistent: bool = True, + ) -> Namespace: ... @overload - def get_namespace(self, options: Config[Values], defaults: bool = True) -> Config[Namespace]: + def get_namespace( + self, + options: Config[Values], + file: bool = False, + cmdline: bool = False, + defaults: bool = True, + persistent: bool = True, + ) -> Config[Namespace]: ... - def get_namespace(self, options: Values | Config[Values], defaults: bool = True) -> Config[Namespace] | Namespace: + def get_namespace( + self, + options: Values | Config[Values], + file: bool = False, + cmdline: bool = False, + defaults: bool = True, + persistent: bool = True, + ) -> Config[Namespace] | Namespace: if isinstance(options, Config): self.definitions = options[1] - return get_namespace(options, defaults=defaults) + return get_namespace(options, file=file, cmdline=cmdline, defaults=defaults, persistent=persistent) else: - return get_namespace(Config(options, self.definitions), defaults=defaults) + config = Config(options, self.definitions) + return get_namespace(config, file=file, cmdline=cmdline, defaults=defaults, persistent=persistent) def parse_file(self, filename: pathlib.Path) -> tuple[Config[Values], bool]: return parse_file(filename=filename, definitions=self.definitions) @@ -668,10 +698,10 @@ def _main(args: list[str] | None = None) -> None: manager.add_persistent_group('persistent', persistent) file_config, success = manager.parse_file(settings_path) - file_namespace = manager.get_namespace(file_config) + file_namespace = manager.get_namespace(file_config, file=True, cmdline=True) merged_config = manager.parse_cmdline(args=args, namespace=file_namespace) - merged_namespace = manager.get_namespace(merged_config) + merged_namespace = manager.get_namespace(merged_config, file=True, cmdline=True) print(f'Hello {merged_config.values["example"]["hello"]}') # noqa: T201 if merged_namespace.values.example_save: diff --git a/setup.cfg b/setup.cfg index 0548812..cf49039 100644 --- a/setup.cfg +++ b/setup.cfg @@ -78,6 +78,8 @@ max_line_length = 120 [flake8] extend-ignore = E501, A003 max_line_length = 120 +per-file-ignores = + *_test.py: LN001 [coverage:run] plugins = covdefaults diff --git a/tests/settngs_test.py b/tests/settngs_test.py index ca292ed..3d1c503 100644 --- a/tests/settngs_test.py +++ b/tests/settngs_test.py @@ -2,7 +2,9 @@ from __future__ import annotations import argparse import json +import pathlib from collections import defaultdict +from typing import Generator import pytest @@ -14,7 +16,7 @@ from testing.settngs import success @pytest.fixture -def settngs_manager(): +def settngs_manager() -> Generator[settngs.Manager, None, None]: manager = settngs.Manager() yield manager @@ -53,94 +55,170 @@ def test_add_setting(settngs_manager): assert settngs_manager.add_setting('--test') is None -def test_get_defaults(settngs_manager): - settngs_manager.add_setting('--test', default='hello') - defaults, _ = settngs_manager.defaults() - assert defaults['']['test'] == 'hello' +class TestValues: + + def test_get_defaults(self, settngs_manager): + settngs_manager.add_setting('--test', default='hello') + defaults, _ = settngs_manager.defaults() + assert defaults['']['test'] == 'hello' + + def test_get_defaults_group(self, settngs_manager): + settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello')) + defaults, _ = settngs_manager.defaults() + assert defaults['tst']['test'] == 'hello' + + 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('tst2', lambda parser: parser.add_setting('--test2', default='hello', cmdline=False)) + + file_normalized, _ = settngs_manager.normalize_config(settngs_manager.defaults(), file=True) + cmdline_normalized, _ = settngs_manager.normalize_config(settngs_manager.defaults(), cmdline=True) + + assert 'test' not in file_normalized['tst'] # cmdline option not in normalized config + assert 'test2' in file_normalized['tst2'] # file option in normalized config + + assert 'test' in cmdline_normalized['tst'] # cmdline option in normalized config + assert 'test2' not in cmdline_normalized['tst2'] # file option not in normalized config + + def test_cmdline_only_persistent_group(self, settngs_manager): + settngs_manager.add_persistent_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(settngs_manager.defaults(), file=True) + cmdline_normalized, _ = settngs_manager.normalize_config(settngs_manager.defaults(), cmdline=True) + + assert 'test' not in file_normalized['tst'] + assert 'test2' in file_normalized['tst2'] + + assert 'test' in cmdline_normalized['tst'] + assert 'test2' not in cmdline_normalized['tst2'] + + def test_normalize_defaults(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', 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, defaults=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, defaults=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')) + + defaults = settngs_manager.defaults() + defaults.values['test'] = 'fail' # Not defined in settngs_manager, should be removed + defaults.values['persistent']['hello'] = 'success' # Not defined in settngs_manager, should stay + + normalized, _ = settngs_manager.normalize_config(defaults, file=True) + + assert 'test' not in normalized + assert 'tst' in normalized + assert 'test' in normalized['tst'] + assert normalized['tst']['test'] == 'hello' + assert normalized['persistent']['hello'] == 'success' + assert normalized['persistent']['world'] == 'world' + + +class TestNamespace: + + def test_get_defaults(self, settngs_manager): + settngs_manager.add_setting('--test', default='hello') + defaults, _ = settngs_manager.get_namespace(settngs_manager.defaults(), file=True, cmdline=True) + assert defaults.test == 'hello' + + def test_get_defaults_group(self, settngs_manager): + settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello')) + defaults, _ = settngs_manager.get_namespace(settngs_manager.defaults(), file=True, cmdline=True) + assert defaults.tst_test == 'hello' + + 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('tst2', lambda parser: parser.add_setting('--test2', default='hello', cmdline=False)) + + file_normalized, _ = settngs_manager.get_namespace(settngs_manager.normalize_config(settngs_manager.defaults(), file=True), file=True) + cmdline_normalized, _ = settngs_manager.get_namespace(settngs_manager.normalize_config(settngs_manager.defaults(), cmdline=True), cmdline=True) + + assert 'tst_test' not in file_normalized.__dict__ + assert 'tst2_test2' in file_normalized.__dict__ + + assert 'tst_test' in cmdline_normalized.__dict__ + assert 'tst2_test2' not in cmdline_normalized.__dict__ + + def test_cmdline_only_persistent_group(self, settngs_manager): + settngs_manager.add_persistent_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.get_namespace(settngs_manager.normalize_config(settngs_manager.defaults(), file=True), file=True) + cmdline_normalized, _ = settngs_manager.get_namespace(settngs_manager.normalize_config(settngs_manager.defaults(), cmdline=True), cmdline=True) + + assert 'tst_test' not in file_normalized.__dict__ + assert 'tst2_test2' in file_normalized.__dict__ + + assert 'tst_test' in cmdline_normalized.__dict__ + assert 'tst2_test2' not in cmdline_normalized.__dict__ + + def test_normalize_defaults(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', 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, defaults=False), file=True, defaults=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, defaults=False), file=True, defaults=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')) + + defaults = settngs_manager.get_namespace(settngs_manager.defaults(), file=True, cmdline=True) + defaults.values.test = 'fail' # Not defined in settngs_manager, should be removed + defaults.values.persistent_hello = 'success' # Not defined in settngs_manager, should stay + + normalized, _ = settngs_manager.get_namespace(settngs_manager.normalize_config(defaults, file=True), file=True) + + assert not hasattr(normalized, 'test') + assert hasattr(normalized, 'tst_test') + assert normalized.tst_test == 'hello' + assert normalized.persistent_hello == 'success' + assert normalized.persistent_world == 'world' def test_get_defaults_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(), file=True, cmdline=True) 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='success')) + defaults, _ = settngs_manager.get_namespace(argparse.Namespace(test='success'), file=True) assert defaults.test == 'success' -def test_get_defaults_group(settngs_manager): - settngs_manager.add_group('tst', lambda parser: parser.add_setting('--test', default='hello')) - 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(), file=True) assert defaults.tst_test == 'hello' -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(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'] - - assert 'test' not in file_normalized['tst'] - assert 'test2' in file_normalized['tst2'] - - -def test_cmdline_only_persistent_group(settngs_manager): - settngs_manager.add_persistent_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(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'] - - assert 'test' not in file_normalized['tst'] - assert 'test2' in file_normalized['tst2'] - - -def test_normalize(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')) - - defaults = settngs_manager.defaults() - defaults.values['test'] = 'fail' # Not defined in settngs_manager, should be removed - defaults.values['persistent']['hello'] = 'success' # Not defined in settngs_manager, should stay - - defaults_namespace = settngs_manager.get_namespace(settngs_manager.defaults()) - defaults_namespace.values.test = 'fail' # Not defined in settngs_manager, should be removed - defaults_namespace.values.persistent_hello = 'success' # Not defined in settngs_manager, should stay - - normalized, _ = settngs_manager.normalize_config(defaults, file=True) - 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 - assert 'test' in normalized['tst'] - assert normalized['tst']['test'] == 'hello' - assert normalized['persistent']['hello'] == 'success' - assert normalized['persistent']['world'] == 'world' - - assert not hasattr(normalized_namespace, 'test') - assert hasattr(normalized_namespace, 'tst_test') - assert normalized_namespace.tst_test == 'hello' - assert normalized_namespace.persistent_hello == 'success' - assert normalized_namespace.persistent_world == 'world' - - 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)) @@ -331,7 +409,7 @@ def test_adding_to_existing_group(settngs_manager, tmp_path): assert default_to_regular(settngs_manager.definitions) == default_to_regular(settngs_manager2.definitions) -def test_adding_to_existing_persistent_group(settngs_manager, tmp_path): +def test_adding_to_existing_persistent_group(settngs_manager: settngs.Manager, tmp_path: pathlib.Path) -> None: def default_to_regular(d): if isinstance(d, defaultdict): d = {k: default_to_regular(v) for k, v in d.items()}