From 916c89e0a00ae5fce42e9fc432094fbec5f8ac0a Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Sun, 5 Nov 2023 22:58:17 +0000 Subject: [PATCH 01/28] feat: Support file based config --- src/arg_init/__init__.py | 7 +- src/arg_init/_arg.py | 83 +++++++--------------- src/arg_init/_arg_defaults.py | 10 ++- src/arg_init/_arg_init.py | 106 ++++++++++++++++++++--------- src/arg_init/_class_arg_init.py | 29 ++++---- src/arg_init/_config.py | 64 +++++++++++++++++ src/arg_init/_function_arg_init.py | 18 ++--- src/arg_init/_priority.py | 22 ++++++ src/arg_init/_values.py | 20 ++++++ src/arg_init/_version.py | 2 +- 10 files changed, 237 insertions(+), 124 deletions(-) create mode 100644 src/arg_init/_config.py create mode 100644 src/arg_init/_priority.py create mode 100644 src/arg_init/_values.py diff --git a/src/arg_init/__init__.py b/src/arg_init/__init__.py index e776a6a..b648354 100644 --- a/src/arg_init/__init__.py +++ b/src/arg_init/__init__.py @@ -1,17 +1,16 @@ # pylint: disable=missing-module-docstring -from ._arg_init import ARG_PRIORITY, ENV_PRIORITY from ._class_arg_init import ClassArgInit from ._arg_defaults import ArgDefaults from ._function_arg_init import FunctionArgInit -# from ._exceptions import AttributeExistsError +from ._priority import Priority, DEFAULT_PRIORITY, ARG_PRIORITY # External API __all__ = [ "ClassArgInit", "FunctionArgInit", - # "AttributeExistsError", "ArgDefaults", + "Priority", + "DEFAULT_PRIORITY", "ARG_PRIORITY", - "ENV_PRIORITY", ] diff --git a/src/arg_init/_arg.py b/src/arg_init/_arg.py index ec3cc41..c7a3c5a 100644 --- a/src/arg_init/_arg.py +++ b/src/arg_init/_arg.py @@ -2,39 +2,32 @@ Data Class used to customise ArgInit behaviour """ -from dataclasses import dataclass -from os import environ import logging +from ._priority import Priority +# from ._values import Values logger = logging.getLogger(__name__) -@dataclass -class Values: - arg: any = None - env: any = None - default: any = None - - def __repr__(self): - return f"" - - class Arg: """Class to represent argument attributes.""" - ARG_PRIORITY = "arg_priority" - ENV_PRIORITY = "env_priority" - DEFAULT_PRIORITY_SYSTEM = ENV_PRIORITY + _mapping = { + Priority.CONFIG: "config", + Priority.ENV: "env", + Priority.ARG: "arg", + Priority.DEFAULT: "default", + } def __init__( self, name: str, - env_name: str = None, + alt_name: str | None = None, values=None, ): self._name = name - self._env_name = env_name + self._alt_name = alt_name self._values = values self._value = None @@ -45,7 +38,7 @@ def __eq__(self, other): def _data(self): return [ f"name={self.name}", - f"env_name={self.env_name}", + f"alt_name={self.alt_name}", f"values={self.values}", f"value={self.value}", ] @@ -67,56 +60,28 @@ def value(self): return self._value @property - def env_name(self): + def alt_name(self): """env attribute.""" - return self._env_name + return self._alt_name @property def values(self): - """default attribute.""" + """Values to use when resolving Arg.""" return self._values - def resolve(self, priority=DEFAULT_PRIORITY_SYSTEM): + def resolve(self, name, priority_order): """ Resolve the value Arg using the selected priority system. """ logger.debug("Resolving value for %s", repr(self)) - if priority == self.ARG_PRIORITY: - value = self._resolve_arg_priority() - if priority == self.ENV_PRIORITY: - value = self._resolve_env_priority() - self._value = value + for priority in priority_order: + logger.debug("Checking %s value", priority) + value = self._get_value(priority) + if value: + logger.debug("Resolved %s = %s from %s", name, value, priority) + self._value = value + break return self - def _if_use_arg_value(self) -> bool: - if self.values.arg: - logger.debug("Using arg: value=%s", self.values.arg) - return True - return False - - def _if_use_env_value(self) -> bool: - if self.values.env: - logger.debug("Using env: value=%s", self.values.env) - return True - return False - - def _log_use_default_value(self): - logger.debug("Using default: value=%s", self.values.default) - - def _resolve_arg_priority(self): - logger.debug("Resolving using arg priority") - if self._if_use_arg_value(): - return self.values.arg - if self._if_use_env_value(): - return self.values.env - self._log_use_default_value() - return self.values.default - - def _resolve_env_priority(self): - logger.debug("Resolving using env priority") - if self._if_use_env_value(): - return self.values.env - if self._if_use_arg_value(): - return self.values.arg - self._log_use_default_value() - return self.values.default + def _get_value(self, priority): + return getattr(self._values, self._mapping[priority]) diff --git a/src/arg_init/_arg_defaults.py b/src/arg_init/_arg_defaults.py index 8ef90de..e52b2b9 100644 --- a/src/arg_init/_arg_defaults.py +++ b/src/arg_init/_arg_defaults.py @@ -4,7 +4,7 @@ """ from dataclasses import dataclass - +from typing import Any @dataclass class ArgDefaults: @@ -14,14 +14,12 @@ class ArgDefaults: """ name: str - default_value: any = None - env_name: str = None - disable_env: bool = False + default_value: Any = None + alt_name: str | None = None def __repr__(self) -> str: return ( f"" + f"alt_name={self.alt_name})>" ) diff --git a/src/arg_init/_arg_init.py b/src/arg_init/_arg_init.py index 76c1b79..2da1f05 100644 --- a/src/arg_init/_arg_init.py +++ b/src/arg_init/_arg_init.py @@ -3,44 +3,60 @@ processed attribute values. """ from abc import ABC, abstractmethod +from inspect import stack from os import environ import logging from box import Box -from ._arg import Values, Arg +from ._arg import Arg +from ._config import read_config +from ._priority import Priority +from ._values import Values logger = logging.getLogger(__name__) - -ARG_PRIORITY = "arg_priority" -ENV_PRIORITY = "env_priority" - - class ArgInit(ABC): """ Class to resolve arguments of a function from passed in values, environment variables or default values. """ - DEFAULT_PRIORITY_SYSTEM = ENV_PRIORITY - STACK_LEVEL_OFFSET = 1 # The calling frame is 1 layer up + STACK_LEVEL_OFFSET = 2 # The calling frame is 2 layers up def __init__( self, - priority: bool = ENV_PRIORITY, - env_prefix: str | None = None, + priority, + env_prefix: str | None, + use_kwargs, + defaults, + config, + **kwargs, # pylint: disable=unused-argument ): self._env_prefix = env_prefix self._priority = priority self._args = Box() + calling_stack = stack()[self.STACK_LEVEL_OFFSET] + calling_stack.frame.f_locals["arg1"] = 2 + name = self._get_name(calling_stack) + self._config = read_config(config) if Priority.CONFIG in priority else {} + if self._config: + logger.debug("Section id in config file: %s", name) + arg_config = self.config.get(name, {}) + self._init_args(name, calling_stack, use_kwargs, defaults, arg_config) + self._post_init(calling_stack) @property def args(self) -> Box: """Return the processed arguments.""" return self._args + @property + def config(self): + """Return the config data""" + return self._config + @abstractmethod def _get_arguments(self, frame, use_kwargs): """ @@ -49,16 +65,32 @@ def _get_arguments(self, frame, use_kwargs): """ raise RuntimeError() # pragma no cover + @abstractmethod + def _get_name(self, calling_stack): + """ + Return the name of the item having arguments initialised. + """ + raise RuntimeError() # pragma no cover + + # @abstractmethod + def _post_init(self, calling_stack): + """ + Class specific post initialisation actions. + This can optionally be overridden by derived classes + """ + def _init_args( self, + name, calling_stack, use_kwargs: bool, defaults, + config, ) -> None: """Resolve argument values.""" - logger.debug("Creating arguments for function: %s", calling_stack.function) + logger.debug("Creating arguments for: %s", name) arguments = self._get_arguments(calling_stack.frame, use_kwargs) - self._make_args(arguments, defaults) + self._make_args(arguments, defaults, config) def _get_kwargs(self, arginfo, use_kwargs) -> dict: """ @@ -71,44 +103,52 @@ def _get_kwargs(self, arginfo, use_kwargs) -> dict: return dict(arginfo.locals[keywords].items()) return {} - def _make_args(self, arguments, defaults) -> None: + def _make_args(self, arguments, defaults, config) -> None: for name, value in arguments.items(): arg_defaults = self._get_arg_defaults(name, defaults) + alt_name = self._get_alt_name(name, arg_defaults) env_name = self._get_env_name(name, arg_defaults) + default_value = self._get_default_value(arg_defaults) values = Values( - arg=value, env=self._get_env_value(env_name), default=default_value + arg=value, + env=self._get_env_value(env_name), + config=config.get(alt_name), + default=default_value ) - self._args[name] = Arg(name, env_name, values).resolve(self._priority) + self._args[name] = Arg(name, alt_name, values).resolve(name, self._priority) def _get_arg_defaults(self, name, defaults): - for arg_defaults in defaults: - if arg_defaults.name == name: - return arg_defaults + """Check if any defaults exist for the named arg.""" + if defaults: + for arg_defaults in defaults: + if arg_defaults.name == name: + return arg_defaults return None + @staticmethod + def _get_alt_name(name, arg_defaults): + """Determine the name to use for the config.""" + if arg_defaults and arg_defaults.alt_name: + return arg_defaults.alt_name + return name + def _get_env_name(self, name, arg_defaults): """Determine the name to use for the env.""" - if arg_defaults: - if arg_defaults.disable_env: - return None - if arg_defaults.env_name: - return arg_defaults.env_name.upper() + if arg_defaults and arg_defaults.alt_name: + return arg_defaults.alt_name env_parts = [item for item in (self._env_prefix, name) if item] return "_".join(env_parts).upper() @staticmethod - def _get_env_value(env_name) -> str: + def _get_env_value(env_name) -> str | None: """Read the env value from environ.""" - if env_name: - logger.debug("Searching for env: %s", env_name) - if env_name in environ: - value = environ[env_name] - logger.debug("Env found: %s=%s", env_name, value) - return value - logger.debug("Env not set") - return None - logger.debug("Env disabled") + logger.debug("Searching for env: %s", env_name) + if env_name in environ: + value = environ[env_name] + logger.debug("Env found: %s=%s", env_name, value) + return value + logger.debug("Env not set") return None @staticmethod diff --git a/src/arg_init/_class_arg_init.py b/src/arg_init/_class_arg_init.py index 684edbb..b4a8b5e 100644 --- a/src/arg_init/_class_arg_init.py +++ b/src/arg_init/_class_arg_init.py @@ -2,10 +2,11 @@ Class to initialise Argument Values for a Class Method """ -from inspect import stack, getargvalues +from inspect import getargvalues import logging -from ._arg_init import ArgInit, ENV_PRIORITY +from ._arg_init import ArgInit +from ._priority import DEFAULT_PRIORITY logger = logging.getLogger(__name__) @@ -20,24 +21,23 @@ class ClassArgInit(ArgInit): def __init__( self, - priority=ENV_PRIORITY, + priority=DEFAULT_PRIORITY, env_prefix=None, use_kwargs=False, set_attrs=True, protect_attrs=True, defaults=None, + config_name="config", **kwargs, ): - super().__init__(priority, env_prefix, **kwargs) - if defaults is None: - defaults = [] - self._set_attrs = set_attrs self._protect_attrs = protect_attrs - calling_stack = stack()[self.STACK_LEVEL_OFFSET] - self._init_args(calling_stack, use_kwargs, defaults) + super().__init__(priority, env_prefix, use_kwargs, defaults, config_name, **kwargs) + + def _post_init(self, calling_stack): + """Class specific post init behaviour.""" class_instance = self._get_class_instance(calling_stack.frame) - self._set_class_attrs(class_instance) + self._set_class_arg_attrs(class_instance) def _get_arguments(self, frame, use_kwargs): """ @@ -55,7 +55,7 @@ def _get_arguments(self, frame, use_kwargs): args.update(self._get_kwargs(arginfo, use_kwargs)) return args - def _set_class_attrs(self, class_ref): + def _set_class_arg_attrs(self, class_ref): """Set attributes for the class object.""" if self._set_attrs: logger.debug("Setting class attributes") @@ -74,7 +74,8 @@ def _set_attr(self, class_instance, name, value): logger.debug(" %s = %s", name, value) setattr(class_instance, name, value) - def _get_class_instance(self, frame): + @staticmethod + def _get_class_instance(frame): """ Return the value of the 1st argument from the calling function. This should be the class instance. @@ -82,3 +83,7 @@ def _get_class_instance(self, frame): arginfo = getargvalues(frame) first_arg = arginfo.args[0] return arginfo.locals.get(first_arg) + + def _get_name(self, calling_stack): + """Return the name of the current class instance.""" + return calling_stack.frame.f_locals["self"].__class__.__name__ diff --git a/src/arg_init/_config.py b/src/arg_init/_config.py new file mode 100644 index 0000000..fb01f1b --- /dev/null +++ b/src/arg_init/_config.py @@ -0,0 +1,64 @@ +""" +Helper module to read a config file + +Supported formats are: +- JSON +- TOML +- YAML +""" + +from pathlib import Path +from json import load as json_load +from tomllib import load as toml_load +import logging + +from yaml import safe_load as yaml_safe_load + + +logger = logging.getLogger(__name__) +FORMATS = ["yaml", "toml", "json"] + + +def _yaml_loader(): + return yaml_safe_load + +def _json_loader(): + return json_load + +def _toml_loader(): + return toml_load + +def _get_loader(path): + match path.suffix: + case ".json": + return _json_loader() + case ".yaml": + return _yaml_loader() + case ".toml": + return _toml_loader() + case _: + raise RuntimeError(f"Unsupported file format: {path.suffix}") + +def _find_config(file): + if isinstance(file, Path): + file.resolve() + logger.debug("Using named config file: %s", file.resolve()) + if not file.exists(): + raise FileNotFoundError(file) + return file + for ext in FORMATS: + path = Path(f"{file}.{ext}").resolve() + logger.debug("Searching for config: %s", path) + if path.exists(): + logger.debug("config found: %s", path) + return path + return None + +def read_config(file="config"): + """Read a config file.""" + path = _find_config(file) + if path: + loader = _get_loader(path) + with open(path, "rb") as f: + return loader(f) + return {} diff --git a/src/arg_init/_function_arg_init.py b/src/arg_init/_function_arg_init.py index b93b4b2..c830e6c 100644 --- a/src/arg_init/_function_arg_init.py +++ b/src/arg_init/_function_arg_init.py @@ -3,11 +3,11 @@ """ -from inspect import stack, getargvalues +from inspect import getargvalues import logging -from ._arg_init import ArgInit, ENV_PRIORITY - +from ._arg_init import ArgInit +from ._priority import DEFAULT_PRIORITY logger = logging.getLogger(__name__) @@ -19,17 +19,14 @@ class FunctionArgInit(ArgInit): def __init__( self, - priority=ENV_PRIORITY, + priority=DEFAULT_PRIORITY, env_prefix=None, use_kwargs=False, defaults=None, + config_name="config", **kwargs, ): - super().__init__(priority, env_prefix, **kwargs) - if defaults is None: - defaults = [] - calling_stack = stack()[self.STACK_LEVEL_OFFSET] - self._init_args(calling_stack, use_kwargs, defaults) + super().__init__(priority, env_prefix, use_kwargs, defaults, config_name, **kwargs) def _get_arguments(self, frame, use_kwargs): """ @@ -40,3 +37,6 @@ def _get_arguments(self, frame, use_kwargs): args = {arg: arginfo.locals.get(arg) for arg in arginfo.args} args.update(self._get_kwargs(arginfo, use_kwargs)) return args + + def _get_name(self, calling_stack): + return calling_stack.function diff --git a/src/arg_init/_priority.py b/src/arg_init/_priority.py new file mode 100644 index 0000000..ecf2767 --- /dev/null +++ b/src/arg_init/_priority.py @@ -0,0 +1,22 @@ +""" +Enum to represent priorities supported by arg_init +""" + +from enum import Enum + + +class Priority(Enum): + """Argument resolution priority""" + CONFIG = 1 + ENV = 2 + ARG = 3 + DEFAULT = 4 + +# Pre-defined priorities +# The user is free to create and use any priority order using the available options +# defined in Priority +CONFIG_PRIORITY = (Priority.CONFIG, Priority.ENV, Priority.ARG, Priority.DEFAULT) +ENV_PRIORITY = (Priority.ENV, Priority.CONFIG, Priority.ARG, Priority.DEFAULT) +ARG_PRIORITY = (Priority.ARG, Priority.CONFIG, Priority.ENV, Priority.DEFAULT) + +DEFAULT_PRIORITY = CONFIG_PRIORITY diff --git a/src/arg_init/_values.py b/src/arg_init/_values.py new file mode 100644 index 0000000..812f383 --- /dev/null +++ b/src/arg_init/_values.py @@ -0,0 +1,20 @@ +""" +Class to represent values used to resolve an argument. +""" + +from dataclasses import dataclass +from typing import Any + +@dataclass +class Values: + """ + Possible values an argument could be resolved from + """ + + arg: Any = None + env: Any = None + config: Any = None + default: Any = None + + def __repr__(self): + return f"" diff --git a/src/arg_init/_version.py b/src/arg_init/_version.py index 59a3e19..cb54b71 100644 --- a/src/arg_init/_version.py +++ b/src/arg_init/_version.py @@ -2,4 +2,4 @@ arg_init package version """ -__version__ = "0.0.5" +__version__ = "0.0.6" From 812bd576951d53afe6a0a8b42f72c331898ee9bb Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Sun, 5 Nov 2023 22:58:46 +0000 Subject: [PATCH 02/28] test: Update test cases --- tests/test_arg_defaults.py | 24 +------- tests/test_arg_priority.py | 55 ++++++++++++------ tests/test_class_arg_init.py | 11 ++-- tests/test_env_priority.py | 14 +++-- tests/test_env_variants.py | 6 +- tests/test_file_configs.py | 109 +++++++++++++++++++++++++++++++++++ tests/test_kwargs.py | 6 +- tests/test_print.py | 12 ++-- 8 files changed, 174 insertions(+), 63 deletions(-) create mode 100644 tests/test_file_configs.py diff --git a/tests/test_arg_defaults.py b/tests/test_arg_defaults.py index 330d97a..c235b6b 100644 --- a/tests/test_arg_defaults.py +++ b/tests/test_arg_defaults.py @@ -41,27 +41,9 @@ class Test: def __init__(self, arg1=None): name = "arg1" - env_name = "ENV1" - defaults = [ArgDefaults(name=name, env_name=env_name)] + alt_name = "ENV1" + defaults = [ArgDefaults(name=name, alt_name=alt_name)] arg_init = ClassArgInit(defaults=defaults) - assert arg_init.args.arg1.env_name == env_name - - Test() - - def test_disable_env(self): - """ - Test disable_env=True sets env_name to None - """ - - class Test: - """Test Class""" - - def __init__(self, arg1=None): - name = "arg1" - env_name = "ENV1" - disable_env = True - defaults = [ArgDefaults(name=name, env_name=env_name, disable_env=disable_env)] - arg_init = ClassArgInit(defaults=defaults) - assert arg_init.args.arg1.env_name is None + assert arg_init.args.arg1.alt_name == alt_name Test() diff --git a/tests/test_arg_priority.py b/tests/test_arg_priority.py index e8f2942..0db95d7 100644 --- a/tests/test_arg_priority.py +++ b/tests/test_arg_priority.py @@ -8,10 +8,12 @@ from arg_init import ArgDefaults from arg_init import FunctionArgInit -from arg_init import ARG_PRIORITY +from arg_init import Priority Expected = namedtuple('Expected', 'key value') +# Custom priority order +PRIORITY_ORDER = (Priority.ARG, Priority.CONFIG, Priority.ENV, Priority.DEFAULT) class TestArgPriority: """ @@ -19,28 +21,31 @@ class TestArgPriority: """ @pytest.mark.parametrize( - "prefix, arg_value, envs, defaults, expected", + "prefix, arg_value, envs, config, defaults, expected", [ # Priority order - (None, "arg1_value", {"ARG1": "env1_value"}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "arg1_value")), - (None, None, {"ARG1": "env1_value"}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "env1_value")), - (None, None, None, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "default")), - (None, None, None, None, Expected("arg1", None)), + (None, "arg1_value", {"ARG1": "env1_value"}, {"test": {"arg1": "config1_value"}}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "arg1_value")), + (None, None, {"ARG1": "env1_value"}, {"test": {"arg1": "config1_value"}}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "config1_value")), + (None, None, {"ARG1": "env1_value"}, {}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "env1_value")), + (None, None, None, {}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "default")), + (None, None, None, {}, None, Expected("arg1", None)), ], ) - def test_matrix(self, prefix, arg_value, envs, defaults, expected): + def test_matrix(self, prefix, arg_value, envs, config, defaults, expected, fs): """ Priority Order 1. All defined - Arg is used + 2. Config, env and default defined - Config is used 2. Env and default defined - Env is used 3. Default is defined - Default is used 4. Nothing defined - None is used """ def test(arg1): # pylint: disable=unused-argument - args = FunctionArgInit(env_prefix=prefix, defaults=defaults, priority=ARG_PRIORITY).args + args = FunctionArgInit(env_prefix=prefix, defaults=defaults, priority=PRIORITY_ORDER).args assert args[expected.key] == expected.value + fs.create_file("config.yaml", contents=str(config)) with pytest.MonkeyPatch.context() as mp: if envs: for env, value in envs.items(): @@ -52,7 +57,7 @@ def test_multiple_args(self): Test multiple arg values """ def test(arg1, arg2): # pylint: disable=unused-argument - args = FunctionArgInit(priority=ARG_PRIORITY).args + args = FunctionArgInit(priority=PRIORITY_ORDER).args assert args["arg1"] == arg1_value assert args["arg2"] == arg2_value @@ -60,13 +65,29 @@ def test(arg1, arg2): # pylint: disable=unused-argument arg2_value = "arg2_value" test(arg1_value, arg2_value) + def _test_multiple_config_args(self, fs): + """ + Test multiple args defined in a config file + """ + def test(arg1=None, arg2=None): # pylint: disable=unused-argument + args = FunctionArgInit(priority=PRIORITY_ORDER).args + assert args[arg1] == config1_value + assert args[arg2] == config2_value + + arg1 = "arg1" + config1_value = "config1_value" + arg2 = "arg2" + config2_value = "config2_value" + config = {"test": {arg1: config1_value, arg2: config2_value}} + fs.create_file("config.yaml", contents=str(config)) + test() def test_multiple_envs(self): """ Test a multiple args from envs """ - def test(arg1, arg2): # pylint: disable=unused-argument - args = FunctionArgInit(priority=ARG_PRIORITY).args + def test(arg1=None, arg2=None): # pylint: disable=unused-argument + args = FunctionArgInit(priority=PRIORITY_ORDER).args assert args["arg1"] == env1_value assert args["arg2"] == env2_value @@ -77,7 +98,7 @@ def test(arg1, arg2): # pylint: disable=unused-argument with pytest.MonkeyPatch.context() as mp: mp.setenv(env1, env1_value) mp.setenv(env2, env2_value) - test(None, None) + test() def test_multiple_mixed(self): """ @@ -87,7 +108,7 @@ def test_multiple_mixed(self): arg3 - eng - arg = None """ def test(arg1, arg2, arg3): # pylint: disable=unused-argument - args = FunctionArgInit(priority=ARG_PRIORITY).args + args = FunctionArgInit(priority=PRIORITY_ORDER).args assert args["arg1"] == arg1_value assert args["arg2"] == arg2_value assert args["arg3"] == env3_value @@ -95,9 +116,9 @@ def test(arg1, arg2, arg3): # pylint: disable=unused-argument env1 = "ARG1" env1_value = "arg1_env" env3 = "ARG3" - env3_value = "arg3_env" - arg1_value = "arg1_arg" - arg2_value = "arg1_arg" + env3_value = "env3_value" + arg1_value = "arg1_value" + arg2_value = "arg2_value" arg3_value = None with pytest.MonkeyPatch.context() as mp: mp.setenv(env1, env1_value) @@ -109,7 +130,7 @@ def test_env_prefix(self): Test using env_prefix does not affect results """ def test(arg1): # pylint: disable=unused-argument - args = FunctionArgInit(env_prefix="prefix", priority=ARG_PRIORITY).args + args = FunctionArgInit(env_prefix="prefix", priority=PRIORITY_ORDER).args assert args["arg1"] == arg1_value arg1_value = "arg1_value" diff --git a/tests/test_class_arg_init.py b/tests/test_class_arg_init.py index 4f44e7b..773f68a 100644 --- a/tests/test_class_arg_init.py +++ b/tests/test_class_arg_init.py @@ -4,6 +4,7 @@ from collections import namedtuple +from pyfakefs.fake_filesystem_unittest import patchfs import pytest from arg_init import ClassArgInit @@ -17,7 +18,7 @@ class TestClassArgInit: Test class attributes are initialised """ - def test_class(self): + def test_class(self, fs): """ Test ArgInit on a class method """ @@ -31,7 +32,7 @@ def __init__(self, arg1): Test(arg1_value) - def test_protect_attr_false_sets_attr(self): + def test_protect_attr_false_sets_attr(self, fs): """ Test ArgInit on a class method """ @@ -45,7 +46,7 @@ def __init__(self, arg1): Test(arg1_value) - def test_exception_raised_if_protected_attr_exists(self): + def test_exception_raised_if_protected_attr_exists(self, fs): """ Test exception raised if attempting to set an attribute that already exists """ @@ -59,7 +60,7 @@ def __init__(self, arg1=None): Test() - def test_exception_raised_if_non_protected_attr_exists(self): + def test_exception_raised_if_non_protected_attr_exists(self, fs): """ Test exception raised if attempting to set an attribute that already exists. Verify "_" is not used as a prefix to attr when protect_args=False. @@ -74,7 +75,7 @@ def __init__(self, arg1=None): Test() - def test_set_attrs_false_does_not_set_attrs(self): + def test_set_attrs_false_does_not_set_attrs(self, fs): """ Test exception raised if attempting to set an attribute that already exists. Verify "_" is not used as a prefix to attr when protect_args=False. diff --git a/tests/test_env_priority.py b/tests/test_env_priority.py index d07e53a..f282f68 100644 --- a/tests/test_env_priority.py +++ b/tests/test_env_priority.py @@ -19,15 +19,16 @@ class TestEnvPriority: """ @pytest.mark.parametrize( - "prefix, arg_value, envs, defaults, expected", + "prefix, arg_value, envs, config, defaults, expected", [ - (None, "arg1_value", {"ARG1": "env1_value"}, None, Expected("arg1", "env1_value")), - (None, "arg1_value", None, None, Expected("arg1", "env1_value")), - (None, None, None, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "default")), - (None, None, None, None, Expected("arg1", None)), + (None, "arg1_value", {"ARG1": "env1_value"}, {"test": {"arg1": "config1_value"}}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "config1_value")), + (None, "arg1_value", {"ARG1": "env1_value"}, {}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "env1_value")), + (None, "arg1_value", None, {}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "env1_value")), + (None, None, None, {}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "default")), + (None, None, None, {}, None, Expected("arg1", None)), ], ) - def test_priority(self, prefix, arg_value, envs, defaults, expected): + def test_priority(self, prefix, arg_value, envs, config, defaults, expected, fs): """ Priority Order 1. All defined - Env is used @@ -39,6 +40,7 @@ def test(arg1): args = FunctionArgInit(env_prefix=prefix, defaults=defaults).args assert args[expected.key] == expected.value + fs.create_file("config.yaml", contents=str(config)) with pytest.MonkeyPatch.context() as mp: if envs: for env, value in envs.items(): diff --git a/tests/test_env_variants.py b/tests/test_env_variants.py index 6fb8e37..e08b1b9 100644 --- a/tests/test_env_variants.py +++ b/tests/test_env_variants.py @@ -21,9 +21,9 @@ class TestEnvVariants: "prefix, arg_value, envs, defaults, expected", [ ("prefix", None, {"PREFIX_ARG1": "env1_value"}, None, Expected("arg1", "env1_value")), - ("prefix", None, {"ENV1": "env1_value"}, [ArgDefaults(name="arg1", env_name="ENV1")], Expected("arg1", "env1_value")), - (None, None, {"ARG1": "env1_value"}, [ArgDefaults(name="arg1", default_value="default", disable_env=True)], Expected("arg1", "default")), - (None, None, {"ENV1": "env1_value"}, [ArgDefaults(name="arg1", default_value="default", env_name="ENV1", disable_env=True)], Expected("arg1", "default")), + ("prefix", None, {"ENV1": "env1_value"}, [ArgDefaults(name="arg1", alt_name="ENV1")], Expected("arg1", "env1_value")), + # (None, None, {"ARG1": "env1_value"}, [ArgDefaults(name="arg1", default_value="default", disable_env=True)], Expected("arg1", "default")), + # (None, None, {"ENV1": "env1_value"}, [ArgDefaults(name="arg1", default_value="default", env_name="ENV1", disable_env=True)], Expected("arg1", "default")), ], ) def test_env_variants(self, prefix, arg_value, envs, defaults, expected): diff --git a/tests/test_file_configs.py b/tests/test_file_configs.py new file mode 100644 index 0000000..18f5a3a --- /dev/null +++ b/tests/test_file_configs.py @@ -0,0 +1,109 @@ +""" +Test file based argument initialisation +""" + +from pathlib import Path + +import pytest + +from arg_init import FunctionArgInit + + +class TestFileConfigs: + """ + """ + + def test_toml_file(self, fs): + """ + Test toml file can be used to initialise arguments + """ + def test(arg1=None): # pylint: disable=unused-argument + args = FunctionArgInit().args + assert args["arg1"] == config1_value + + config1_value = "config1_value" + config = "[test]\n"\ + f"arg1='{config1_value}'" + fs.create_file("config.toml", contents=config) + test() + + def test_yaml_file(self, fs): + """ + Test toml file can be used to initialise arguments + """ + def test(arg1=None): # pylint: disable=unused-argument + args = FunctionArgInit().args + assert args["arg1"] == config1_value + + config1_value = "config1_value" + config = "test:\n"\ + f" arg1: {config1_value}" + fs.create_file("config.yaml", contents=config) + test() + + def test_json_file(self, fs): + """ + Test toml file can be used to initialise arguments + """ + def test(arg1=None): # pylint: disable=unused-argument + args = FunctionArgInit().args + assert args["arg1"] == config1_value + + config1_value = "config1_value" + config = '{"test": {"arg1": "config1_value"}}' + fs.create_file("config.json", contents=config) + test() + + + def test_named_file_as_string(self, fs): + """ + Test toml file can be used to initialise arguments + """ + def test(arg1=None): # pylint: disable=unused-argument + args = FunctionArgInit(config_name="named_file").args + assert args["arg1"] == config1_value + + config1_value = "config1_value" + config = "[test]\n"\ + f"arg1='{config1_value}'" + fs.create_file("named_file.toml", contents=config) + test() + + def test_specified_file_as_path(self, fs): + """ + Test toml file can be used to initialise arguments + """ + def test(arg1=None): # pylint: disable=unused-argument + config_name = Path("named_file.toml") + args = FunctionArgInit(config_name=config_name).args + assert args["arg1"] == config1_value + + config1_value = "config1_value" + config = "[test]\n"\ + f"arg1='{config1_value}'" + fs.create_file("named_file.toml", contents=config) + test() + + def test_unsupported_format_raises_exception(self, fs): + """ + Test unsupported config file format raises an exception + """ + def test(arg1=None): # pylint: disable=unused-argument + config_name = Path("named_file.ini") + FunctionArgInit(config_name=config_name) + + with pytest.raises(RuntimeError): + fs.create_file("named_file.ini") + test() + + def test_missing_named_file_raises_exception(self, fs): + """ + Test missing named config file raises an exception. + When an alternate config file is specified, it MUST exist. + """ + def test(arg1=None): # pylint: disable=unused-argument + config_name = Path("missing_file.toml") + FunctionArgInit(config_name=config_name) + + with pytest.raises(FileNotFoundError): + test() diff --git a/tests/test_kwargs.py b/tests/test_kwargs.py index 4f594f7..b5a5c44 100644 --- a/tests/test_kwargs.py +++ b/tests/test_kwargs.py @@ -16,7 +16,7 @@ class TestKwargs: Class to test ArgInit for argument priority. """ - def test_kwargs_not_used(self): + def test_kwargs_not_used(self, fs): """ Test kwargs are ignored if not explicity enabled """ @@ -33,7 +33,7 @@ def test(arg1, **kwargs): test(arg1_value, **kwargs) - def test_kwargs_used_for_function(self): + def test_kwargs_used_for_function(self, fs): """ Test kwargs are processed if enabled """ @@ -50,7 +50,7 @@ def test(arg1, **kwargs): test(arg1_value, **kwargs) - def test_kwargs_used_for_class(self): + def test_kwargs_used_for_class(self, fs): """ Test kwargs are processed if enabled """ diff --git a/tests/test_print.py b/tests/test_print.py index 8ccd7aa..e2f66f6 100644 --- a/tests/test_print.py +++ b/tests/test_print.py @@ -40,8 +40,8 @@ def test(arg1): arg1_value = "arg1_value" expected = ( ", " + "name=arg1, alt_name=arg1, " + "values=, " "value=arg1_value)" ) @@ -53,15 +53,11 @@ def test_defaults_repr(self): """ arg1_defaults = ArgDefaults( - name="arg1", default_value="default", env_name="ENV", disable_env="True" + name="arg1", default_value="default", alt_name="ENV" ) defaults = [arg1_defaults] out = repr(defaults) expected = ( - "" ) assert expected in out From a4200a107b8743a9fff7a9ddd256e45aa5a880c1 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Sun, 5 Nov 2023 23:01:42 +0000 Subject: [PATCH 03/28] docs: Update docs --- docs/reference.md | 14 ++++++----- docs/usage.md | 42 +++++++++++++++++++++++++++----- readme.md | 61 +++++++++++++++++++++++++++-------------------- 3 files changed, 79 insertions(+), 38 deletions(-) diff --git a/docs/reference.md b/docs/reference.md index 7372d80..44d8bfd 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -3,7 +3,7 @@ ## ClassArgInit ```python -ClassArgInit(env_prefix=None, priority=ENV_PRIORITY, use_kwargs=False, set_attrs=True, protect_atts=True, defaults=None) +ClassArgInit(env_prefix=None, priority=ENV_PRIORITY, use_kwargs=False, set_attrs=True, protect_atts=True, defaults=None, config="config") ``` Resolve argument values using the bound function that calls ClassArgInit as the reference. Process each argument (skipping the first argument as this is a class reference) from the calling function, resolving and storing the value in a dictionary, where the argument name is the key. @@ -22,6 +22,8 @@ Resolve argument values using the bound function that calls ClassArgInit as the + **defaults**: A list of ArgDefault objects. ++ **config**: The name of the config file to load defaults from. If this is a Path object it can be a relative or absolute path to a config file. If a string, it can be the name of the file (excluding the extension). Default is to search for a file named "config" in the current working directory. + ### Attributes #### args @@ -33,7 +35,7 @@ Note: The returned object is a [python-box](https://github.com/cdgriffith/Box) B ## FunctionArgInit ```python -FunctionArgInit(env_prefix=None, priority=ENV_PRIORITY, use_kwargs=False, defaults=None) +FunctionArgInit(env_prefix=None, priority=ENV_PRIORITY, use_kwargs=False, defaults=None, config="config") ``` Resolve argument values using the function that calls FunctionArgInit as the reference. Process each argument from the calling function, resolving and storing the value in a dictionary, where the argument name is the key. @@ -42,12 +44,14 @@ Resolve argument values using the function that calls FunctionArgInit as the ref + **env_prefix**: env_prefix is used to avoid namespace clashes with environment variables. If set, all environment variables must include this prefix followed by an "_" character and the name of the argument. -+ **priority**: By default arguments will be set based on the priority env, arg, default. An alternate priority of arg, env, default is available by setting priority=ARG_PRIORITY. ++ **priority**: By default arguments will be set based on the priority config, env, arg, default. An alternate priority of arg, config, env, default is available by setting priority=ARG_PRIORITY. Alternatively, this can be specified by defining a tuple containing the required priority order. + **use_kwargs**: When initialising arguments, only named arguments will be initialised by default. If use_kwargs=True, then any keyword arguments will also be initialised + **defaults**: A list of ArgDefault objects. ++ **config**: The name of the config file to load defaults from. If this is a Path object it can be a relative or absolute path to a config file. If a string, it can be the name of the file (excluding the extension). Default is to search for a file named "config" in the current working directory. + ### Attributes #### args @@ -59,7 +63,7 @@ Note: The returned object is a [python-box](https://github.com/cdgriffith/Box) B ### ArgDefaults ```python -ArgDefaults(name, default_value=None, env_name="", disable_env=False) +ArgDefaults(name, default_value=None, env_name="") ``` A class that can be used to modify settings for an individual argument. @@ -69,5 +73,3 @@ A class that can be used to modify settings for an individual argument. + **env_name**: The name of the associated environment variable. If not set, env defaults to the uppercase equivalent of the argument name. + **default_value**: The default value to be applied if both arg and env values are not used. - -+ **disable_env**: If True then do not consider the env value when resolving the value. diff --git a/docs/usage.md b/docs/usage.md index b878643..ec728e6 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -30,7 +30,7 @@ FunctionArgInit() should be called from a function that arguments should be proc ```python from arg_init import FunctionArgInit -def my_func(arg1=99): +def my_func(arg1=None): args = FunctionArgInit().args print(args.arg1) ``` @@ -39,6 +39,41 @@ Resolved arguments are exposed by accessing the args attribute of FunctionArgIni ## Other Use Cases +### Using config files to resolve argument values + +By default arg-init will search for a config file named "config", with the extension: toml, yaml, json (in that order) in the current working directory. This behaviour can be overridden by specifying an absolute or relative path to a different config file. + +#### TOML files + +The section heading should be the name of the class, if using ClassArgInit or the name of the function, if using FunctionArgInit. + +```toml +[MyApp] +arg1 = 42 +``` + +#### YAML Files + +The top level dictionary key should be the name of the class, if using ClassArgInit or the name of the function, if using FunctionArgInit. + +```yaml +MyApp: + arg1: 42 +``` + +#### JSON Files + +The top level dictionary key should be the name of the class, if using ClassArgInit or the name of the function, if using FunctionArgInit. + +```json +{ + "MyApp": + { + "arg1": 42 + } +} +``` + ### Setting a Common Prefix for all Environment Variables To avoid namespace clashes with environment variables, it is recommneded to always supply an env_prefix argument when initialising ClassArgInit/FunctionArgInit. All environment variables are expected to have this prefix e.g. with an env_prefix of "myapp", arg1 would map to the environment variable "MYAPP_ARG1". @@ -87,7 +122,6 @@ ArgDefaults takes a "name" argumment and zero or more of the following optional + default_value + env_name -+ disable_env #### default_value @@ -101,10 +135,6 @@ Setting this value allows a custom env name to be set as the lookup for an argum Note: env_name is converted to uppercase before use. -#### disable_env - -If an argument should not use env value in its resolution process then set this attribute to True. If this attribute is set, even if the env exists it will not be used to resolve the argument value. - #### Example using ArgDefaults In the example below, arg1 is modified to have a default value of 1 and to resolve from the environmnet variable "ALT_NAME" diff --git a/readme.md b/readme.md index d7f62db..c15b1f2 100644 --- a/readme.md +++ b/readme.md @@ -6,15 +6,15 @@ [![PyPI][pypi_badge]][pypi_url] [![PyPI - License][license_badge]][license_url] -When running code there is often a need to initialise arguments either directly from a passed in value, indirectly via an environment variable or a via default value. Argparse provides this functionality (or can be easily augmented to) already but has one major drawback; It does not work when the code is invoked as a library. +When running code there is often a need to initialise arguments either directly from a passed in value, indirectly via an environment variable or config file or a via default value. Argparse provides this functionality (or can be easily augmented to, with the exception of loading from a config file) already but has one major drawback; It does not work when the code is invoked as a library. -arg_init provides functionality to resolve argument values for a given function/method from either an environment variable, an argument value or a default value. Introspection is used to determine the arguments of the calling function, and a dictionary is created of resolved values for each argument. Resolved values are determined using either Environment Priority (default) or Argument Priority. +arg_init provides functionality to resolve argument values for a given function/method from either a config file, an environment variable, an argument value or a default value. Introspection is used to determine the arguments of the calling function, and a dictionary is created of resolved values for each argument. Resolved values are determined using a predefined priority system that can be customised by the user. -When resolving from an environment variable, the environment variable name is assumed to be the same as the argument name, in uppercase e.g. An argument, arg1 would resolve from an environment variable, "ARG1". This behaviour can be modified by providing a custom env_name via argument defaults or by setting an env_prefix. +When resolving from an environment variable, the environment variable name is assumed to be the same as the argument name, in uppercase e.g. An argument, arg1 would resolve from an environment variable, "ARG1". This behaviour can be modified by providing an alternate name via argument defaults or by setting an env_prefix. If the calling function is a class method, arguments may also be made available as class attributes. See reference for more details. -Because it is implemented in the application, it will work if called via a CLI script or as a library by another python program. +Because argument initialisation is implemented in the application, it will work if called via a CLI script or as a library by another python program. **arg_init** provides two classes; ClassArgInit and FunctionArgInit for initialising arguments of bound class functions and unbound functions respectively. These classes iterate over all arguments of the calling function, exposing a dictionary containing key/value pairs of argument name, with values assigned according to the priority system selected. @@ -30,47 +30,56 @@ ClassArgInit: - Class attributes may be set that represent the resolved argument values -If ArgumentParser is used to create a CLI for an application then default values should **not** be assigned in add_argument(). This is to prevent different behaviours between launching as a CLI and an imported library. What happens is that ArgumentParser will provide values for all arguments that have a default assigned. This effectively renders default values in the called function redundant as a value is always provided, even if the value is None. - ## Priority -The argument value is set when a non **None** value is found, or all options are exhausted. At this point the argument is set to None. +The argument value is set when a value is found , or all options are exhausted. At this point the argument is set to None. What priority should be used to set an argument? ### Argument Priority Order -If passed in arguments have priorty over environment variables. - -1. Arg -2. Env -3. Default +If passed in arguments have priorty: -And if the function has a non **None** default argument e.g. f(a=1), then the argument value will always be used to set the value, never allowing an env value to take effect. +And if the function has a non **None** default argument e.g. f(a=1), then the argument value will always be used to set the value, never allowing a config or env value to take effect. There are two obvious solutions to this: -1. Change the priority order. +1. Lower the priority of arguments. 2. Provide an alternate means to specify a default value. If a default value is required in the function signature, to allow ommission of an argument when calling, ensure it is set to None. -### Env Priority Order +### Default Values + +The problem: How to avoid violating the DRY principle when an application can be invoked via a CLI or as a library. + +If an application is to be called as a library then the defaults MUST be implemented in the application, not the CLI script. But ArgumentParser will pass in None values if no value is specified for an argument. This None value will be used in preference to function default! So defaults must be specified in ArgumentParser and the applicication. This is not a good design pattern. + +Providing an alternate means to specify a default value resolves this. +There is a small gotcha here though. It is not possible to apply a non None value via an argument. + + +**arg-init** supports customisable priority models. +This becomes a personal choice, and behaviour can be chosen at implementation/run time. + +### Default Priority Order -Environment variables have prioirty over passed in arguments. +The default priority implemented is: -1. Env -2. Arg -3. Default +- **CONFIG_PRIORITY** + 1. Config + 1. Env + 1. Arg + 1. Default -This allows use of the standard default argument values for a python function if no env is defined. +Two further predifined priority models are provided -**ArgInit** supports both priority models. -This becomes a personal choice, and behaviour can be chosen at implementation/run time. Default priority order is: **Env Priority**. +- **ENV_PRIORITY** +- **ARG_PRIOIRTY** ## Usage ### Simple Useage -The following examples show how to use arg_init to initialise a class or function, with a default value assigned to the argument. +The following examples show how to use arg_init to initialise a class or function ```python from arg_init import ClassArgInit @@ -89,9 +98,9 @@ def func(arg1=10): ... ``` -In the examples above, arg1 will be initialised with the value from the environment variable "ARG1" if set, else it will take the passed in value. Finally it will have a default value of None assigned. +In the examples above, arg1 will be initialised with the value from the config file, the environment variable "ARG1", else it will take the passed in value. Finally it will have a default value of None assigned. -As these examples use the default priority sytem: ENV_PRIORITY, standard python function defaults can be used in the function signature. +As these examples use the default priority sytem, they will not work if used with ArgumentParser without ArgumentParser replicating the default values. ### Other use cases @@ -118,7 +127,7 @@ def func(arg1=None): ``` Note: -As this example uses argument priority, a default **must** be provided via ArgDefaults. +As this example uses argument priority, a default **must** be provided via ArgDefaults if the default is not None. ### Recommendation From e65c8a2d418aa93f177f4156bf481bfdecf283fc Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Mon, 6 Nov 2023 08:46:10 +0000 Subject: [PATCH 04/28] feat: update dependencies --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index af408d3..3647ff3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ classifiers = [ ] dependencies = [ "python-box>=7.0.1", + "pyyaml>=6.0.1", ] license = {text = "MIT"} dynamic = ["version"] @@ -37,6 +38,9 @@ lint = [ test = [ "pytest>=7.4.2", "pytest-cov>=4.1.0", + "pyfakefs>=5.3.0", + "mypy>=1.6.1", + "types-PyYAML>=6.0.12.12", ] docs = [ "mkdocs>=1.5.3", From 5185cdd4de531a21910c7b21e28ac673ee4fcac4 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Mon, 6 Nov 2023 08:47:26 +0000 Subject: [PATCH 05/28] bug: Logical false not setting argument value --- src/arg_init/_arg.py | 2 +- tests/test_arg_priority.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/arg_init/_arg.py b/src/arg_init/_arg.py index c7a3c5a..6e5e065 100644 --- a/src/arg_init/_arg.py +++ b/src/arg_init/_arg.py @@ -77,7 +77,7 @@ def resolve(self, name, priority_order): for priority in priority_order: logger.debug("Checking %s value", priority) value = self._get_value(priority) - if value: + if value is not None: logger.debug("Resolved %s = %s from %s", name, value, priority) self._value = value break diff --git a/tests/test_arg_priority.py b/tests/test_arg_priority.py index 0db95d7..c5627aa 100644 --- a/tests/test_arg_priority.py +++ b/tests/test_arg_priority.py @@ -52,6 +52,20 @@ def test(arg1): # pylint: disable=unused-argument mp.setenv(env, value) test(arg1=arg_value) + + def test_false_values(self): + """ + Test a logical false value sets the argument. + """ + def test(arg1): # pylint: disable=unused-argument + arg1_defaults = ArgDefaults("arg1", default_value=1) + args = FunctionArgInit(priority=PRIORITY_ORDER, defaults=[arg1_defaults]).args + assert args["arg1"] == arg1_value + + arg1_value = 0 + test(arg1_value) + + def test_multiple_args(self): """ Test multiple arg values From 1b73efc5d0802da42ea30d9eac135b92c24f2798 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Mon, 6 Nov 2023 08:48:11 +0000 Subject: [PATCH 06/28] chore: tweak alt_name usage --- src/arg_init/_arg_init.py | 34 ++++++++++++++++++++++------------ tests/test_print.py | 4 ++-- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/arg_init/_arg_init.py b/src/arg_init/_arg_init.py index 2da1f05..149233b 100644 --- a/src/arg_init/_arg_init.py +++ b/src/arg_init/_arg_init.py @@ -106,16 +106,16 @@ def _get_kwargs(self, arginfo, use_kwargs) -> dict: def _make_args(self, arguments, defaults, config) -> None: for name, value in arguments.items(): arg_defaults = self._get_arg_defaults(name, defaults) - alt_name = self._get_alt_name(name, arg_defaults) - env_name = self._get_env_name(name, arg_defaults) - + config_name = self._get_config_name(name, arg_defaults) + env_name = self._get_env_name(self._env_prefix, name, arg_defaults) default_value = self._get_default_value(arg_defaults) values = Values( arg=value, env=self._get_env_value(env_name), - config=config.get(alt_name), + config=config.get(config_name), default=default_value ) + alt_name = self._get_alt_name(arg_defaults) self._args[name] = Arg(name, alt_name, values).resolve(name, self._priority) def _get_arg_defaults(self, name, defaults): @@ -127,19 +127,29 @@ def _get_arg_defaults(self, name, defaults): return None @staticmethod - def _get_alt_name(name, arg_defaults): - """Determine the name to use for the config.""" + def _get_alt_name(arg_defaults): + """Return the alternate name for the argument.""" if arg_defaults and arg_defaults.alt_name: return arg_defaults.alt_name - return name + return None - def _get_env_name(self, name, arg_defaults): - """Determine the name to use for the env.""" - if arg_defaults and arg_defaults.alt_name: - return arg_defaults.alt_name - env_parts = [item for item in (self._env_prefix, name) if item] + @classmethod + def _get_config_name(cls, name, arg_defaults): + """Determine the name to use for the config.""" + alt_name = cls._get_alt_name(arg_defaults) + return alt_name if alt_name else name + + @staticmethod + def _construct_env_name(env_prefix, name): + env_parts = [item for item in (env_prefix, name) if item] return "_".join(env_parts).upper() + @classmethod + def _get_env_name(cls, env_prefix, name, arg_defaults): + """Determine the name to use for the env.""" + alt_name = cls._get_alt_name(arg_defaults) + return (alt_name if alt_name else cls._construct_env_name(env_prefix, name)).upper() + @staticmethod def _get_env_value(env_name) -> str | None: """Read the env value from environ.""" diff --git a/tests/test_print.py b/tests/test_print.py index e2f66f6..c93514b 100644 --- a/tests/test_print.py +++ b/tests/test_print.py @@ -40,9 +40,9 @@ def test(arg1): arg1_value = "arg1_value" expected = ( ", " - "value=arg1_value)" + "value=arg1_value)>" ) test(arg1_value) From bbcfdf0e92d44ccb659d9675a73198099a45f27d Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Tue, 7 Nov 2023 23:06:08 +0000 Subject: [PATCH 07/28] Test logical false values initialise argumets --- tests/test_arg_defaults.py | 49 ------------------- tests/test_arguments.py | 98 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 49 deletions(-) delete mode 100644 tests/test_arg_defaults.py create mode 100644 tests/test_arguments.py diff --git a/tests/test_arg_defaults.py b/tests/test_arg_defaults.py deleted file mode 100644 index c235b6b..0000000 --- a/tests/test_arg_defaults.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Test ArgDefaults -""" - -import logging - -from arg_init import ClassArgInit, ArgDefaults - -logger = logging.getLogger(__name__) - - -class TestArgDefaults: - """ - Test default values are correctly applied when creating args - """ - - def test_default_value(self): - """ - Test overriding default_value - """ - - class Test: - """Test Class""" - - def __init__(self, arg1=None): - name = "arg1" - default_value = "arg1_default" - defaults = [ArgDefaults(name=name, default_value=default_value)] - arg_init = ClassArgInit(defaults=defaults) - assert arg_init.args.arg1.values.default == default_value - - Test() - - def test_env_name(self): - """ - Test overriding env_name - """ - - class Test: - """Test Class""" - - def __init__(self, arg1=None): - name = "arg1" - alt_name = "ENV1" - defaults = [ArgDefaults(name=name, alt_name=alt_name)] - arg_init = ClassArgInit(defaults=defaults) - assert arg_init.args.arg1.alt_name == alt_name - - Test() diff --git a/tests/test_arguments.py b/tests/test_arguments.py new file mode 100644 index 0000000..03849e6 --- /dev/null +++ b/tests/test_arguments.py @@ -0,0 +1,98 @@ +""" +Test ArgDefaults +""" + +from collections import namedtuple +import logging + +import pytest + +from arg_init import ClassArgInit, FunctionArgInit, ArgDefaults, Priority + +logger = logging.getLogger(__name__) +Expected = namedtuple("Expected", "key value") + + +# Common test defaults +PRIORITY_ORDER = (Priority.ARG, Priority.CONFIG, Priority.ENV, Priority.DEFAULT) +ENV = {"ARG1": "env1_value"} +CONFIG = '{"test": {"arg1": "config1_value"}}' +DEFAULTS = [ArgDefaults(name="arg1", default_value="default")] + + +class TestArguments: + """ + Class to test ArgInit for argument priority. + """ + + @pytest.mark.parametrize( + "arg_value, envs, config, defaults, expected", + [ + # Priority order + (0, None, None, DEFAULTS, Expected("arg1", 0)), + ("", None, None, DEFAULTS, Expected("arg1", "")), + (None, {"ARG1": ""}, None, DEFAULTS, Expected("arg1", "")), + (None, None, '{"test": {"arg1": 0}}', DEFAULTS, Expected("arg1", 0)), + (None, None, '{"test": {"arg1": ""}}', DEFAULTS, Expected("arg1", "")), + ], + ) + def test_logical_false_values( + self, arg_value, envs, config, defaults, expected, fs + ): + """ + Priority Order + 1. All defined - Arg is used + 2. Config, env and default defined - Config is used + 2. Env and default defined - Env is used + 3. Default is defined - Default is used + 4. Nothing defined - None is used + """ + + def test(arg1): # pylint: disable=unused-argument + args = FunctionArgInit( + defaults=defaults, priority=PRIORITY_ORDER + ).args + print(args[expected.key], expected.value) + assert args[expected.key] == expected.value + + if config: + fs.create_file("config.yaml", contents=config) + with pytest.MonkeyPatch.context() as mp: + if envs: + for env, value in envs.items(): + mp.setenv(env, value) + test(arg1=arg_value) + + def test_default_value(self): + """ + Test overriding default_value + """ + + class Test: + """Test Class""" + + def __init__(self, arg1=None): # pylint: disable=unused-argument + name = "arg1" + default_value = "arg1_default" + defaults = [ArgDefaults(name=name, default_value=default_value)] + arg_init = ClassArgInit(defaults=defaults) + assert arg_init.args.arg1.values.default == default_value + + Test() + + def test_env_name(self): + """ + Test overriding env_name + """ + + class Test: + """Test Class""" + + def __init__(self, arg1=None): # pylint: disable=unused-argument + name = "arg1" + alt_name = "ENV1" + defaults = [ArgDefaults(name=name, alt_name=alt_name)] + arg_init = ClassArgInit(defaults=defaults) + assert arg_init.args.arg1.env_name == alt_name + + Test() From d9dfd268fe1068b0e8c4d3e5705527c5a0e964f3 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Tue, 7 Nov 2023 23:08:10 +0000 Subject: [PATCH 08/28] test: remove warnings --- tests/test_arg_priority.py | 52 ++++++++++++++++----------------- tests/test_class_arg_init.py | 25 ++++++++-------- tests/test_env_priority.py | 37 +++++++++++++---------- tests/test_env_variants.py | 6 +--- tests/test_file_configs.py | 3 +- tests/test_function_arg_init.py | 4 +-- tests/test_kwargs.py | 12 ++++---- tests/test_print.py | 14 ++++----- tests/test_version.py | 2 +- 9 files changed, 78 insertions(+), 77 deletions(-) diff --git a/tests/test_arg_priority.py b/tests/test_arg_priority.py index c5627aa..ec8f354 100644 --- a/tests/test_arg_priority.py +++ b/tests/test_arg_priority.py @@ -10,10 +10,14 @@ from arg_init import FunctionArgInit from arg_init import Priority -Expected = namedtuple('Expected', 'key value') +Expected = namedtuple("Expected", "key value") -# Custom priority order +# Common test defaults PRIORITY_ORDER = (Priority.ARG, Priority.CONFIG, Priority.ENV, Priority.DEFAULT) +ENV = {"ARG1": "env1_value"} +CONFIG = '{"test": {"arg1": "config1_value"}}' +DEFAULTS = [ArgDefaults(name="arg1", default_value="default")] + class TestArgPriority: """ @@ -24,11 +28,11 @@ class TestArgPriority: "prefix, arg_value, envs, config, defaults, expected", [ # Priority order - (None, "arg1_value", {"ARG1": "env1_value"}, {"test": {"arg1": "config1_value"}}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "arg1_value")), - (None, None, {"ARG1": "env1_value"}, {"test": {"arg1": "config1_value"}}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "config1_value")), - (None, None, {"ARG1": "env1_value"}, {}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "env1_value")), - (None, None, None, {}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "default")), - (None, None, None, {}, None, Expected("arg1", None)), + (None, "arg1_value", ENV, CONFIG, DEFAULTS, Expected("arg1", "arg1_value")), + (None, None, ENV, CONFIG, DEFAULTS, Expected("arg1", "config1_value")), + (None, None, ENV, None, DEFAULTS, Expected("arg1", "env1_value")), + (None, None, None, None, DEFAULTS, Expected("arg1", "default")), + (None, None, None, None, None, Expected("arg1", None)), ], ) def test_matrix(self, prefix, arg_value, envs, config, defaults, expected, fs): @@ -42,34 +46,24 @@ def test_matrix(self, prefix, arg_value, envs, config, defaults, expected, fs): """ def test(arg1): # pylint: disable=unused-argument - args = FunctionArgInit(env_prefix=prefix, defaults=defaults, priority=PRIORITY_ORDER).args + args = FunctionArgInit( + env_prefix=prefix, defaults=defaults, priority=PRIORITY_ORDER + ).args assert args[expected.key] == expected.value - fs.create_file("config.yaml", contents=str(config)) + if config: + fs.create_file("config.yaml", contents=config) with pytest.MonkeyPatch.context() as mp: if envs: for env, value in envs.items(): mp.setenv(env, value) test(arg1=arg_value) - - def test_false_values(self): - """ - Test a logical false value sets the argument. - """ - def test(arg1): # pylint: disable=unused-argument - arg1_defaults = ArgDefaults("arg1", default_value=1) - args = FunctionArgInit(priority=PRIORITY_ORDER, defaults=[arg1_defaults]).args - assert args["arg1"] == arg1_value - - arg1_value = 0 - test(arg1_value) - - - def test_multiple_args(self): + def test_multiple_args(self, fs): # pylint: disable=unused-argument """ Test multiple arg values """ + def test(arg1, arg2): # pylint: disable=unused-argument args = FunctionArgInit(priority=PRIORITY_ORDER).args assert args["arg1"] == arg1_value @@ -83,6 +77,7 @@ def _test_multiple_config_args(self, fs): """ Test multiple args defined in a config file """ + def test(arg1=None, arg2=None): # pylint: disable=unused-argument args = FunctionArgInit(priority=PRIORITY_ORDER).args assert args[arg1] == config1_value @@ -96,10 +91,11 @@ def test(arg1=None, arg2=None): # pylint: disable=unused-argument fs.create_file("config.yaml", contents=str(config)) test() - def test_multiple_envs(self): + def test_multiple_envs(self, fs): # pylint: disable=unused-argument """ Test a multiple args from envs """ + def test(arg1=None, arg2=None): # pylint: disable=unused-argument args = FunctionArgInit(priority=PRIORITY_ORDER).args assert args["arg1"] == env1_value @@ -114,13 +110,14 @@ def test(arg1=None, arg2=None): # pylint: disable=unused-argument mp.setenv(env2, env2_value) test() - def test_multiple_mixed(self): + def test_multiple_mixed(self, fs): # pylint: disable=unused-argument """ Test mixed initialisation arg1 - arg priority arg2 - arg, env not set arg3 - eng - arg = None """ + def test(arg1, arg2, arg3): # pylint: disable=unused-argument args = FunctionArgInit(priority=PRIORITY_ORDER).args assert args["arg1"] == arg1_value @@ -139,10 +136,11 @@ def test(arg1, arg2, arg3): # pylint: disable=unused-argument mp.setenv(env3, env3_value) test(arg1_value, arg2_value, arg3_value) - def test_env_prefix(self): + def test_env_prefix(self, fs): # pylint: disable=unused-argument """ Test using env_prefix does not affect results """ + def test(arg1): # pylint: disable=unused-argument args = FunctionArgInit(env_prefix="prefix", priority=PRIORITY_ORDER).args assert args["arg1"] == arg1_value diff --git a/tests/test_class_arg_init.py b/tests/test_class_arg_init.py index 773f68a..da7e890 100644 --- a/tests/test_class_arg_init.py +++ b/tests/test_class_arg_init.py @@ -4,7 +4,6 @@ from collections import namedtuple -from pyfakefs.fake_filesystem_unittest import patchfs import pytest from arg_init import ClassArgInit @@ -18,41 +17,41 @@ class TestClassArgInit: Test class attributes are initialised """ - def test_class(self, fs): + def test_class(self, fs): # pylint: disable=unused-argument """ Test ArgInit on a class method """ class Test: """Test Class""" - def __init__(self, arg1): + def __init__(self, arg1): # pylint: disable=unused-argument ClassArgInit() - assert self._arg1 == arg1_value + assert self._arg1 == arg1_value # pylint: disable=no-member arg1_value = "arg1_value" Test(arg1_value) - def test_protect_attr_false_sets_attr(self, fs): + def test_protect_attr_false_sets_attr(self, fs): # pylint: disable=unused-argument """ Test ArgInit on a class method """ class Test: """Test Class""" - def __init__(self, arg1): + def __init__(self, arg1): # pylint: disable=unused-argument ClassArgInit(protect_attrs=False) - assert self.arg1 == arg1_value + assert self.arg1 == arg1_value # pylint: disable=no-member arg1_value = "arg1_value" Test(arg1_value) - def test_exception_raised_if_protected_attr_exists(self, fs): + def test_exception_raised_if_protected_attr_exists(self, fs): # pylint: disable=unused-argument """ Test exception raised if attempting to set an attribute that already exists """ class Test: """Test Class""" - def __init__(self, arg1=None): + def __init__(self, arg1=None): # pylint: disable=unused-argument self._arg1 = "other_value" ClassArgInit() @@ -60,14 +59,14 @@ def __init__(self, arg1=None): Test() - def test_exception_raised_if_non_protected_attr_exists(self, fs): + def test_exception_raised_if_non_protected_attr_exists(self, fs): # pylint: disable=unused-argument """ Test exception raised if attempting to set an attribute that already exists. Verify "_" is not used as a prefix to attr when protect_args=False. """ class Test: """Test Class""" - def __init__(self, arg1=None): + def __init__(self, arg1=None): # pylint: disable=unused-argument self.arg1 = "other_value" ClassArgInit(protect_attrs=False) @@ -75,14 +74,14 @@ def __init__(self, arg1=None): Test() - def test_set_attrs_false_does_not_set_attrs(self, fs): + def test_set_attrs_false_does_not_set_attrs(self, fs): # pylint: disable=unused-argument """ Test exception raised if attempting to set an attribute that already exists. Verify "_" is not used as a prefix to attr when protect_args=False. """ class Test: """Test Class""" - def __init__(self, arg1=None): + def __init__(self, arg1=None): # pylint: disable=unused-argument self.arg1 = "other_value" ClassArgInit(set_attrs=False) assert hasattr(self, "_arg1") is False diff --git a/tests/test_env_priority.py b/tests/test_env_priority.py index f282f68..5bd397b 100644 --- a/tests/test_env_priority.py +++ b/tests/test_env_priority.py @@ -6,13 +6,20 @@ import pytest -from arg_init import ArgDefaults -from arg_init import FunctionArgInit + +from arg_init import FunctionArgInit, ArgDefaults, Priority Expected = namedtuple('Expected', 'key value') +# Common test defaults +PRIORITY_ORDER = (Priority.ARG, Priority.CONFIG, Priority.ENV, Priority.DEFAULT) +ENV = {"ARG1": "env1_value"} +CONFIG = '{"test": {"arg1": "config1_value"}}' +DEFAULTS = [ArgDefaults(name="arg1", default_value="default")] + + class TestEnvPriority: """ Class to test ArgInit for argument priority. @@ -21,10 +28,10 @@ class TestEnvPriority: @pytest.mark.parametrize( "prefix, arg_value, envs, config, defaults, expected", [ - (None, "arg1_value", {"ARG1": "env1_value"}, {"test": {"arg1": "config1_value"}}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "config1_value")), - (None, "arg1_value", {"ARG1": "env1_value"}, {}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "env1_value")), - (None, "arg1_value", None, {}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "env1_value")), - (None, None, None, {}, [ArgDefaults(name="arg1", default_value="default")], Expected("arg1", "default")), + (None, "arg1_value", ENV, CONFIG, DEFAULTS, Expected("arg1", "config1_value")), + (None, "arg1_value", ENV, {}, DEFAULTS, Expected("arg1", "env1_value")), + (None, "arg1_value", None, {}, DEFAULTS, Expected("arg1", "env1_value")), + (None, None, None, {}, DEFAULTS, Expected("arg1", "default")), (None, None, None, {}, None, Expected("arg1", None)), ], ) @@ -36,7 +43,7 @@ def test_priority(self, prefix, arg_value, envs, config, defaults, expected, fs) 3. Default is defined - Default is used 4. Nothing defined - None is used """ - def test(arg1): + def test(arg1): # pylint: disable=unused-argument args = FunctionArgInit(env_prefix=prefix, defaults=defaults).args assert args[expected.key] == expected.value @@ -48,12 +55,12 @@ def test(arg1): test(arg1=arg_value) - def test_function_default(self): + def test_function_default(self, fs): # pylint: disable=unused-argument """ Test function default is used if set and no arg passed in. """ - def test(arg1="func_default"): + def test(arg1="func_default"): # pylint: disable=unused-argument defaults = [ArgDefaults(name="arg1", default_value="default")] args = FunctionArgInit(defaults=defaults).args assert args["arg1"] == "func_default" @@ -61,11 +68,11 @@ def test(arg1="func_default"): test() - def test_multiple_args(self): + def test_multiple_args(self, fs): # pylint: disable=unused-argument """ Test initialisation from args when no envs defined """ - def test(arg1, arg2): + def test(arg1, arg2): # pylint: disable=unused-argument """Test Class""" args = FunctionArgInit().args assert args["arg1"] == arg1_value @@ -76,11 +83,11 @@ def test(arg1, arg2): test(arg1_value, arg2_value) - def test_multiple_envs(self): + def test_multiple_envs(self, fs): # pylint: disable=unused-argument """ Test initialised from envs """ - def test(arg1, arg2): + def test(arg1, arg2): # pylint: disable=unused-argument """Test Class""" args = FunctionArgInit().args assert args["arg1"] == env1_value @@ -96,14 +103,14 @@ def test(arg1, arg2): test("arg1_value", "arg2_value") - def test_multiple_mixed(self): + def test_multiple_mixed(self, fs): # pylint: disable=unused-argument """ Test mixed initialisation arg1 - env priority arg2 - env, arg = None arg3 - arg - env not set """ - def test(arg1, arg2, arg3): + def test(arg1, arg2, arg3): # pylint: disable=unused-argument """Test Class""" args = FunctionArgInit().args assert args["arg1"] == env1_value diff --git a/tests/test_env_variants.py b/tests/test_env_variants.py index e08b1b9..9bd407e 100644 --- a/tests/test_env_variants.py +++ b/tests/test_env_variants.py @@ -22,18 +22,14 @@ class TestEnvVariants: [ ("prefix", None, {"PREFIX_ARG1": "env1_value"}, None, Expected("arg1", "env1_value")), ("prefix", None, {"ENV1": "env1_value"}, [ArgDefaults(name="arg1", alt_name="ENV1")], Expected("arg1", "env1_value")), - # (None, None, {"ARG1": "env1_value"}, [ArgDefaults(name="arg1", default_value="default", disable_env=True)], Expected("arg1", "default")), - # (None, None, {"ENV1": "env1_value"}, [ArgDefaults(name="arg1", default_value="default", env_name="ENV1", disable_env=True)], Expected("arg1", "default")), ], ) - def test_env_variants(self, prefix, arg_value, envs, defaults, expected): + def test_env_variants(self, prefix, arg_value, envs, defaults, expected, fs): # pylint: disable=unused-argument """ Test advanced env use cases 1. Prefix - Env is used 2. Default env_name (Prefix not used) - Env is used - 3. Default env_name with env disabled - Default is used - 4. env_name defined with env_disabled - Default is used """ def test(arg1): # pylint: disable=unused-argument diff --git a/tests/test_file_configs.py b/tests/test_file_configs.py index 18f5a3a..5c60b25 100644 --- a/tests/test_file_configs.py +++ b/tests/test_file_configs.py @@ -11,6 +11,7 @@ class TestFileConfigs: """ + Test initialising content from various config file formats """ def test_toml_file(self, fs): @@ -96,7 +97,7 @@ def test(arg1=None): # pylint: disable=unused-argument fs.create_file("named_file.ini") test() - def test_missing_named_file_raises_exception(self, fs): + def test_missing_named_file_raises_exception(self, fs): # pylint: disable=unused-argument """ Test missing named config file raises an exception. When an alternate config file is specified, it MUST exist. diff --git a/tests/test_function_arg_init.py b/tests/test_function_arg_init.py index 1609754..a2c520b 100644 --- a/tests/test_function_arg_init.py +++ b/tests/test_function_arg_init.py @@ -15,11 +15,11 @@ class TestFunctionArgInit: Test function arguments are initialised """ - def test_function(self): + def test_function(self, fs): # pylint: disable=unused-argument """ Test FunctionArgInit """ - def test(arg1): + def test(arg1): # pylint: disable=unused-argument """Test Class""" arg_init = FunctionArgInit() assert arg_init.args.arg1 == arg1_value diff --git a/tests/test_kwargs.py b/tests/test_kwargs.py index b5a5c44..165d57d 100644 --- a/tests/test_kwargs.py +++ b/tests/test_kwargs.py @@ -16,11 +16,11 @@ class TestKwargs: Class to test ArgInit for argument priority. """ - def test_kwargs_not_used(self, fs): + def test_kwargs_not_used(self, fs): # pylint: disable=unused-argument """ Test kwargs are ignored if not explicity enabled """ - def test(arg1, **kwargs): + def test(arg1, **kwargs): # pylint: disable=unused-argument """Test Class""" args = FunctionArgInit(use_kwargs=True).args assert args["arg1"] == arg1_value @@ -33,11 +33,11 @@ def test(arg1, **kwargs): test(arg1_value, **kwargs) - def test_kwargs_used_for_function(self, fs): + def test_kwargs_used_for_function(self, fs): # pylint: disable=unused-argument """ Test kwargs are processed if enabled """ - def test(arg1, **kwargs): + def test(arg1, **kwargs): # pylint: disable=unused-argument """Test Class""" args = FunctionArgInit(use_kwargs=True).args assert args["arg1"] == arg1_value @@ -50,13 +50,13 @@ def test(arg1, **kwargs): test(arg1_value, **kwargs) - def test_kwargs_used_for_class(self, fs): + def test_kwargs_used_for_class(self, fs): # pylint: disable=unused-argument """ Test kwargs are processed if enabled """ class Test: """Test Class""" - def __init__(self, arg1, **kwargs): + def __init__(self, arg1, **kwargs): # pylint: disable=unused-argument args = ClassArgInit(use_kwargs=True).args assert args["arg1"] == arg1_value assert args["kwarg1"] == kwarg1_value diff --git a/tests/test_print.py b/tests/test_print.py index c93514b..2bcacc9 100644 --- a/tests/test_print.py +++ b/tests/test_print.py @@ -11,12 +11,12 @@ class TestPrintFunctions: Class to test default config . """ - def test_arg_str(self): + def test_arg_str(self, fs): # pylint: disable=unused-argument """ Test str() returns correct string """ - def test(arg1): + def test(arg1): # pylint: disable=unused-argument args = FunctionArgInit().args out = str(args[arg1_key]) assert expected in out @@ -26,12 +26,12 @@ def test(arg1): expected = arg1_value test(arg1_value) - def test_arg_repr(self): + def test_arg_repr(self, fs): # pylint: disable=unused-argument """ Test repr() returns correct string """ - def test(arg1): + def test(arg1): # pylint: disable=unused-argument args = FunctionArgInit().args out = repr(args[arg1_key]) assert expected in out @@ -40,14 +40,14 @@ def test(arg1): arg1_value = "arg1_value" expected = ( ", " "value=arg1_value)>" ) test(arg1_value) - def test_defaults_repr(self): + def test_defaults_repr(self, fs): # pylint: disable=unused-argument """ Test repr() returns correct string """ @@ -58,6 +58,6 @@ def test_defaults_repr(self): defaults = [arg1_defaults] out = repr(defaults) expected = ( - "" + "" ) assert expected in out diff --git a/tests/test_version.py b/tests/test_version.py index 2cd7a5b..f61bf37 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -7,7 +7,7 @@ class TestVersionDefined: """ - Class to test a version is defined in _version.py + Test a version is defined in _version.py """ def test_version_defined(self): From c2d0454c8ace409ce18e21d2804533af3ba24ccc Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Tue, 7 Nov 2023 23:08:37 +0000 Subject: [PATCH 09/28] chore: store env and config names in Arg --- src/arg_init/_arg.py | 18 ++++++++++---- src/arg_init/_arg_init.py | 51 +++++++++++++++++++++++++++++++-------- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/arg_init/_arg.py b/src/arg_init/_arg.py index 6e5e065..5f2c6f6 100644 --- a/src/arg_init/_arg.py +++ b/src/arg_init/_arg.py @@ -23,11 +23,13 @@ class Arg: def __init__( self, name: str, - alt_name: str | None = None, + env_name: str | None = None, + config_name: str | None = None, values=None, ): self._name = name - self._alt_name = alt_name + self._env_name = env_name + self._config_name = config_name self._values = values self._value = None @@ -38,7 +40,8 @@ def __eq__(self, other): def _data(self): return [ f"name={self.name}", - f"alt_name={self.alt_name}", + f"env_name={self.env_name}", + f"config_name={self.config_name}", f"values={self.values}", f"value={self.value}", ] @@ -60,9 +63,14 @@ def value(self): return self._value @property - def alt_name(self): + def env_name(self): """env attribute.""" - return self._alt_name + return self._env_name + + @property + def config_name(self): + """env attribute.""" + return self._config_name @property def values(self): diff --git a/src/arg_init/_arg_init.py b/src/arg_init/_arg_init.py index 149233b..5d285ae 100644 --- a/src/arg_init/_arg_init.py +++ b/src/arg_init/_arg_init.py @@ -112,11 +112,10 @@ def _make_args(self, arguments, defaults, config) -> None: values = Values( arg=value, env=self._get_env_value(env_name), - config=config.get(config_name), + config=self._get_config_value(config, config_name), default=default_value ) - alt_name = self._get_alt_name(arg_defaults) - self._args[name] = Arg(name, alt_name, values).resolve(name, self._priority) + self._args[name] = Arg(name, env_name, config_name, values).resolve(name, self._priority) def _get_arg_defaults(self, name, defaults): """Check if any defaults exist for the named arg.""" @@ -150,17 +149,49 @@ def _get_env_name(cls, env_prefix, name, arg_defaults): alt_name = cls._get_alt_name(arg_defaults) return (alt_name if alt_name else cls._construct_env_name(env_prefix, name)).upper() + # @staticmethod + # def _get_env_value(env_name) -> str | None: + # """Read the env value from environ.""" + # logger.debug("Searching for env: %s", env_name) + # if env_name in environ: + # value = environ[env_name] + # logger.debug("Env found: %s=%s", env_name, value) + # return value + # logger.debug("Env not set") + # return None + @staticmethod - def _get_env_value(env_name) -> str | None: - """Read the env value from environ.""" - logger.debug("Searching for env: %s", env_name) - if env_name in environ: - value = environ[env_name] - logger.debug("Env found: %s=%s", env_name, value) + def _get_value(name, dictionary) -> str | None: + """Read the env value.""" + # logger.debug("Searching for %s", name) + if name in dictionary: + value = dictionary[name] + logger.debug("Not found: %s=%s", name, value) return value - logger.debug("Env not set") + logger.debug("%s not set", name) return None + @classmethod + def _get_config_value(cls, config, name): + logger.debug("Searching config for: %s", name) + return cls._get_value(name, config) + + @classmethod + def _get_env_value(cls, name): + logger.debug("Searching environment for: %s", name) + return cls._get_value(name, environ) + + + # @staticmethod + # def _get_config_value(config, name): + # logger.debug("Searching for config: %s", name) + # if name in config: + # value = config[name] + # logger.debug("Config found: %s=%s", name, value) + # return value + # logger.debug("Config not set") + # return None + @staticmethod def _get_default_value(arg_defaults): if arg_defaults: From 843a200249c86d04bc9ba25cb7f82a2e2fc8da97 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Wed, 8 Nov 2023 10:05:23 +0000 Subject: [PATCH 10/28] imports on single line --- tests/test_arg_priority.py | 5 ++--- tests/test_arguments.py | 16 ++++++++-------- tests/test_env_variants.py | 3 +-- tests/test_function_arg_init.py | 2 +- tests/test_kwargs.py | 5 ++--- tests/test_print.py | 3 +-- 6 files changed, 15 insertions(+), 19 deletions(-) diff --git a/tests/test_arg_priority.py b/tests/test_arg_priority.py index ec8f354..26a1490 100644 --- a/tests/test_arg_priority.py +++ b/tests/test_arg_priority.py @@ -6,9 +6,8 @@ import pytest -from arg_init import ArgDefaults -from arg_init import FunctionArgInit -from arg_init import Priority +from arg_init import FunctionArgInit, ArgDefaults, Priority + Expected = namedtuple("Expected", "key value") diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 03849e6..cdf97b4 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -22,7 +22,7 @@ class TestArguments: """ - Class to test ArgInit for argument priority. + Class to test arguments are initialised correctly. """ @pytest.mark.parametrize( @@ -41,11 +41,11 @@ def test_logical_false_values( ): """ Priority Order - 1. All defined - Arg is used - 2. Config, env and default defined - Config is used - 2. Env and default defined - Env is used - 3. Default is defined - Default is used - 4. Nothing defined - None is used + 1. Test 0 argument + 2. Test "" argument + 2. Test "" env. Note env only supports string + 3. Test 0 config + 4. Test "" config """ def test(arg1): # pylint: disable=unused-argument @@ -65,7 +65,7 @@ def test(arg1): # pylint: disable=unused-argument def test_default_value(self): """ - Test overriding default_value + Test setting a default_value """ class Test: @@ -82,7 +82,7 @@ def __init__(self, arg1=None): # pylint: disable=unused-argument def test_env_name(self): """ - Test overriding env_name + Test setting an explicit an env_name """ class Test: diff --git a/tests/test_env_variants.py b/tests/test_env_variants.py index 9bd407e..2085cb6 100644 --- a/tests/test_env_variants.py +++ b/tests/test_env_variants.py @@ -6,8 +6,7 @@ import pytest -from arg_init import ArgDefaults -from arg_init import FunctionArgInit +from arg_init import FunctionArgInit, ArgDefaults Expected = namedtuple('Expected', 'key value') diff --git a/tests/test_function_arg_init.py b/tests/test_function_arg_init.py index a2c520b..9a952e6 100644 --- a/tests/test_function_arg_init.py +++ b/tests/test_function_arg_init.py @@ -7,7 +7,7 @@ from arg_init import FunctionArgInit -Expected = namedtuple('Expcted', 'key value') +Expected = namedtuple('Expected', 'key value') class TestFunctionArgInit: diff --git a/tests/test_kwargs.py b/tests/test_kwargs.py index 165d57d..d5bbdf4 100644 --- a/tests/test_kwargs.py +++ b/tests/test_kwargs.py @@ -4,11 +4,10 @@ from collections import namedtuple -from arg_init import ClassArgInit -from arg_init import FunctionArgInit +from arg_init import ClassArgInit, FunctionArgInit -Expected = namedtuple('Expcted', 'key value') +Expected = namedtuple('Expected', 'key value') class TestKwargs: diff --git a/tests/test_print.py b/tests/test_print.py index 2bcacc9..6e943f9 100644 --- a/tests/test_print.py +++ b/tests/test_print.py @@ -2,8 +2,7 @@ Test printing functions """ -from arg_init import FunctionArgInit -from arg_init import ArgDefaults +from arg_init import FunctionArgInit, ArgDefaults class TestPrintFunctions: From 71dce10eb8a234451cf99fe7f73a9dad8f017a34 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Wed, 8 Nov 2023 10:05:45 +0000 Subject: [PATCH 11/28] add py.typed --- src/arg_init/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/arg_init/py.typed diff --git a/src/arg_init/py.typed b/src/arg_init/py.typed new file mode 100644 index 0000000..e69de29 From 40db4abd0471a9936c8c4fb58d8efa678fec4930 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Wed, 8 Nov 2023 10:15:21 +0000 Subject: [PATCH 12/28] test: add action to run mypy --- .github/workflows/mypi.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/mypi.yml diff --git a/.github/workflows/mypi.yml b/.github/workflows/mypi.yml new file mode 100644 index 0000000..2db4029 --- /dev/null +++ b/.github/workflows/mypi.yml @@ -0,0 +1,25 @@ +name: mypy + +on: +- push +- workflow_call + +jobs: + build: + name: Static type check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup PDM + uses: pdm-project/setup-pdm@v3 + with: + python-version: 3.11 + cache: true + + - name: Install dependencies + run: pdm install -G test + + - name: Run mypy + run: pdm run mypy From 69415f81672facd650f8f3290ab7f7ece73b8e54 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Wed, 8 Nov 2023 10:17:44 +0000 Subject: [PATCH 13/28] test: add mypy config to pyproject.toml --- pyproject.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 3647ff3..8be1d81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,3 +66,10 @@ addopts = "--cov=src --cov-report term-missing" testpaths = [ "tests", ] + +[tool.mypy] +files = "src/arg_init/" +disallow_untyped_calls = true +# disallow_untyped_defs = true +check_untyped_defs = true +# disallow_untyped_decorators = true From 73e5b2bfea8e0361cbc035792fb911ba5920d27d Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Wed, 8 Nov 2023 10:32:56 +0000 Subject: [PATCH 14/28] test: rename mypi.yml to mypy.yml --- .github/workflows/{mypi.yml => mypy.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{mypi.yml => mypy.yml} (100%) diff --git a/.github/workflows/mypi.yml b/.github/workflows/mypy.yml similarity index 100% rename from .github/workflows/mypi.yml rename to .github/workflows/mypy.yml From 9faa93b828c19727a4a5b0e08f8b4c4ecc45e2bc Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Wed, 8 Nov 2023 11:09:02 +0000 Subject: [PATCH 15/28] docs: add mypy badge --- readme.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/readme.md b/readme.md index c15b1f2..d111b98 100644 --- a/readme.md +++ b/readme.md @@ -2,6 +2,7 @@ [![tests][tests_badge]][tests_url] [![codecov][codecov_badge]][codecov_url] +[![mypy][mypy_badge]][mypy_url] [![Docs][docs_badge]][docs_url] [![PyPI][pypi_badge]][pypi_url] [![PyPI - License][license_badge]][license_url] @@ -151,6 +152,8 @@ Please see the [documentation](https://srfoster65.github.io/arg_init/) for furth [tests_url]: https://github.com/srfoster65/arg_init/actions/workflows/build.yml [codecov_badge]: https://codecov.io/gh/srfoster65/arg_init/graph/badge.svg?token=FFNWSCS4BB [codecov_url]: https://codecov.io/gh/srfoster65/arg_init +[mypy_badge]: https://github.com/srfoster65/arg_init/actions/workflows/mypy.yml/badge.svg +[mypy_url]: https://github.com/srfoster65/arg_init/actions/workflows/mypy.yml [docs_badge]: https://github.com/srfoster65/arg_init/actions/workflows/docs.yml/badge.svg [docs_url]: https://srfoster65.github.io/arg_init/ [pypi_badge]: https://img.shields.io/pypi/v/arg-init?logo=python&logoColor=%23cccccc From 69f9e2c1794e61ef7b6bf41b5781cc2053ba3ee9 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Wed, 8 Nov 2023 11:09:53 +0000 Subject: [PATCH 16/28] feat: resolve mypy untyped calls --- src/arg_init/_arg.py | 9 +++--- src/arg_init/_arg_defaults.py | 2 +- src/arg_init/_arg_init.py | 48 +++++++++--------------------- src/arg_init/_class_arg_init.py | 16 +++++----- src/arg_init/_config.py | 21 ++++++++----- src/arg_init/_function_arg_init.py | 4 +-- 6 files changed, 44 insertions(+), 56 deletions(-) diff --git a/src/arg_init/_arg.py b/src/arg_init/_arg.py index 5f2c6f6..1ef1635 100644 --- a/src/arg_init/_arg.py +++ b/src/arg_init/_arg.py @@ -2,10 +2,11 @@ Data Class used to customise ArgInit behaviour """ +from typing import Any import logging from ._priority import Priority -# from ._values import Values + logger = logging.getLogger(__name__) @@ -37,7 +38,7 @@ def __eq__(self, other): """When testing for equality, test only the value attribute.""" return self.value == other - def _data(self): + def _data(self) -> list[str]: return [ f"name={self.name}", f"env_name={self.env_name}", @@ -77,7 +78,7 @@ def values(self): """Values to use when resolving Arg.""" return self._values - def resolve(self, name, priority_order): + def resolve(self, name, priority_order) -> Any: """ Resolve the value Arg using the selected priority system. """ @@ -91,5 +92,5 @@ def resolve(self, name, priority_order): break return self - def _get_value(self, priority): + def _get_value(self, priority) -> Any: return getattr(self._values, self._mapping[priority]) diff --git a/src/arg_init/_arg_defaults.py b/src/arg_init/_arg_defaults.py index e52b2b9..e83ff00 100644 --- a/src/arg_init/_arg_defaults.py +++ b/src/arg_init/_arg_defaults.py @@ -14,7 +14,7 @@ class ArgDefaults: """ name: str - default_value: Any = None + default_value: Any | None = None alt_name: str | None = None def __repr__(self) -> str: diff --git a/src/arg_init/_arg_init.py b/src/arg_init/_arg_init.py index 5d285ae..6a9d0e6 100644 --- a/src/arg_init/_arg_init.py +++ b/src/arg_init/_arg_init.py @@ -5,11 +5,13 @@ from abc import ABC, abstractmethod from inspect import stack from os import environ +from typing import Any import logging from box import Box from ._arg import Arg +from ._arg_defaults import ArgDefaults from ._config import read_config from ._priority import Priority from ._values import Values @@ -33,7 +35,7 @@ def __init__( defaults, config, **kwargs, # pylint: disable=unused-argument - ): + ) -> None: self._env_prefix = env_prefix self._priority = priority self._args = Box() @@ -58,7 +60,7 @@ def config(self): return self._config @abstractmethod - def _get_arguments(self, frame, use_kwargs): + def _get_arguments(self, frame, use_kwargs) -> dict: """ Returns a dictionary containing key value pairs of all named arguments and their values associated with the frame. @@ -66,14 +68,14 @@ def _get_arguments(self, frame, use_kwargs): raise RuntimeError() # pragma no cover @abstractmethod - def _get_name(self, calling_stack): + def _get_name(self, calling_stack) -> str: """ Return the name of the item having arguments initialised. """ raise RuntimeError() # pragma no cover # @abstractmethod - def _post_init(self, calling_stack): + def _post_init(self, calling_stack) -> None: """ Class specific post initialisation actions. This can optionally be overridden by derived classes @@ -117,7 +119,7 @@ def _make_args(self, arguments, defaults, config) -> None: ) self._args[name] = Arg(name, env_name, config_name, values).resolve(name, self._priority) - def _get_arg_defaults(self, name, defaults): + def _get_arg_defaults(self, name, defaults)-> ArgDefaults | None: """Check if any defaults exist for the named arg.""" if defaults: for arg_defaults in defaults: @@ -126,40 +128,29 @@ def _get_arg_defaults(self, name, defaults): return None @staticmethod - def _get_alt_name(arg_defaults): + def _get_alt_name(arg_defaults) -> str | None: """Return the alternate name for the argument.""" if arg_defaults and arg_defaults.alt_name: return arg_defaults.alt_name return None @classmethod - def _get_config_name(cls, name, arg_defaults): + def _get_config_name(cls, name, arg_defaults) -> str: """Determine the name to use for the config.""" alt_name = cls._get_alt_name(arg_defaults) return alt_name if alt_name else name @staticmethod - def _construct_env_name(env_prefix, name): + def _construct_env_name(env_prefix, name) -> str: env_parts = [item for item in (env_prefix, name) if item] return "_".join(env_parts).upper() @classmethod - def _get_env_name(cls, env_prefix, name, arg_defaults): + def _get_env_name(cls, env_prefix, name, arg_defaults) -> str: """Determine the name to use for the env.""" alt_name = cls._get_alt_name(arg_defaults) return (alt_name if alt_name else cls._construct_env_name(env_prefix, name)).upper() - # @staticmethod - # def _get_env_value(env_name) -> str | None: - # """Read the env value from environ.""" - # logger.debug("Searching for env: %s", env_name) - # if env_name in environ: - # value = environ[env_name] - # logger.debug("Env found: %s=%s", env_name, value) - # return value - # logger.debug("Env not set") - # return None - @staticmethod def _get_value(name, dictionary) -> str | None: """Read the env value.""" @@ -172,28 +163,17 @@ def _get_value(name, dictionary) -> str | None: return None @classmethod - def _get_config_value(cls, config, name): + def _get_config_value(cls, config, name) -> Any: logger.debug("Searching config for: %s", name) return cls._get_value(name, config) @classmethod - def _get_env_value(cls, name): + def _get_env_value(cls, name) -> str | None: logger.debug("Searching environment for: %s", name) return cls._get_value(name, environ) - - # @staticmethod - # def _get_config_value(config, name): - # logger.debug("Searching for config: %s", name) - # if name in config: - # value = config[name] - # logger.debug("Config found: %s=%s", name, value) - # return value - # logger.debug("Config not set") - # return None - @staticmethod - def _get_default_value(arg_defaults): + def _get_default_value(arg_defaults) -> Any: if arg_defaults: return arg_defaults.default_value return None diff --git a/src/arg_init/_class_arg_init.py b/src/arg_init/_class_arg_init.py index b4a8b5e..b59d8d8 100644 --- a/src/arg_init/_class_arg_init.py +++ b/src/arg_init/_class_arg_init.py @@ -3,6 +3,7 @@ """ from inspect import getargvalues +from typing import Any import logging from ._arg_init import ArgInit @@ -29,7 +30,7 @@ def __init__( defaults=None, config_name="config", **kwargs, - ): + ) -> None: self._set_attrs = set_attrs self._protect_attrs = protect_attrs super().__init__(priority, env_prefix, use_kwargs, defaults, config_name, **kwargs) @@ -39,7 +40,7 @@ def _post_init(self, calling_stack): class_instance = self._get_class_instance(calling_stack.frame) self._set_class_arg_attrs(class_instance) - def _get_arguments(self, frame, use_kwargs): + def _get_arguments(self, frame, use_kwargs) -> dict: """ Returns a dictionary containing key value pairs of all named arguments for the specified frame. The first @@ -55,19 +56,19 @@ def _get_arguments(self, frame, use_kwargs): args.update(self._get_kwargs(arginfo, use_kwargs)) return args - def _set_class_arg_attrs(self, class_ref): + def _set_class_arg_attrs(self, class_ref) -> None: """Set attributes for the class object.""" if self._set_attrs: logger.debug("Setting class attributes") for arg in self._args.values(): self._set_attr(class_ref, arg.name, arg.value) - def _get_attr_name(self, name): + def _get_attr_name(self, name) -> str: if self._protect_attrs: return name if name.startswith("_") else "_" + name return name - def _set_attr(self, class_instance, name, value): + def _set_attr(self, class_instance, name, value) -> None: name = self._get_attr_name(name) if hasattr(class_instance, name): raise AttributeError(f"Attribute already exists: {name}") @@ -75,7 +76,7 @@ def _set_attr(self, class_instance, name, value): setattr(class_instance, name, value) @staticmethod - def _get_class_instance(frame): + def _get_class_instance(frame) -> Any | None: """ Return the value of the 1st argument from the calling function. This should be the class instance. @@ -84,6 +85,7 @@ def _get_class_instance(frame): first_arg = arginfo.args[0] return arginfo.locals.get(first_arg) - def _get_name(self, calling_stack): + @staticmethod + def _get_name(calling_stack) -> str: """Return the name of the current class instance.""" return calling_stack.frame.f_locals["self"].__class__.__name__ diff --git a/src/arg_init/_config.py b/src/arg_init/_config.py index fb01f1b..4326512 100644 --- a/src/arg_init/_config.py +++ b/src/arg_init/_config.py @@ -10,6 +10,7 @@ from pathlib import Path from json import load as json_load from tomllib import load as toml_load +from typing import Callable import logging from yaml import safe_load as yaml_safe_load @@ -19,16 +20,19 @@ FORMATS = ["yaml", "toml", "json"] -def _yaml_loader(): +def _yaml_loader() -> Callable: return yaml_safe_load -def _json_loader(): + +def _json_loader() -> Callable: return json_load -def _toml_loader(): + +def _toml_loader() -> Callable: return toml_load -def _get_loader(path): + +def _get_loader(path) -> Callable: match path.suffix: case ".json": return _json_loader() @@ -39,12 +43,11 @@ def _get_loader(path): case _: raise RuntimeError(f"Unsupported file format: {path.suffix}") -def _find_config(file): + +def _find_config(file) -> Path | None: if isinstance(file, Path): file.resolve() logger.debug("Using named config file: %s", file.resolve()) - if not file.exists(): - raise FileNotFoundError(file) return file for ext in FORMATS: path = Path(f"{file}.{ext}").resolve() @@ -54,8 +57,10 @@ def _find_config(file): return path return None -def read_config(file="config"): + +def read_config(file="config") -> dict: """Read a config file.""" + logger.debug("Reading config file") path = _find_config(file) if path: loader = _get_loader(path) diff --git a/src/arg_init/_function_arg_init.py b/src/arg_init/_function_arg_init.py index c830e6c..0045237 100644 --- a/src/arg_init/_function_arg_init.py +++ b/src/arg_init/_function_arg_init.py @@ -25,10 +25,10 @@ def __init__( defaults=None, config_name="config", **kwargs, - ): + ) -> None: super().__init__(priority, env_prefix, use_kwargs, defaults, config_name, **kwargs) - def _get_arguments(self, frame, use_kwargs): + def _get_arguments(self, frame, use_kwargs) -> dict: """ Returns a dictionary containing key value pairs of all named arguments and their values associated with the frame. From b37bdbd89c7dbcccc8d209f1fb0870707a5dbc65 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:56:50 +0000 Subject: [PATCH 17/28] refactor: improve logging when reading config file --- src/arg_init/_arg_init.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/arg_init/_arg_init.py b/src/arg_init/_arg_init.py index 6a9d0e6..dbf7b61 100644 --- a/src/arg_init/_arg_init.py +++ b/src/arg_init/_arg_init.py @@ -13,7 +13,7 @@ from ._arg import Arg from ._arg_defaults import ArgDefaults from ._config import read_config -from ._priority import Priority +from ._priority import Priority, DEFAULT_PRIORITY from ._values import Values @@ -29,23 +29,19 @@ class ArgInit(ABC): def __init__( self, - priority, - env_prefix: str | None, - use_kwargs, - defaults, - config, + priority = DEFAULT_PRIORITY, + env_prefix: str | None = None, + use_kwargs = False, + defaults = None, + config_name = "config", **kwargs, # pylint: disable=unused-argument ) -> None: self._env_prefix = env_prefix self._priority = priority self._args = Box() calling_stack = stack()[self.STACK_LEVEL_OFFSET] - calling_stack.frame.f_locals["arg1"] = 2 name = self._get_name(calling_stack) - self._config = read_config(config) if Priority.CONFIG in priority else {} - if self._config: - logger.debug("Section id in config file: %s", name) - arg_config = self.config.get(name, {}) + arg_config = self._read_config(config_name, name, priority) self._init_args(name, calling_stack, use_kwargs, defaults, arg_config) self._post_init(calling_stack) @@ -177,3 +173,20 @@ def _get_default_value(arg_defaults) -> Any: if arg_defaults: return arg_defaults.default_value return None + + def _read_config( + self, + config_name, + section_name: str, + priority: tuple, + ) -> dict: + config = config = ( + read_config(config_name) if Priority.CONFIG in priority else {} + ) + if config: + logger.debug("Checking for section '%s' in config file", section_name) + if section_name in config: + logger.debug("config=%s", config[section_name]) + return config[section_name] + logger.debug("No config data found for section: %s", section_name) + return {} From 6ad6339ca069a4460084ed79ab00b533ce641f73 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Wed, 8 Nov 2023 22:14:55 +0000 Subject: [PATCH 18/28] test: add mypy defaults to pyproject.toml --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8be1d81..1b524a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,6 @@ testpaths = [ [tool.mypy] files = "src/arg_init/" disallow_untyped_calls = true -# disallow_untyped_defs = true +disallow_untyped_defs = true check_untyped_defs = true -# disallow_untyped_decorators = true +disallow_untyped_decorators = true From e911eb1c19d20801f7d11516a7467f603990d4c4 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Wed, 8 Nov 2023 22:55:19 +0000 Subject: [PATCH 19/28] feat: Add type definitions --- src/arg_init/_arg.py | 26 +++---- src/arg_init/_arg_init.py | 109 ++++++++++++++++------------- src/arg_init/_class_arg_init.py | 47 +++++++------ src/arg_init/_config.py | 9 +-- src/arg_init/_function_arg_init.py | 21 ++---- src/arg_init/_values.py | 4 +- tests/test_arg_priority.py | 15 ++-- 7 files changed, 118 insertions(+), 113 deletions(-) diff --git a/src/arg_init/_arg.py b/src/arg_init/_arg.py index 1ef1635..612d28f 100644 --- a/src/arg_init/_arg.py +++ b/src/arg_init/_arg.py @@ -6,7 +6,7 @@ import logging from ._priority import Priority - +from ._values import Values logger = logging.getLogger(__name__) @@ -26,15 +26,15 @@ def __init__( name: str, env_name: str | None = None, config_name: str | None = None, - values=None, - ): + values: Values | None = None, + ) -> None: self._name = name self._env_name = env_name self._config_name = config_name self._values = values self._value = None - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: """When testing for equality, test only the value attribute.""" return self.value == other @@ -47,38 +47,38 @@ def _data(self) -> list[str]: f"value={self.value}", ] - def __str__(self): + def __str__(self) -> str: return str(self.value) - def __repr__(self): + def __repr__(self) -> str: return "" @property - def name(self): + def name(self) -> str: """Name of Arg.""" return self._name @property - def value(self): + def value(self) -> Any: """Resolved value of Arg.""" return self._value @property - def env_name(self): + def env_name(self) -> str | None: """env attribute.""" return self._env_name @property - def config_name(self): + def config_name(self) -> str | None: """env attribute.""" return self._config_name @property - def values(self): + def values(self) -> Values | None: """Values to use when resolving Arg.""" return self._values - def resolve(self, name, priority_order) -> Any: + def resolve(self, name: str, priority_order: tuple) -> Any: """ Resolve the value Arg using the selected priority system. """ @@ -92,5 +92,5 @@ def resolve(self, name, priority_order) -> Any: break return self - def _get_value(self, priority) -> Any: + def _get_value(self, priority: Priority) -> Any: return getattr(self._values, self._mapping[priority]) diff --git a/src/arg_init/_arg_init.py b/src/arg_init/_arg_init.py index dbf7b61..74c1b59 100644 --- a/src/arg_init/_arg_init.py +++ b/src/arg_init/_arg_init.py @@ -3,9 +3,10 @@ processed attribute values. """ from abc import ABC, abstractmethod -from inspect import stack +from inspect import stack, FrameInfo from os import environ -from typing import Any +from pathlib import Path +from typing import Any, Optional, Mapping import logging from box import Box @@ -18,6 +19,8 @@ logger = logging.getLogger(__name__) +Defaults = Optional[list[ArgDefaults]] +Priorities= tuple[Priority, Priority, Priority, Priority] class ArgInit(ABC): """ @@ -25,24 +28,24 @@ class ArgInit(ABC): variables or default values. """ - STACK_LEVEL_OFFSET = 2 # The calling frame is 2 layers up + STACK_LEVEL_OFFSET = 0 # Overridden by concrete class - def __init__( + def __init__( # pylint: disable=unused-argument self, - priority = DEFAULT_PRIORITY, - env_prefix: str | None = None, - use_kwargs = False, - defaults = None, - config_name = "config", - **kwargs, # pylint: disable=unused-argument + priorities: Priorities = DEFAULT_PRIORITY, + env_prefix: Optional[str] = None, + use_kwargs: bool = False, + defaults: Defaults = None, + config_name: str | Path = "config", + **kwargs: dict, # pylint: disable=unused-argument ) -> None: self._env_prefix = env_prefix - self._priority = priority + self._priorities = priorities self._args = Box() calling_stack = stack()[self.STACK_LEVEL_OFFSET] name = self._get_name(calling_stack) - arg_config = self._read_config(config_name, name, priority) - self._init_args(name, calling_stack, use_kwargs, defaults, arg_config) + config_data = self._read_config(config_name, name, priorities) + self._init_args(name, calling_stack, use_kwargs, defaults, config_data) self._post_init(calling_stack) @property @@ -50,13 +53,8 @@ def args(self) -> Box: """Return the processed arguments.""" return self._args - @property - def config(self): - """Return the config data""" - return self._config - @abstractmethod - def _get_arguments(self, frame, use_kwargs) -> dict: + def _get_arguments(self, frame: Any, use_kwargs: bool) -> dict: """ Returns a dictionary containing key value pairs of all named arguments and their values associated with the frame. @@ -64,14 +62,14 @@ def _get_arguments(self, frame, use_kwargs) -> dict: raise RuntimeError() # pragma no cover @abstractmethod - def _get_name(self, calling_stack) -> str: + def _get_name(self, calling_stack: FrameInfo) -> str: """ Return the name of the item having arguments initialised. """ raise RuntimeError() # pragma no cover # @abstractmethod - def _post_init(self, calling_stack) -> None: + def _post_init(self, calling_stack: FrameInfo) -> None: """ Class specific post initialisation actions. This can optionally be overridden by derived classes @@ -79,18 +77,18 @@ def _post_init(self, calling_stack) -> None: def _init_args( self, - name, - calling_stack, + name: str, + calling_stack: FrameInfo, use_kwargs: bool, - defaults, - config, + defaults: Defaults, + config: dict, ) -> None: """Resolve argument values.""" logger.debug("Creating arguments for: %s", name) arguments = self._get_arguments(calling_stack.frame, use_kwargs) self._make_args(arguments, defaults, config) - def _get_kwargs(self, arginfo, use_kwargs) -> dict: + def _get_kwargs(self, arginfo: Any, use_kwargs: bool) -> dict: """ Return a dictionary containing kwargs to be resolved. Returns an empty dictionary if use_kwargs=False @@ -101,7 +99,9 @@ def _get_kwargs(self, arginfo, use_kwargs) -> dict: return dict(arginfo.locals[keywords].items()) return {} - def _make_args(self, arguments, defaults, config) -> None: + def _make_args( + self, arguments: dict, defaults: Defaults, config: Mapping + ) -> None: for name, value in arguments.items(): arg_defaults = self._get_arg_defaults(name, defaults) config_name = self._get_config_name(name, arg_defaults) @@ -111,11 +111,15 @@ def _make_args(self, arguments, defaults, config) -> None: arg=value, env=self._get_env_value(env_name), config=self._get_config_value(config, config_name), - default=default_value + default=default_value, + ) + self._args[name] = Arg(name, env_name, config_name, values).resolve( + name, self._priorities ) - self._args[name] = Arg(name, env_name, config_name, values).resolve(name, self._priority) - def _get_arg_defaults(self, name, defaults)-> ArgDefaults | None: + def _get_arg_defaults( + self, name: str, defaults: Defaults + ) -> ArgDefaults | None: """Check if any defaults exist for the named arg.""" if defaults: for arg_defaults in defaults: @@ -124,31 +128,35 @@ def _get_arg_defaults(self, name, defaults)-> ArgDefaults | None: return None @staticmethod - def _get_alt_name(arg_defaults) -> str | None: + def _get_alt_name(arg_defaults: ArgDefaults | None) -> str | None: """Return the alternate name for the argument.""" if arg_defaults and arg_defaults.alt_name: return arg_defaults.alt_name return None @classmethod - def _get_config_name(cls, name, arg_defaults) -> str: + def _get_config_name(cls, name: str, arg_defaults: ArgDefaults | None) -> str: """Determine the name to use for the config.""" alt_name = cls._get_alt_name(arg_defaults) return alt_name if alt_name else name @staticmethod - def _construct_env_name(env_prefix, name) -> str: + def _construct_env_name(env_prefix: str | None, name: str) -> str: env_parts = [item for item in (env_prefix, name) if item] return "_".join(env_parts).upper() @classmethod - def _get_env_name(cls, env_prefix, name, arg_defaults) -> str: + def _get_env_name( + cls, env_prefix: str | None, name: str, arg_defaults: ArgDefaults | None + ) -> str: """Determine the name to use for the env.""" alt_name = cls._get_alt_name(arg_defaults) - return (alt_name if alt_name else cls._construct_env_name(env_prefix, name)).upper() + return ( + alt_name if alt_name else cls._construct_env_name(env_prefix, name) + ).upper() @staticmethod - def _get_value(name, dictionary) -> str | None: + def _get_value(name: str, dictionary: Mapping) -> str | None: """Read the env value.""" # logger.debug("Searching for %s", name) if name in dictionary: @@ -159,34 +167,35 @@ def _get_value(name, dictionary) -> str | None: return None @classmethod - def _get_config_value(cls, config, name) -> Any: + def _get_config_value(cls, config: Mapping, name: str) -> Any: logger.debug("Searching config for: %s", name) return cls._get_value(name, config) @classmethod - def _get_env_value(cls, name) -> str | None: + def _get_env_value(cls, name: str) -> str | None: logger.debug("Searching environment for: %s", name) return cls._get_value(name, environ) @staticmethod - def _get_default_value(arg_defaults) -> Any: + def _get_default_value(arg_defaults: ArgDefaults | None) -> Any: if arg_defaults: return arg_defaults.default_value return None def _read_config( self, - config_name, + config_name: str | Path, section_name: str, - priority: tuple, + priorities: Priorities, ) -> dict: - config = config = ( - read_config(config_name) if Priority.CONFIG in priority else {} - ) - if config: - logger.debug("Checking for section '%s' in config file", section_name) - if section_name in config: - logger.debug("config=%s", config[section_name]) - return config[section_name] - logger.debug("No config data found for section: %s", section_name) + if Priority.CONFIG in priorities: + config = read_config(config_name) + if config: + logger.debug("Checking for section '%s' in config file", section_name) + if section_name in config: + logger.debug("config=%s", config[section_name]) + return config[section_name] + logger.debug("No section '%s' data found", section_name) + return {} + logger.debug("skipping file based config based on priorities") return {} diff --git a/src/arg_init/_class_arg_init.py b/src/arg_init/_class_arg_init.py index b59d8d8..08a7726 100644 --- a/src/arg_init/_class_arg_init.py +++ b/src/arg_init/_class_arg_init.py @@ -2,15 +2,19 @@ Class to initialise Argument Values for a Class Method """ -from inspect import getargvalues -from typing import Any +from inspect import getargvalues, FrameInfo +from pathlib import Path +from typing import Any, Optional, Callable import logging from ._arg_init import ArgInit -from ._priority import DEFAULT_PRIORITY +from ._arg_defaults import ArgDefaults +from ._priority import Priority, DEFAULT_PRIORITY logger = logging.getLogger(__name__) +Defaults = Optional[list[ArgDefaults]] +Priorities= tuple[Priority, Priority, Priority, Priority] class ClassArgInit(ArgInit): @@ -20,27 +24,29 @@ class ClassArgInit(ArgInit): i.e. an argument named "self" """ + STACK_LEVEL_OFFSET = 2 # The calling frame is 2 layers up + def __init__( self, - priority=DEFAULT_PRIORITY, - env_prefix=None, - use_kwargs=False, - set_attrs=True, - protect_attrs=True, - defaults=None, - config_name="config", - **kwargs, + priorities: Priorities = DEFAULT_PRIORITY, + env_prefix: Optional[str] = None, + use_kwargs: bool = False, + defaults: Defaults = None, + config_name: str | Path = "config", + set_attrs: bool = True, + protect_attrs: bool = True, + **kwargs: dict, # pylint: disable=unused-argument ) -> None: self._set_attrs = set_attrs self._protect_attrs = protect_attrs - super().__init__(priority, env_prefix, use_kwargs, defaults, config_name, **kwargs) + super().__init__(priorities, env_prefix, use_kwargs, defaults, config_name, **kwargs) - def _post_init(self, calling_stack): + def _post_init(self, calling_stack: FrameInfo) -> None: """Class specific post init behaviour.""" class_instance = self._get_class_instance(calling_stack.frame) self._set_class_arg_attrs(class_instance) - def _get_arguments(self, frame, use_kwargs) -> dict: + def _get_arguments(self, frame: Any, use_kwargs: bool) -> dict: """ Returns a dictionary containing key value pairs of all named arguments for the specified frame. The first @@ -56,19 +62,19 @@ def _get_arguments(self, frame, use_kwargs) -> dict: args.update(self._get_kwargs(arginfo, use_kwargs)) return args - def _set_class_arg_attrs(self, class_ref) -> None: + def _set_class_arg_attrs(self, class_ref: Callable) -> None: """Set attributes for the class object.""" if self._set_attrs: logger.debug("Setting class attributes") for arg in self._args.values(): self._set_attr(class_ref, arg.name, arg.value) - def _get_attr_name(self, name) -> str: + def _get_attr_name(self, name: str) -> str: if self._protect_attrs: return name if name.startswith("_") else "_" + name return name - def _set_attr(self, class_instance, name, value) -> None: + def _set_attr(self, class_instance: Callable, name: str, value: Any) -> None: name = self._get_attr_name(name) if hasattr(class_instance, name): raise AttributeError(f"Attribute already exists: {name}") @@ -76,16 +82,15 @@ def _set_attr(self, class_instance, name, value) -> None: setattr(class_instance, name, value) @staticmethod - def _get_class_instance(frame) -> Any | None: + def _get_class_instance(frame: Any) -> Callable: """ Return the value of the 1st argument from the calling function. This should be the class instance. """ arginfo = getargvalues(frame) first_arg = arginfo.args[0] - return arginfo.locals.get(first_arg) + return arginfo.locals[first_arg] - @staticmethod - def _get_name(calling_stack) -> str: + def _get_name(self, calling_stack: FrameInfo) -> str: """Return the name of the current class instance.""" return calling_stack.frame.f_locals["self"].__class__.__name__ diff --git a/src/arg_init/_config.py b/src/arg_init/_config.py index 4326512..fd7b2ce 100644 --- a/src/arg_init/_config.py +++ b/src/arg_init/_config.py @@ -32,7 +32,7 @@ def _toml_loader() -> Callable: return toml_load -def _get_loader(path) -> Callable: +def _get_loader(path: Path) -> Callable: match path.suffix: case ".json": return _json_loader() @@ -44,7 +44,7 @@ def _get_loader(path) -> Callable: raise RuntimeError(f"Unsupported file format: {path.suffix}") -def _find_config(file) -> Path | None: +def _find_config(file: str | Path) -> Path | None: if isinstance(file, Path): file.resolve() logger.debug("Using named config file: %s", file.resolve()) @@ -55,10 +55,11 @@ def _find_config(file) -> Path | None: if path.exists(): logger.debug("config found: %s", path) return path + logger.debug("No supported config files found") return None -def read_config(file="config") -> dict: +def read_config(file: str | Path) -> dict | None: """Read a config file.""" logger.debug("Reading config file") path = _find_config(file) @@ -66,4 +67,4 @@ def read_config(file="config") -> dict: loader = _get_loader(path) with open(path, "rb") as f: return loader(f) - return {} + return None diff --git a/src/arg_init/_function_arg_init.py b/src/arg_init/_function_arg_init.py index 0045237..635035a 100644 --- a/src/arg_init/_function_arg_init.py +++ b/src/arg_init/_function_arg_init.py @@ -3,11 +3,11 @@ """ -from inspect import getargvalues +from inspect import FrameInfo, getargvalues +from typing import Any import logging from ._arg_init import ArgInit -from ._priority import DEFAULT_PRIORITY logger = logging.getLogger(__name__) @@ -17,18 +17,9 @@ class FunctionArgInit(ArgInit): Initialises arguments from a function. """ - def __init__( - self, - priority=DEFAULT_PRIORITY, - env_prefix=None, - use_kwargs=False, - defaults=None, - config_name="config", - **kwargs, - ) -> None: - super().__init__(priority, env_prefix, use_kwargs, defaults, config_name, **kwargs) - - def _get_arguments(self, frame, use_kwargs) -> dict: + STACK_LEVEL_OFFSET = 1 # The calling frame is 2 layers up + + def _get_arguments(self, frame: Any, use_kwargs: bool) -> dict: """ Returns a dictionary containing key value pairs of all named arguments and their values associated with the frame. @@ -38,5 +29,5 @@ def _get_arguments(self, frame, use_kwargs) -> dict: args.update(self._get_kwargs(arginfo, use_kwargs)) return args - def _get_name(self, calling_stack): + def _get_name(self, calling_stack: FrameInfo) -> str: return calling_stack.function diff --git a/src/arg_init/_values.py b/src/arg_init/_values.py index 812f383..3d6b5cf 100644 --- a/src/arg_init/_values.py +++ b/src/arg_init/_values.py @@ -12,9 +12,9 @@ class Values: """ arg: Any = None - env: Any = None + env: str | None = None config: Any = None default: Any = None - def __repr__(self): + def __repr__(self) -> str: return f"" diff --git a/tests/test_arg_priority.py b/tests/test_arg_priority.py index 26a1490..b44b56a 100644 --- a/tests/test_arg_priority.py +++ b/tests/test_arg_priority.py @@ -6,13 +6,12 @@ import pytest -from arg_init import FunctionArgInit, ArgDefaults, Priority +from arg_init import FunctionArgInit, ArgDefaults, ARG_PRIORITY Expected = namedtuple("Expected", "key value") # Common test defaults -PRIORITY_ORDER = (Priority.ARG, Priority.CONFIG, Priority.ENV, Priority.DEFAULT) ENV = {"ARG1": "env1_value"} CONFIG = '{"test": {"arg1": "config1_value"}}' DEFAULTS = [ArgDefaults(name="arg1", default_value="default")] @@ -46,7 +45,7 @@ def test_matrix(self, prefix, arg_value, envs, config, defaults, expected, fs): def test(arg1): # pylint: disable=unused-argument args = FunctionArgInit( - env_prefix=prefix, defaults=defaults, priority=PRIORITY_ORDER + env_prefix=prefix, defaults=defaults, priorities=ARG_PRIORITY ).args assert args[expected.key] == expected.value @@ -64,7 +63,7 @@ def test_multiple_args(self, fs): # pylint: disable=unused-argument """ def test(arg1, arg2): # pylint: disable=unused-argument - args = FunctionArgInit(priority=PRIORITY_ORDER).args + args = FunctionArgInit(priorities=ARG_PRIORITY).args assert args["arg1"] == arg1_value assert args["arg2"] == arg2_value @@ -78,7 +77,7 @@ def _test_multiple_config_args(self, fs): """ def test(arg1=None, arg2=None): # pylint: disable=unused-argument - args = FunctionArgInit(priority=PRIORITY_ORDER).args + args = FunctionArgInit(priorities=ARG_PRIORITY).args assert args[arg1] == config1_value assert args[arg2] == config2_value @@ -96,7 +95,7 @@ def test_multiple_envs(self, fs): # pylint: disable=unused-argument """ def test(arg1=None, arg2=None): # pylint: disable=unused-argument - args = FunctionArgInit(priority=PRIORITY_ORDER).args + args = FunctionArgInit(priorities=ARG_PRIORITY).args assert args["arg1"] == env1_value assert args["arg2"] == env2_value @@ -118,7 +117,7 @@ def test_multiple_mixed(self, fs): # pylint: disable=unused-argument """ def test(arg1, arg2, arg3): # pylint: disable=unused-argument - args = FunctionArgInit(priority=PRIORITY_ORDER).args + args = FunctionArgInit(priorities=ARG_PRIORITY).args assert args["arg1"] == arg1_value assert args["arg2"] == arg2_value assert args["arg3"] == env3_value @@ -141,7 +140,7 @@ def test_env_prefix(self, fs): # pylint: disable=unused-argument """ def test(arg1): # pylint: disable=unused-argument - args = FunctionArgInit(env_prefix="prefix", priority=PRIORITY_ORDER).args + args = FunctionArgInit(env_prefix="prefix", priorities=ARG_PRIORITY).args assert args["arg1"] == arg1_value arg1_value = "arg1_value" From a1bd332852527247dd55c9c92268d6215943a8f3 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Thu, 9 Nov 2023 19:25:12 +0000 Subject: [PATCH 20/28] feat: improve mypy type checking feature coverage --- pdm.lock | 194 ++++++++++++++++++++++++++++- pyproject.toml | 23 +++- src/arg_init/_aliases.py | 12 ++ src/arg_init/_arg.py | 4 +- src/arg_init/_arg_defaults.py | 1 + src/arg_init/_arg_init.py | 25 ++-- src/arg_init/_class_arg_init.py | 16 ++- src/arg_init/_config.py | 15 +-- src/arg_init/_function_arg_init.py | 2 +- 9 files changed, 257 insertions(+), 35 deletions(-) create mode 100644 src/arg_init/_aliases.py diff --git a/pdm.lock b/pdm.lock index 00ac3ed..090cd84 100644 --- a/pdm.lock +++ b/pdm.lock @@ -6,7 +6,20 @@ groups = ["default", "test", "lint", "docs"] cross_platform = true static_urls = false lock_version = "4.3" -content_hash = "sha256:776748b6cfc103e9e74b47c2a2e54300ad208b52ad6435732f43c1795b4df50b" +content_hash = "sha256:680db50e3762184abf8cd28794b690b5194ac70989ee677fb12a071cee93aa8f" + +[[package]] +name = "annotated-types" +version = "0.6.0" +requires_python = ">=3.8" +summary = "Reusable constraint types to use with typing.Annotated" +dependencies = [ + "typing-extensions>=4.0.0; python_version < \"3.9\"", +] +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] [[package]] name = "babel" @@ -362,6 +375,46 @@ files = [ {file = "mkdocs_material_extensions-1.2.tar.gz", hash = "sha256:27e2d1ed2d031426a6e10d5ea06989d67e90bb02acd588bc5673106b5ee5eedf"}, ] +[[package]] +name = "mypy" +version = "1.6.1" +requires_python = ">=3.8" +summary = "Optional static typing for Python" +dependencies = [ + "mypy-extensions>=1.0.0", + "tomli>=1.1.0; python_version < \"3.11\"", + "typing-extensions>=4.1.0", +] +files = [ + {file = "mypy-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c"}, + {file = "mypy-1.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb"}, + {file = "mypy-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e"}, + {file = "mypy-1.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f"}, + {file = "mypy-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c"}, + {file = "mypy-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5"}, + {file = "mypy-1.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245"}, + {file = "mypy-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183"}, + {file = "mypy-1.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0"}, + {file = "mypy-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7"}, + {file = "mypy-1.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f"}, + {file = "mypy-1.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660"}, + {file = "mypy-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7"}, + {file = "mypy-1.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71"}, + {file = "mypy-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"}, + {file = "mypy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169"}, + {file = "mypy-1.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143"}, + {file = "mypy-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46"}, + {file = "mypy-1.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85"}, + {file = "mypy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45"}, + {file = "mypy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208"}, + {file = "mypy-1.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd"}, + {file = "mypy-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332"}, + {file = "mypy-1.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f"}, + {file = "mypy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30"}, + {file = "mypy-1.6.1-py3-none-any.whl", hash = "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1"}, + {file = "mypy-1.6.1.tar.gz", hash = "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1"}, +] + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -420,6 +473,136 @@ files = [ {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] +[[package]] +name = "pydantic" +version = "2.4.2" +requires_python = ">=3.7" +summary = "Data validation using Python type hints" +dependencies = [ + "annotated-types>=0.4.0", + "pydantic-core==2.10.1", + "typing-extensions>=4.6.1", +] +files = [ + {file = "pydantic-2.4.2-py3-none-any.whl", hash = "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"}, + {file = "pydantic-2.4.2.tar.gz", hash = "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7"}, +] + +[[package]] +name = "pydantic-core" +version = "2.10.1" +requires_python = ">=3.7" +summary = "" +dependencies = [ + "typing-extensions!=4.7.0,>=4.6.0", +] +files = [ + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63"}, + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6"}, + {file = "pydantic_core-2.10.1-cp310-none-win32.whl", hash = "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b"}, + {file = "pydantic_core-2.10.1-cp310-none-win_amd64.whl", hash = "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f"}, + {file = "pydantic_core-2.10.1-cp311-none-win32.whl", hash = "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6"}, + {file = "pydantic_core-2.10.1-cp311-none-win_amd64.whl", hash = "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27"}, + {file = "pydantic_core-2.10.1-cp311-none-win_arm64.whl", hash = "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c"}, + {file = "pydantic_core-2.10.1-cp312-none-win32.whl", hash = "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f"}, + {file = "pydantic_core-2.10.1-cp312-none-win_amd64.whl", hash = "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430"}, + {file = "pydantic_core-2.10.1-cp312-none-win_arm64.whl", hash = "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de"}, + {file = "pydantic_core-2.10.1-cp38-none-win32.whl", hash = "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee"}, + {file = "pydantic_core-2.10.1-cp38-none-win_amd64.whl", hash = "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595"}, + {file = "pydantic_core-2.10.1-cp39-none-win32.whl", hash = "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a"}, + {file = "pydantic_core-2.10.1-cp39-none-win_amd64.whl", hash = "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776"}, + {file = "pydantic_core-2.10.1.tar.gz", hash = "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82"}, +] + +[[package]] +name = "pyfakefs" +version = "5.3.0" +requires_python = ">=3.7" +summary = "pyfakefs implements a fake file system that mocks the Python file system modules." +files = [ + {file = "pyfakefs-5.3.0-py3-none-any.whl", hash = "sha256:33c1f891078c727beec465e75cb314120635e2298456493cc2cc0539e2130cbb"}, + {file = "pyfakefs-5.3.0.tar.gz", hash = "sha256:e3e35f65ce55ee8ecc5e243d55cfdbb5d0aa24938f6e04e19f0fab062f255020"}, +] + [[package]] name = "pygments" version = "2.16.1" @@ -631,6 +814,15 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.12" +summary = "Typing stubs for PyYAML" +files = [ + {file = "types-PyYAML-6.0.12.12.tar.gz", hash = "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062"}, + {file = "types_PyYAML-6.0.12.12-py3-none-any.whl", hash = "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"}, +] + [[package]] name = "typing-extensions" version = "4.8.0" diff --git a/pyproject.toml b/pyproject.toml index 1b524a7..fd098aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ test = [ "pyfakefs>=5.3.0", "mypy>=1.6.1", "types-PyYAML>=6.0.12.12", + "pydantic>=2.4.2", ] docs = [ "mkdocs>=1.5.3", @@ -68,8 +69,24 @@ testpaths = [ ] [tool.mypy] +plugins = [ + "pydantic.mypy" +] files = "src/arg_init/" -disallow_untyped_calls = true -disallow_untyped_defs = true + +follow_imports = "silent" +warn_redundant_casts = true +warn_unused_ignores = true +disallow_any_generics = true check_untyped_defs = true -disallow_untyped_decorators = true +no_implicit_reexport = true + +# for strict mypy: (this is the tricky one :-)) +disallow_untyped_defs = true + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true + +show-error-content = true diff --git a/src/arg_init/_aliases.py b/src/arg_init/_aliases.py new file mode 100644 index 0000000..f3f3c11 --- /dev/null +++ b/src/arg_init/_aliases.py @@ -0,0 +1,12 @@ +""" +mypy type aliases +""" + +from typing import Any, Optional, Callable + +from ._arg_defaults import ArgDefaults +from ._priority import Priority + +Defaults = Optional[list[ArgDefaults]] +Priorities= tuple[Priority, Priority, Priority, Priority] +ClassCallback = Callable[[Any], None] diff --git a/src/arg_init/_arg.py b/src/arg_init/_arg.py index 612d28f..70d1fa6 100644 --- a/src/arg_init/_arg.py +++ b/src/arg_init/_arg.py @@ -9,6 +9,8 @@ from ._values import Values logger = logging.getLogger(__name__) +# Typing aliases +Priorities = tuple[Priority, Priority, Priority, Priority] class Arg: @@ -78,7 +80,7 @@ def values(self) -> Values | None: """Values to use when resolving Arg.""" return self._values - def resolve(self, name: str, priority_order: tuple) -> Any: + def resolve(self, name: str, priority_order: Priorities) -> Any: """ Resolve the value Arg using the selected priority system. """ diff --git a/src/arg_init/_arg_defaults.py b/src/arg_init/_arg_defaults.py index e83ff00..ea78c0b 100644 --- a/src/arg_init/_arg_defaults.py +++ b/src/arg_init/_arg_defaults.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from typing import Any + @dataclass class ArgDefaults: """ diff --git a/src/arg_init/_arg_init.py b/src/arg_init/_arg_init.py index 74c1b59..596caef 100644 --- a/src/arg_init/_arg_init.py +++ b/src/arg_init/_arg_init.py @@ -11,6 +11,7 @@ from box import Box +from ._aliases import Defaults, Priorities from ._arg import Arg from ._arg_defaults import ArgDefaults from ._config import read_config @@ -19,13 +20,12 @@ logger = logging.getLogger(__name__) -Defaults = Optional[list[ArgDefaults]] -Priorities= tuple[Priority, Priority, Priority, Priority] + class ArgInit(ABC): """ - Class to resolve arguments of a function from passed in values, environment - variables or default values. + Class to resolve arguments of a function from passed in values, a config file, + environment variables or default values. """ STACK_LEVEL_OFFSET = 0 # Overridden by concrete class @@ -37,7 +37,7 @@ def __init__( # pylint: disable=unused-argument use_kwargs: bool = False, defaults: Defaults = None, config_name: str | Path = "config", - **kwargs: dict, # pylint: disable=unused-argument + **kwargs: dict[Any, Any], # pylint: disable=unused-argument ) -> None: self._env_prefix = env_prefix self._priorities = priorities @@ -54,7 +54,7 @@ def args(self) -> Box: return self._args @abstractmethod - def _get_arguments(self, frame: Any, use_kwargs: bool) -> dict: + def _get_arguments(self, frame: Any, use_kwargs: bool) -> dict[Any, Any]: """ Returns a dictionary containing key value pairs of all named arguments and their values associated with the frame. @@ -81,14 +81,14 @@ def _init_args( calling_stack: FrameInfo, use_kwargs: bool, defaults: Defaults, - config: dict, + config: dict[Any, Any], ) -> None: """Resolve argument values.""" logger.debug("Creating arguments for: %s", name) arguments = self._get_arguments(calling_stack.frame, use_kwargs) self._make_args(arguments, defaults, config) - def _get_kwargs(self, arginfo: Any, use_kwargs: bool) -> dict: + def _get_kwargs(self, arginfo: Any, use_kwargs: bool) -> dict[Any, Any]: """ Return a dictionary containing kwargs to be resolved. Returns an empty dictionary if use_kwargs=False @@ -100,7 +100,7 @@ def _get_kwargs(self, arginfo: Any, use_kwargs: bool) -> dict: return {} def _make_args( - self, arguments: dict, defaults: Defaults, config: Mapping + self, arguments: dict[Any, Any], defaults: Defaults, config: Mapping[Any, Any] ) -> None: for name, value in arguments.items(): arg_defaults = self._get_arg_defaults(name, defaults) @@ -156,9 +156,8 @@ def _get_env_name( ).upper() @staticmethod - def _get_value(name: str, dictionary: Mapping) -> str | None: + def _get_value(name: str, dictionary: Mapping[Any, Any]) -> str | None: """Read the env value.""" - # logger.debug("Searching for %s", name) if name in dictionary: value = dictionary[name] logger.debug("Not found: %s=%s", name, value) @@ -167,7 +166,7 @@ def _get_value(name: str, dictionary: Mapping) -> str | None: return None @classmethod - def _get_config_value(cls, config: Mapping, name: str) -> Any: + def _get_config_value(cls, config: Mapping[Any, Any], name: str) -> Any: logger.debug("Searching config for: %s", name) return cls._get_value(name, config) @@ -187,7 +186,7 @@ def _read_config( config_name: str | Path, section_name: str, priorities: Priorities, - ) -> dict: + ) -> dict[Any, Any]: if Priority.CONFIG in priorities: config = read_config(config_name) if config: diff --git a/src/arg_init/_class_arg_init.py b/src/arg_init/_class_arg_init.py index 08a7726..0c00bfd 100644 --- a/src/arg_init/_class_arg_init.py +++ b/src/arg_init/_class_arg_init.py @@ -7,14 +7,12 @@ from typing import Any, Optional, Callable import logging +from ._aliases import Defaults, Priorities, ClassCallback from ._arg_init import ArgInit -from ._arg_defaults import ArgDefaults -from ._priority import Priority, DEFAULT_PRIORITY +from ._priority import DEFAULT_PRIORITY logger = logging.getLogger(__name__) -Defaults = Optional[list[ArgDefaults]] -Priorities= tuple[Priority, Priority, Priority, Priority] class ClassArgInit(ArgInit): @@ -35,7 +33,7 @@ def __init__( config_name: str | Path = "config", set_attrs: bool = True, protect_attrs: bool = True, - **kwargs: dict, # pylint: disable=unused-argument + **kwargs: dict[Any, Any], # pylint: disable=unused-argument ) -> None: self._set_attrs = set_attrs self._protect_attrs = protect_attrs @@ -46,7 +44,7 @@ def _post_init(self, calling_stack: FrameInfo) -> None: class_instance = self._get_class_instance(calling_stack.frame) self._set_class_arg_attrs(class_instance) - def _get_arguments(self, frame: Any, use_kwargs: bool) -> dict: + def _get_arguments(self, frame: Any, use_kwargs: bool) -> dict[Any, Any]: """ Returns a dictionary containing key value pairs of all named arguments for the specified frame. The first @@ -62,7 +60,7 @@ def _get_arguments(self, frame: Any, use_kwargs: bool) -> dict: args.update(self._get_kwargs(arginfo, use_kwargs)) return args - def _set_class_arg_attrs(self, class_ref: Callable) -> None: + def _set_class_arg_attrs(self, class_ref: ClassCallback) -> None: """Set attributes for the class object.""" if self._set_attrs: logger.debug("Setting class attributes") @@ -74,7 +72,7 @@ def _get_attr_name(self, name: str) -> str: return name if name.startswith("_") else "_" + name return name - def _set_attr(self, class_instance: Callable, name: str, value: Any) -> None: + def _set_attr(self, class_instance: ClassCallback, name: str, value: Any) -> None: name = self._get_attr_name(name) if hasattr(class_instance, name): raise AttributeError(f"Attribute already exists: {name}") @@ -82,7 +80,7 @@ def _set_attr(self, class_instance: Callable, name: str, value: Any) -> None: setattr(class_instance, name, value) @staticmethod - def _get_class_instance(frame: Any) -> Callable: + def _get_class_instance(frame: Any) -> ClassCallback: """ Return the value of the 1st argument from the calling function. This should be the class instance. diff --git a/src/arg_init/_config.py b/src/arg_init/_config.py index fd7b2ce..461c2b9 100644 --- a/src/arg_init/_config.py +++ b/src/arg_init/_config.py @@ -10,7 +10,8 @@ from pathlib import Path from json import load as json_load from tomllib import load as toml_load -from typing import Callable +# from typing import Callable, Any, SupportsRead, DefaultNamedArg +from typing import Callable, Any import logging from yaml import safe_load as yaml_safe_load @@ -18,21 +19,21 @@ logger = logging.getLogger(__name__) FORMATS = ["yaml", "toml", "json"] +LoaderCallback = Callable[[Any], dict[Any, Any]] - -def _yaml_loader() -> Callable: +def _yaml_loader() -> LoaderCallback: return yaml_safe_load -def _json_loader() -> Callable: +def _json_loader() -> LoaderCallback: return json_load -def _toml_loader() -> Callable: +def _toml_loader() -> LoaderCallback: return toml_load -def _get_loader(path: Path) -> Callable: +def _get_loader(path: Path) -> LoaderCallback: match path.suffix: case ".json": return _json_loader() @@ -59,7 +60,7 @@ def _find_config(file: str | Path) -> Path | None: return None -def read_config(file: str | Path) -> dict | None: +def read_config(file: str | Path) -> dict[Any, Any] | None: """Read a config file.""" logger.debug("Reading config file") path = _find_config(file) diff --git a/src/arg_init/_function_arg_init.py b/src/arg_init/_function_arg_init.py index 635035a..521f8ff 100644 --- a/src/arg_init/_function_arg_init.py +++ b/src/arg_init/_function_arg_init.py @@ -19,7 +19,7 @@ class FunctionArgInit(ArgInit): STACK_LEVEL_OFFSET = 1 # The calling frame is 2 layers up - def _get_arguments(self, frame: Any, use_kwargs: bool) -> dict: + def _get_arguments(self, frame: Any, use_kwargs: bool) -> dict[Any, Any]: """ Returns a dictionary containing key value pairs of all named arguments and their values associated with the frame. From 02b6b86efc5cff32517dcb9f942f827090674230 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Thu, 9 Nov 2023 22:14:41 +0000 Subject: [PATCH 21/28] test: Add test cases for priority sequences --- src/arg_init/__init__.py | 10 +++- src/arg_init/_class_arg_init.py | 2 +- tests/test_priorities.py | 101 ++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 tests/test_priorities.py diff --git a/src/arg_init/__init__.py b/src/arg_init/__init__.py index b648354..2d714d4 100644 --- a/src/arg_init/__init__.py +++ b/src/arg_init/__init__.py @@ -3,7 +3,12 @@ from ._class_arg_init import ClassArgInit from ._arg_defaults import ArgDefaults from ._function_arg_init import FunctionArgInit -from ._priority import Priority, DEFAULT_PRIORITY, ARG_PRIORITY +from ._priority import ( + Priority, + CONFIG_PRIORITY, + ENV_PRIORITY, + ARG_PRIORITY, +) # External API __all__ = [ @@ -11,6 +16,7 @@ "FunctionArgInit", "ArgDefaults", "Priority", - "DEFAULT_PRIORITY", + "CONFIG_PRIORITY", + "ENV_PRIORITY", "ARG_PRIORITY", ] diff --git a/src/arg_init/_class_arg_init.py b/src/arg_init/_class_arg_init.py index 0c00bfd..5139981 100644 --- a/src/arg_init/_class_arg_init.py +++ b/src/arg_init/_class_arg_init.py @@ -4,7 +4,7 @@ from inspect import getargvalues, FrameInfo from pathlib import Path -from typing import Any, Optional, Callable +from typing import Any, Optional import logging from ._aliases import Defaults, Priorities, ClassCallback diff --git a/tests/test_priorities.py b/tests/test_priorities.py new file mode 100644 index 0000000..518231d --- /dev/null +++ b/tests/test_priorities.py @@ -0,0 +1,101 @@ +""" +Test priority sequences +""" + +from collections import namedtuple + +import pytest + +from arg_init import ( + FunctionArgInit, + ArgDefaults, + Priority, + ENV_PRIORITY, + CONFIG_PRIORITY, + ARG_PRIORITY, +) + + +Expected = namedtuple("Expected", "key value") + +# Common test defaults +ENV = {"ARG1": "env1_value"} +CONFIG = '{"test": {"arg1": "config1_value"}}' +DEFAULTS = [ArgDefaults(name="arg1", default_value="default")] + + +class TestPrioritySequences: + """ + Class to test ArgInit for argument priority. + """ + + def test_config_priority(self, fs): # pylint: disable=unused-argument + """ + Test config priority + """ + + def test(arg1=None): # pylint: disable=unused-argument + args = FunctionArgInit(priorities=CONFIG_PRIORITY).args + assert args["arg1"] == config1_value + + fs.create_file("config.yaml", contents=CONFIG) + env1 = "ARG1" + config1_value = "config1_value" + env1_value = "env1_value" + arg1_value = "arg1_value" + with pytest.MonkeyPatch.context() as mp: + mp.setenv(env1, env1_value) + test(arg1_value) + + def test_env_priority(self, fs): # pylint: disable=unused-argument + """ + Test config priority + """ + + def test(arg1=None): # pylint: disable=unused-argument + args = FunctionArgInit(priorities=ENV_PRIORITY).args + assert args["arg1"] == env1_value + + fs.create_file("config.yaml", contents=CONFIG) + env1 = "ARG1" + env1_value = "env1_value" + arg1_value = "arg1_value" + with pytest.MonkeyPatch.context() as mp: + mp.setenv(env1, env1_value) + test(arg1_value) + + def test_arg_priority(self, fs): # pylint: disable=unused-argument + """ + Test config priority + """ + + def test(arg1=None): # pylint: disable=unused-argument + args = FunctionArgInit(priorities=ARG_PRIORITY).args + assert args["arg1"] == arg1_value + + fs.create_file("config.yaml", contents=CONFIG) + env1 = "ARG1" + env1_value = "env1_value" + arg1_value = "arg1_value" + with pytest.MonkeyPatch.context() as mp: + mp.setenv(env1, env1_value) + test(arg1_value) + + def test_custom_priority(self, fs): # pylint: disable=unused-argument + """ + Test config priority + """ + + def test(arg1=None): # pylint: disable=unused-argument + priorities = (Priority.ARG, Priority.DEFAULT) + args = FunctionArgInit(priorities=priorities).args + # CONFIG and ENV are disabled, so should use arg value + assert args["arg1"] == arg1_value + + fs.create_file("config.yaml", contents=CONFIG) + env1 = "ARG1" + env1_value = "env1_value" + arg1_value = "arg1_value" + with pytest.MonkeyPatch.context() as mp: + mp.setenv(env1, env1_value) + test(arg1_value) From d137856def53c10c2ee495ebf0cce347128d3ead Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Thu, 9 Nov 2023 22:20:33 +0000 Subject: [PATCH 22/28] docs: ALign docs with implementation --- docs/reference.md | 51 +++++++++++++++++++++++++++++++++++++---------- docs/usage.md | 15 ++++++++++++++ readme.md | 10 ++++------ 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/docs/reference.md b/docs/reference.md index 44d8bfd..2159397 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -3,27 +3,27 @@ ## ClassArgInit ```python -ClassArgInit(env_prefix=None, priority=ENV_PRIORITY, use_kwargs=False, set_attrs=True, protect_atts=True, defaults=None, config="config") +ClassArgInit(priorities=DEFAULT_PRIORITY, env_prefix=None, use_kwargs=False, defaults=None, config="config", set_attrs=True, protect_atts=True) ``` Resolve argument values using the bound function that calls ClassArgInit as the reference. Process each argument (skipping the first argument as this is a class reference) from the calling function, resolving and storing the value in a dictionary, where the argument name is the key. ### Arguments -+ **env_prefix**: env_prefix is used to avoid namespace clashes with environment variables. If set, all environment variables must include this prefix followed by an "_" character and the name of the argument. ++ **priorities**: By default arguments will be set based on the priority sequence: config, env, arg, default. Several alternate priority sequences are predefined, or a custom sequence can be defined. -+ **priority**: By default arguments will be set based on the priority env, arg, default. An alternate priority of arg, env, default is available by setting priority=ARG_PRIORITY. ++ **env_prefix**: env_prefix is used to avoid namespace clashes with environment variables. If set, all environment variables must include this prefix followed by an "_" character and the name of the argument. + **use_kwargs**: When initialising arguments, only named arguments will be initialised by default. If use_kwargs=True, then any keyword arguments will also be initialised -+ **set_attrs**: Set the arguments as class attributes. Default is true. - -+ **protect_attrs**: Add a leading "_" character to all assigned attribute names. Default is True. - + **defaults**: A list of ArgDefault objects. + **config**: The name of the config file to load defaults from. If this is a Path object it can be a relative or absolute path to a config file. If a string, it can be the name of the file (excluding the extension). Default is to search for a file named "config" in the current working directory. ++ **set_attrs**: Set the arguments as class attributes. Default is true. + ++ **protect_attrs**: Add a leading "_" character to all assigned attribute names. Default is True. + ### Attributes #### args @@ -35,17 +35,17 @@ Note: The returned object is a [python-box](https://github.com/cdgriffith/Box) B ## FunctionArgInit ```python -FunctionArgInit(env_prefix=None, priority=ENV_PRIORITY, use_kwargs=False, defaults=None, config="config") +FunctionArgInit(env_prefix=None, priority=DEFAULT_PRIORITY, use_kwargs=False, defaults=None, config="config") ``` Resolve argument values using the function that calls FunctionArgInit as the reference. Process each argument from the calling function, resolving and storing the value in a dictionary, where the argument name is the key. ### Arguments ++ **priorities**: By default arguments will be set based on the priority sequence: config, env, arg, default. Several alternate priority sequences are predefined, or a custom sequence can be defined. ++ + **env_prefix**: env_prefix is used to avoid namespace clashes with environment variables. If set, all environment variables must include this prefix followed by an "_" character and the name of the argument. -+ **priority**: By default arguments will be set based on the priority config, env, arg, default. An alternate priority of arg, config, env, default is available by setting priority=ARG_PRIORITY. Alternatively, this can be specified by defining a tuple containing the required priority order. - + **use_kwargs**: When initialising arguments, only named arguments will be initialised by default. If use_kwargs=True, then any keyword arguments will also be initialised + **defaults**: A list of ArgDefault objects. @@ -73,3 +73,34 @@ A class that can be used to modify settings for an individual argument. + **env_name**: The name of the associated environment variable. If not set, env defaults to the uppercase equivalent of the argument name. + **default_value**: The default value to be applied if both arg and env values are not used. + +## Priorities + +### Priority Sequences + +A prioity sequence defines the resolution priority when resolving argument values. It is a list of Priority eunums. + +The following priority sequences are defined: + ++ CONFIG_PRIORITY = CONFIG, ENV, ARG, DEFAULT ++ ENV_PRIORITY = ENV, CONFIG, ARG, DEFAULT ++ ARG_PRIORITY = ARG, CONFIG, ENV, DEFAULT + +DEFAULT_PRIORITY = CONFIG_PRIORITY + +The following Priority values are defined: + ++ Priority.CONFIG ++ Priority.ENV ++ Priority.ARG ++ Priority.DEFAULT + +These values can be used to define a custom priority sequence. If a Priority is omitted, then it will not be used in the resolution process. + +e.g. + +```python +priorities = list(Priority.ENV, Priority.ARG, Priority.DEFAULT) +``` + +Will define a priority sequence that does not use a config file during the resolution process. diff --git a/docs/usage.md b/docs/usage.md index ec728e6..1c4b8f1 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -183,3 +183,18 @@ def my_func(self, **kwargs): args = FunctionArgInit(use_kwargs=True) ... ``` + +### Using a Custom Prioirity Sequence + +A custom priority sequence can be defined. This can be used, for example, to disable a specific resolution feature. + +```python +from arg_init import FunctionArgInit, Priority + +def my_func(self, **kwargs): + priorities = list(Priority.ENV, Priority.ARG, Priority.DEFAULT) + args = FunctionArgInit(priorities=priorities) + ... +``` + +The example above disables the use of a config file during the resolution process. diff --git a/readme.md b/readme.md index d111b98..4386783 100644 --- a/readme.md +++ b/readme.md @@ -21,7 +21,7 @@ Because argument initialisation is implemented in the application, it will work ## Notes -ArgInit uses introspection (via the [inspect](https://docs.python.org/3/library/inspect.html) module) to determine function arguments and values. Its use is minimal and is only executed once at startup so performance should not be an issue. +arg_init uses introspection (via the [inspect](https://docs.python.org/3/library/inspect.html) module) to determine function arguments and values. Its use is minimal and is only executed once at startup so performance should not be an issue. It is not practical to dynamically determine if the function to be processed is a bound function (a class method, with a class reference (self) as the first parameter) or an unbound function (a simple function), so selection is determined by the use of the called class: ClassArgInit of FunctionArgInit. @@ -29,11 +29,11 @@ Fucntionality is identical for both implementations, with the following exceptio ClassArgInit: -- Class attributes may be set that represent the resolved argument values +- Class attributes are set (may be optionally disabled) that represent the resolved argument values ## Priority -The argument value is set when a value is found , or all options are exhausted. At this point the argument is set to None. +The argument value is set when a non **None** value is found, or all options are exhausted. At this point the argument is set to None. What priority should be used to set an argument? @@ -55,11 +55,9 @@ The problem: How to avoid violating the DRY principle when an application can be If an application is to be called as a library then the defaults MUST be implemented in the application, not the CLI script. But ArgumentParser will pass in None values if no value is specified for an argument. This None value will be used in preference to function default! So defaults must be specified in ArgumentParser and the applicication. This is not a good design pattern. Providing an alternate means to specify a default value resolves this. -There is a small gotcha here though. It is not possible to apply a non None value via an argument. - **arg-init** supports customisable priority models. -This becomes a personal choice, and behaviour can be chosen at implementation/run time. +It is left to the user to select an appropriate priority sequence (or use the default option) for each specfic use case. ### Default Priority Order From 3a24ccbbcedb5e8876fab42fd7bb4b3f651e89de Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Fri, 10 Nov 2023 22:50:08 +0000 Subject: [PATCH 23/28] chore: use annotation alias from _alias.py --- src/arg_init/_arg.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/arg_init/_arg.py b/src/arg_init/_arg.py index 70d1fa6..9db6756 100644 --- a/src/arg_init/_arg.py +++ b/src/arg_init/_arg.py @@ -5,12 +5,11 @@ from typing import Any import logging +from ._aliases import Priorities from ._priority import Priority from ._values import Values logger = logging.getLogger(__name__) -# Typing aliases -Priorities = tuple[Priority, Priority, Priority, Priority] class Arg: From 3c90c566db9e0e54e35dd73de80f1bf879649133 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:45:44 +0000 Subject: [PATCH 24/28] feat: Use ruff to lint code --- .vscode/settings.json | 2 +- pdm.lock | 32 ++++++++++-- pyproject.toml | 81 ++++++++++++++++++++++++++++++ src/arg_init/__init__.py | 8 +-- src/arg_init/_aliases.py | 12 ++--- src/arg_init/_arg.py | 24 ++++----- src/arg_init/_arg_defaults.py | 16 ++---- src/arg_init/_arg_init.py | 73 ++++++++++++--------------- src/arg_init/_class_arg_init.py | 42 +++++++--------- src/arg_init/_config.py | 17 ++++--- src/arg_init/_enums.py | 21 ++++++++ src/arg_init/_exceptions.py | 9 ++++ src/arg_init/_function_arg_init.py | 19 ++++--- src/arg_init/_priority.py | 8 +-- src/arg_init/_values.py | 9 ++-- src/arg_init/_version.py | 4 +- tests/test_file_configs.py | 55 +++++++++++++++----- 17 files changed, 283 insertions(+), 149 deletions(-) create mode 100644 src/arg_init/_enums.py create mode 100644 src/arg_init/_exceptions.py diff --git a/.vscode/settings.json b/.vscode/settings.json index d99f2f3..89a1af7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter" + "editor.defaultFormatter": "charliermarsh.ruff" }, "python.formatting.provider": "none" } \ No newline at end of file diff --git a/pdm.lock b/pdm.lock index 090cd84..224c06b 100644 --- a/pdm.lock +++ b/pdm.lock @@ -3,10 +3,9 @@ [metadata] groups = ["default", "test", "lint", "docs"] -cross_platform = true -static_urls = false -lock_version = "4.3" -content_hash = "sha256:680db50e3762184abf8cd28794b690b5194ac70989ee677fb12a071cee93aa8f" +strategy = ["cross_platform"] +lock_version = "4.4" +content_hash = "sha256:7abbb986689d7b996e0fd1b96dab3998cda7c546e79cb9b7230ba3b2e1835d4d" [[package]] name = "annotated-types" @@ -794,6 +793,31 @@ files = [ {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] +[[package]] +name = "ruff" +version = "0.1.5" +requires_python = ">=3.7" +summary = "An extremely fast Python linter and code formatter, written in Rust." +files = [ + {file = "ruff-0.1.5-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:32d47fc69261c21a4c48916f16ca272bf2f273eb635d91c65d5cd548bf1f3d96"}, + {file = "ruff-0.1.5-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:171276c1df6c07fa0597fb946139ced1c2978f4f0b8254f201281729981f3c17"}, + {file = "ruff-0.1.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17ef33cd0bb7316ca65649fc748acc1406dfa4da96a3d0cde6d52f2e866c7b39"}, + {file = "ruff-0.1.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b2c205827b3f8c13b4a432e9585750b93fd907986fe1aec62b2a02cf4401eee6"}, + {file = "ruff-0.1.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb408e3a2ad8f6881d0f2e7ad70cddb3ed9f200eb3517a91a245bbe27101d379"}, + {file = "ruff-0.1.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f20dc5e5905ddb407060ca27267c7174f532375c08076d1a953cf7bb016f5a24"}, + {file = "ruff-0.1.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aafb9d2b671ed934998e881e2c0f5845a4295e84e719359c71c39a5363cccc91"}, + {file = "ruff-0.1.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4894dddb476597a0ba4473d72a23151b8b3b0b5f958f2cf4d3f1c572cdb7af7"}, + {file = "ruff-0.1.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00a7ec893f665ed60008c70fe9eeb58d210e6b4d83ec6654a9904871f982a2a"}, + {file = "ruff-0.1.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8c11206b47f283cbda399a654fd0178d7a389e631f19f51da15cbe631480c5b"}, + {file = "ruff-0.1.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fa29e67b3284b9a79b1a85ee66e293a94ac6b7bb068b307a8a373c3d343aa8ec"}, + {file = "ruff-0.1.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9b97fd6da44d6cceb188147b68db69a5741fbc736465b5cea3928fdac0bc1aeb"}, + {file = "ruff-0.1.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:721f4b9d3b4161df8dc9f09aa8562e39d14e55a4dbaa451a8e55bdc9590e20f4"}, + {file = "ruff-0.1.5-py3-none-win32.whl", hash = "sha256:f80c73bba6bc69e4fdc73b3991db0b546ce641bdcd5b07210b8ad6f64c79f1ab"}, + {file = "ruff-0.1.5-py3-none-win_amd64.whl", hash = "sha256:c21fe20ee7d76206d290a76271c1af7a5096bc4c73ab9383ed2ad35f852a0087"}, + {file = "ruff-0.1.5-py3-none-win_arm64.whl", hash = "sha256:82bfcb9927e88c1ed50f49ac6c9728dab3ea451212693fe40d08d314663e412f"}, + {file = "ruff-0.1.5.tar.gz", hash = "sha256:5cbec0ef2ae1748fb194f420fb03fb2c25c3258c86129af7172ff8f198f125ab"}, +] + [[package]] name = "six" version = "1.16.0" diff --git a/pyproject.toml b/pyproject.toml index fd098aa..b10d674 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ test = [ "mypy>=1.6.1", "types-PyYAML>=6.0.12.12", "pydantic>=2.4.2", + "ruff>=0.1.5", ] docs = [ "mkdocs>=1.5.3", @@ -68,6 +69,7 @@ testpaths = [ "tests", ] + [tool.mypy] plugins = [ "pydantic.mypy" @@ -90,3 +92,82 @@ init_typed = true warn_required_dynamic_aliases = true show-error-content = true + + +[tool.ruff] +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + # "tests/**" +] + +# Same as Black. +line-length = 120 +indent-width = 4 + +# Assume Python 3.8 +target-version = "py311" + +[tool.ruff.lint] +exclude = [ + "tests/**" +] +# select = [ +# "A", # prevent using keywords that clobber python builtins +# "B", # bugbear: security warnings +# "E", # pycodestyle +# "F", # pyflakes +# "ISC", # implicit string concatenation +# "UP", # alert you when better syntax is available in your python version +# "RUF", # the ruff developer's own rules +# ] +select = ["ALL"] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# select = ["E4", "E7", "E9", "F"] +ignore = ["ANN002", "ANN003", "ANN101", "ANN102", "D203", "D212", "COM812", "ISC001", "D205"] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.per-file-ignores] +"__init__.py" = ["D104"] + + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + + diff --git a/src/arg_init/__init__.py b/src/arg_init/__init__.py index 2d714d4..8e0b6fa 100644 --- a/src/arg_init/__init__.py +++ b/src/arg_init/__init__.py @@ -1,13 +1,14 @@ # pylint: disable=missing-module-docstring -from ._class_arg_init import ClassArgInit from ._arg_defaults import ArgDefaults +from ._class_arg_init import ClassArgInit +from ._exceptions import UnsupportedFileFormatError from ._function_arg_init import FunctionArgInit from ._priority import ( - Priority, + ARG_PRIORITY, CONFIG_PRIORITY, ENV_PRIORITY, - ARG_PRIORITY, + Priority, ) # External API @@ -19,4 +20,5 @@ "CONFIG_PRIORITY", "ENV_PRIORITY", "ARG_PRIORITY", + "UnsupportedFileFormatError", ] diff --git a/src/arg_init/_aliases.py b/src/arg_init/_aliases.py index f3f3c11..7dcffaf 100644 --- a/src/arg_init/_aliases.py +++ b/src/arg_init/_aliases.py @@ -1,12 +1,12 @@ -""" -mypy type aliases -""" +"""mypy type aliases.""" -from typing import Any, Optional, Callable +from collections.abc import Callable +from typing import Any from ._arg_defaults import ArgDefaults from ._priority import Priority -Defaults = Optional[list[ArgDefaults]] -Priorities= tuple[Priority, Priority, Priority, Priority] ClassCallback = Callable[[Any], None] +Defaults = list[ArgDefaults] | None +LoaderCallback = Callable[[Any], dict[Any, Any]] +Priorities = tuple[Priority, Priority, Priority, Priority] diff --git a/src/arg_init/_arg.py b/src/arg_init/_arg.py index 9db6756..6e53b62 100644 --- a/src/arg_init/_arg.py +++ b/src/arg_init/_arg.py @@ -1,9 +1,7 @@ -""" -Data Class used to customise ArgInit behaviour -""" +"""Class to represent an Argument.""" -from typing import Any import logging +from typing import Any from ._aliases import Priorities from ._priority import Priority @@ -15,7 +13,7 @@ class Arg: """Class to represent argument attributes.""" - _mapping = { + _mapping = { # noqa: RUF012 Priority.CONFIG: "config", Priority.ENV: "env", Priority.ARG: "arg", @@ -35,7 +33,7 @@ def __init__( self._values = values self._value = None - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """When testing for equality, test only the value attribute.""" return self.value == other @@ -60,18 +58,18 @@ def name(self) -> str: return self._name @property - def value(self) -> Any: + def value(self) -> object | None: """Resolved value of Arg.""" return self._value @property def env_name(self) -> str | None: - """env attribute.""" + """Env attribute.""" return self._env_name @property def config_name(self) -> str | None: - """env attribute.""" + """Config_name attribute.""" return self._config_name @property @@ -79,10 +77,8 @@ def values(self) -> Values | None: """Values to use when resolving Arg.""" return self._values - def resolve(self, name: str, priority_order: Priorities) -> Any: - """ - Resolve the value Arg using the selected priority system. - """ + def resolve(self, name: str, priority_order: Priorities) -> object | None: + """Resolve the value Arg using the selected priority system.""" logger.debug("Resolving value for %s", repr(self)) for priority in priority_order: logger.debug("Checking %s value", priority) @@ -93,5 +89,5 @@ def resolve(self, name: str, priority_order: Priorities) -> Any: break return self - def _get_value(self, priority: Priority) -> Any: + def _get_value(self, priority: Priority) -> Any | None: # noqa: ANN401 return getattr(self._values, self._mapping[priority]) diff --git a/src/arg_init/_arg_defaults.py b/src/arg_init/_arg_defaults.py index ea78c0b..0a3880c 100644 --- a/src/arg_init/_arg_defaults.py +++ b/src/arg_init/_arg_defaults.py @@ -1,7 +1,4 @@ -""" -Dataclass torepresent argument defaults that may be overridden -on a per argument basis. -""" +"""Dataclass torepresent argument defaults that may be overridden on a per argument basis.""" from dataclasses import dataclass from typing import Any @@ -9,18 +6,11 @@ @dataclass class ArgDefaults: - """ - Dataclass to represent argument defaults that may be overridden - on a per argument basis. - """ + """Dataclass to represent argument defaults that may be overridden on a per argument basis.""" name: str default_value: Any | None = None alt_name: str | None = None def __repr__(self) -> str: - return ( - f"" - ) + return f"" diff --git a/src/arg_init/_arg_init.py b/src/arg_init/_arg_init.py index 596caef..15ce2eb 100644 --- a/src/arg_init/_arg_init.py +++ b/src/arg_init/_arg_init.py @@ -2,12 +2,13 @@ Class to process arguments, environment variables and return a set of processed attribute values. """ +import logging from abc import ABC, abstractmethod -from inspect import stack, FrameInfo +from collections.abc import Mapping +from inspect import ArgInfo, FrameInfo, stack from os import environ from pathlib import Path -from typing import Any, Optional, Mapping -import logging +from typing import Any from box import Box @@ -15,10 +16,10 @@ from ._arg import Arg from ._arg_defaults import ArgDefaults from ._config import read_config -from ._priority import Priority, DEFAULT_PRIORITY +from ._enums import UseKWArgs +from ._priority import DEFAULT_PRIORITY, Priority from ._values import Values - logger = logging.getLogger(__name__) @@ -30,14 +31,15 @@ class ArgInit(ABC): STACK_LEVEL_OFFSET = 0 # Overridden by concrete class - def __init__( # pylint: disable=unused-argument + def __init__( # noqa: PLR0913 self, + # *, priorities: Priorities = DEFAULT_PRIORITY, - env_prefix: Optional[str] = None, - use_kwargs: bool = False, + env_prefix: str | None = None, + use_kwargs: UseKWArgs = UseKWArgs.FALSE, defaults: Defaults = None, config_name: str | Path = "config", - **kwargs: dict[Any, Any], # pylint: disable=unused-argument + **kwargs: dict[Any, Any], # noqa: ARG002 ) -> None: self._env_prefix = env_prefix self._priorities = priorities @@ -54,32 +56,31 @@ def args(self) -> Box: return self._args @abstractmethod - def _get_arguments(self, frame: Any, use_kwargs: bool) -> dict[Any, Any]: + def _get_arguments(self, frame: object, use_kwargs: UseKWArgs) -> dict[str, object]: """ - Returns a dictionary containing key value pairs of all + Return a dictionary containing key value pairs of all named arguments and their values associated with the frame. """ - raise RuntimeError() # pragma no cover + raise RuntimeError # pragma no cover @abstractmethod def _get_name(self, calling_stack: FrameInfo) -> str: - """ - Return the name of the item having arguments initialised. - """ - raise RuntimeError() # pragma no cover + """Return the name of the item having arguments initialised.""" + raise RuntimeError # pragma no cover - # @abstractmethod + @abstractmethod def _post_init(self, calling_stack: FrameInfo) -> None: """ Class specific post initialisation actions. + This can optionally be overridden by derived classes """ - def _init_args( + def _init_args( # noqa: PLR0913 self, name: str, calling_stack: FrameInfo, - use_kwargs: bool, + use_kwargs: UseKWArgs, defaults: Defaults, config: dict[Any, Any], ) -> None: @@ -88,9 +89,10 @@ def _init_args( arguments = self._get_arguments(calling_stack.frame, use_kwargs) self._make_args(arguments, defaults, config) - def _get_kwargs(self, arginfo: Any, use_kwargs: bool) -> dict[Any, Any]: + def _get_kwargs(self, arginfo: ArgInfo, use_kwargs: UseKWArgs) -> dict[Any, Any]: """ Return a dictionary containing kwargs to be resolved. + Returns an empty dictionary if use_kwargs=False """ if use_kwargs and arginfo.keywords: @@ -99,9 +101,7 @@ def _get_kwargs(self, arginfo: Any, use_kwargs: bool) -> dict[Any, Any]: return dict(arginfo.locals[keywords].items()) return {} - def _make_args( - self, arguments: dict[Any, Any], defaults: Defaults, config: Mapping[Any, Any] - ) -> None: + def _make_args(self, arguments: dict[Any, Any], defaults: Defaults, config: Mapping[Any, Any]) -> None: for name, value in arguments.items(): arg_defaults = self._get_arg_defaults(name, defaults) config_name = self._get_config_name(name, arg_defaults) @@ -113,13 +113,9 @@ def _make_args( config=self._get_config_value(config, config_name), default=default_value, ) - self._args[name] = Arg(name, env_name, config_name, values).resolve( - name, self._priorities - ) + self._args[name] = Arg(name, env_name, config_name, values).resolve(name, self._priorities) - def _get_arg_defaults( - self, name: str, defaults: Defaults - ) -> ArgDefaults | None: + def _get_arg_defaults(self, name: str, defaults: Defaults) -> ArgDefaults | None: """Check if any defaults exist for the named arg.""" if defaults: for arg_defaults in defaults: @@ -146,14 +142,10 @@ def _construct_env_name(env_prefix: str | None, name: str) -> str: return "_".join(env_parts).upper() @classmethod - def _get_env_name( - cls, env_prefix: str | None, name: str, arg_defaults: ArgDefaults | None - ) -> str: + def _get_env_name(cls, env_prefix: str | None, name: str, arg_defaults: ArgDefaults | None) -> str: """Determine the name to use for the env.""" alt_name = cls._get_alt_name(arg_defaults) - return ( - alt_name if alt_name else cls._construct_env_name(env_prefix, name) - ).upper() + return (alt_name if alt_name else cls._construct_env_name(env_prefix, name)).upper() @staticmethod def _get_value(name: str, dictionary: Mapping[Any, Any]) -> str | None: @@ -166,7 +158,7 @@ def _get_value(name: str, dictionary: Mapping[Any, Any]) -> str | None: return None @classmethod - def _get_config_value(cls, config: Mapping[Any, Any], name: str) -> Any: + def _get_config_value(cls, config: Mapping[Any, Any], name: str) -> object: logger.debug("Searching config for: %s", name) return cls._get_value(name, config) @@ -176,7 +168,7 @@ def _get_env_value(cls, name: str) -> str | None: return cls._get_value(name, environ) @staticmethod - def _get_default_value(arg_defaults: ArgDefaults | None) -> Any: + def _get_default_value(arg_defaults: ArgDefaults | None) -> object: if arg_defaults: return arg_defaults.default_value return None @@ -189,12 +181,11 @@ def _read_config( ) -> dict[Any, Any]: if Priority.CONFIG in priorities: config = read_config(config_name) - if config: - logger.debug("Checking for section '%s' in config file", section_name) - if section_name in config: + logger.debug("Checking for section '%s' in config file", section_name) + if config and section_name in config: logger.debug("config=%s", config[section_name]) return config[section_name] - logger.debug("No section '%s' data found", section_name) + logger.debug("No section '%s' data found", section_name) return {} logger.debug("skipping file based config based on priorities") return {} diff --git a/src/arg_init/_class_arg_init.py b/src/arg_init/_class_arg_init.py index 5139981..e0d8895 100644 --- a/src/arg_init/_class_arg_init.py +++ b/src/arg_init/_class_arg_init.py @@ -1,38 +1,38 @@ -""" -Class to initialise Argument Values for a Class Method -""" +"""Class to initialise Argument Values for a Class Method.""" -from inspect import getargvalues, FrameInfo -from pathlib import Path -from typing import Any, Optional import logging +from inspect import FrameInfo, getargvalues +from pathlib import Path +from typing import Any -from ._aliases import Defaults, Priorities, ClassCallback +from ._aliases import ClassCallback, Defaults, Priorities from ._arg_init import ArgInit +from ._enums import ProtectAttrs, SetAttrs, UseKWArgs from ._priority import DEFAULT_PRIORITY - logger = logging.getLogger(__name__) class ClassArgInit(ArgInit): """ Initialises arguments from a class method (Not a simple function). + The first parameter of the calling function must be a class instance i.e. an argument named "self" """ STACK_LEVEL_OFFSET = 2 # The calling frame is 2 layers up - def __init__( + def __init__( # noqa: PLR0913 self, + # *, priorities: Priorities = DEFAULT_PRIORITY, - env_prefix: Optional[str] = None, - use_kwargs: bool = False, + env_prefix: str | None = None, + use_kwargs: UseKWArgs = UseKWArgs.FALSE, defaults: Defaults = None, config_name: str | Path = "config", - set_attrs: bool = True, - protect_attrs: bool = True, + set_attrs: SetAttrs = SetAttrs.TRUE, + protect_attrs: ProtectAttrs = ProtectAttrs.TRUE, **kwargs: dict[Any, Any], # pylint: disable=unused-argument ) -> None: self._set_attrs = set_attrs @@ -44,19 +44,15 @@ def _post_init(self, calling_stack: FrameInfo) -> None: class_instance = self._get_class_instance(calling_stack.frame) self._set_class_arg_attrs(class_instance) - def _get_arguments(self, frame: Any, use_kwargs: bool) -> dict[Any, Any]: + def _get_arguments(self, frame: Any, use_kwargs: UseKWArgs) -> dict[Any, Any]: # noqa: ANN401 """ - Returns a dictionary containing key value pairs of all + Return a dictionary containing key value pairs of all named arguments for the specified frame. The first argument is skipped as this is a reference to the class instance. """ arginfo = getargvalues(frame) - args = { - arg: arginfo.locals.get(arg) - for count, arg in enumerate(arginfo.args) - if count > 0 - } + args = {arg: arginfo.locals.get(arg) for count, arg in enumerate(arginfo.args) if count > 0} args.update(self._get_kwargs(arginfo, use_kwargs)) return args @@ -72,15 +68,15 @@ def _get_attr_name(self, name: str) -> str: return name if name.startswith("_") else "_" + name return name - def _set_attr(self, class_instance: ClassCallback, name: str, value: Any) -> None: + def _set_attr(self, class_instance: ClassCallback, name: str, value: object) -> None: name = self._get_attr_name(name) if hasattr(class_instance, name): - raise AttributeError(f"Attribute already exists: {name}") + raise AttributeError(name=name, obj=class_instance) logger.debug(" %s = %s", name, value) setattr(class_instance, name, value) @staticmethod - def _get_class_instance(frame: Any) -> ClassCallback: + def _get_class_instance(frame: Any) -> ClassCallback: # noqa: ANN401 """ Return the value of the 1st argument from the calling function. This should be the class instance. diff --git a/src/arg_init/_config.py b/src/arg_init/_config.py index 461c2b9..4f83872 100644 --- a/src/arg_init/_config.py +++ b/src/arg_init/_config.py @@ -1,5 +1,5 @@ """ -Helper module to read a config file +Helper module to read a config file. Supported formats are: - JSON @@ -7,19 +7,20 @@ - YAML """ -from pathlib import Path +import logging from json import load as json_load +from pathlib import Path from tomllib import load as toml_load -# from typing import Callable, Any, SupportsRead, DefaultNamedArg -from typing import Callable, Any -import logging +from typing import Any from yaml import safe_load as yaml_safe_load +from ._aliases import LoaderCallback +from ._exceptions import UnsupportedFileFormatError logger = logging.getLogger(__name__) FORMATS = ["yaml", "toml", "json"] -LoaderCallback = Callable[[Any], dict[Any, Any]] + def _yaml_loader() -> LoaderCallback: return yaml_safe_load @@ -42,7 +43,7 @@ def _get_loader(path: Path) -> LoaderCallback: case ".toml": return _toml_loader() case _: - raise RuntimeError(f"Unsupported file format: {path.suffix}") + raise UnsupportedFileFormatError(path.suffix) def _find_config(file: str | Path) -> Path | None: @@ -66,6 +67,6 @@ def read_config(file: str | Path) -> dict[Any, Any] | None: path = _find_config(file) if path: loader = _get_loader(path) - with open(path, "rb") as f: + with Path.open(path, "rb") as f: return loader(f) return None diff --git a/src/arg_init/_enums.py b/src/arg_init/_enums.py new file mode 100644 index 0000000..e10883f --- /dev/null +++ b/src/arg_init/_enums.py @@ -0,0 +1,21 @@ +"""Enums used by arg_init.""" + +from enum import Enum + + +class UseKWArgs(Enum): + # Use 0 as 1st enum to allow simple boolean eqivalence test + FALSE = False + TRUE = True + + +class SetAttrs(Enum): + # Use 0 as 1st enum to allow simple boolean eqivalence test + FALSE = False + TRUE = True + + +class ProtectAttrs(Enum): + # Use 0 as 1st enum to allow simple boolean eqivalence test + FALSE = False + TRUE = True diff --git a/src/arg_init/_exceptions.py b/src/arg_init/_exceptions.py new file mode 100644 index 0000000..50d0cb0 --- /dev/null +++ b/src/arg_init/_exceptions.py @@ -0,0 +1,9 @@ +"""Exceptions raised by arg-init.""" + +from typing import Any + + +class UnsupportedFileFormatError(Exception): + def __init__(self, suffix: str, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 + msg = f"Unsupported file format: {suffix}" + super().__init__(msg, *args, **kwargs) diff --git a/src/arg_init/_function_arg_init.py b/src/arg_init/_function_arg_init.py index 521f8ff..c63fded 100644 --- a/src/arg_init/_function_arg_init.py +++ b/src/arg_init/_function_arg_init.py @@ -1,27 +1,23 @@ -""" -Class to initialise Argument Values for a Function - -""" +"""Class to initialise Argument Values for a Function.""" +import logging from inspect import FrameInfo, getargvalues from typing import Any -import logging from ._arg_init import ArgInit +from ._enums import UseKWArgs logger = logging.getLogger(__name__) class FunctionArgInit(ArgInit): - """ - Initialises arguments from a function. - """ + """Initialises arguments from a function.""" STACK_LEVEL_OFFSET = 1 # The calling frame is 2 layers up - def _get_arguments(self, frame: Any, use_kwargs: bool) -> dict[Any, Any]: + def _get_arguments(self, frame: Any, use_kwargs: UseKWArgs) -> dict[str, object]: # noqa: ANN401 """ - Returns a dictionary containing key value pairs of all + Return a dictionary containing key value pairs of all named arguments and their values associated with the frame. """ arginfo = getargvalues(frame) @@ -29,5 +25,8 @@ def _get_arguments(self, frame: Any, use_kwargs: bool) -> dict[Any, Any]: args.update(self._get_kwargs(arginfo, use_kwargs)) return args + def _post_init(self, calling_stack: FrameInfo) -> None: + pass + def _get_name(self, calling_stack: FrameInfo) -> str: return calling_stack.function diff --git a/src/arg_init/_priority.py b/src/arg_init/_priority.py index ecf2767..22ae86f 100644 --- a/src/arg_init/_priority.py +++ b/src/arg_init/_priority.py @@ -1,17 +1,17 @@ -""" -Enum to represent priorities supported by arg_init -""" +"""Enum to represent priorities supported by arg_init.""" from enum import Enum class Priority(Enum): - """Argument resolution priority""" + """Argument resolution priority.""" + CONFIG = 1 ENV = 2 ARG = 3 DEFAULT = 4 + # Pre-defined priorities # The user is free to create and use any priority order using the available options # defined in Priority diff --git a/src/arg_init/_values.py b/src/arg_init/_values.py index 3d6b5cf..457501f 100644 --- a/src/arg_init/_values.py +++ b/src/arg_init/_values.py @@ -1,15 +1,12 @@ -""" -Class to represent values used to resolve an argument. -""" +"""Class to represent values used to resolve an argument.""" from dataclasses import dataclass from typing import Any + @dataclass class Values: - """ - Possible values an argument could be resolved from - """ + """Possible values an argument could be resolved from.""" arg: Any = None env: str | None = None diff --git a/src/arg_init/_version.py b/src/arg_init/_version.py index cb54b71..3ea02fe 100644 --- a/src/arg_init/_version.py +++ b/src/arg_init/_version.py @@ -1,5 +1,3 @@ -""" -arg_init package version -""" +"""arg_init package version.""" __version__ = "0.0.6" diff --git a/tests/test_file_configs.py b/tests/test_file_configs.py index 5c60b25..347dedf 100644 --- a/tests/test_file_configs.py +++ b/tests/test_file_configs.py @@ -7,6 +7,7 @@ import pytest from arg_init import FunctionArgInit +from arg_init import UnsupportedFileFormatError class TestFileConfigs: @@ -18,55 +19,81 @@ def test_toml_file(self, fs): """ Test toml file can be used to initialise arguments """ + def test(arg1=None): # pylint: disable=unused-argument args = FunctionArgInit().args assert args["arg1"] == config1_value config1_value = "config1_value" - config = "[test]\n"\ - f"arg1='{config1_value}'" - fs.create_file("config.toml", contents=config) + contents = f"[test]\narg1='{config1_value}'" + fs.create_file("config.toml", contents=contents) test() def test_yaml_file(self, fs): """ Test toml file can be used to initialise arguments """ + def test(arg1=None): # pylint: disable=unused-argument args = FunctionArgInit().args assert args["arg1"] == config1_value config1_value = "config1_value" - config = "test:\n"\ - f" arg1: {config1_value}" - fs.create_file("config.yaml", contents=config) + contents = f"test:\n arg1: {config1_value}" + fs.create_file("config.yaml", contents=contents) test() def test_json_file(self, fs): """ Test toml file can be used to initialise arguments """ + def test(arg1=None): # pylint: disable=unused-argument args = FunctionArgInit().args assert args["arg1"] == config1_value config1_value = "config1_value" - config = '{"test": {"arg1": "config1_value"}}' - fs.create_file("config.json", contents=config) + contents = '{"test": {"arg1": "' + config1_value + '"}}' + fs.create_file("config.json", contents=contents) + test() + + def test_empty_file(self, fs): + """ + Test toml file with missing section is processed. + """ + + def test(arg1=None): # pylint: disable=unused-argument + args = FunctionArgInit().args + assert args["arg1"] == None + + contents = "" + fs.create_file("config.toml", contents=contents) test() + def test_empty_config_section(self, fs): + """ + Test toml file with missing section is processed. + """ + + def test(arg1=None): # pylint: disable=unused-argument + args = FunctionArgInit().args + assert args["arg1"] == None + + contents = "[test]\n" + fs.create_file("config.toml", contents=contents) + test() def test_named_file_as_string(self, fs): """ Test toml file can be used to initialise arguments """ + def test(arg1=None): # pylint: disable=unused-argument args = FunctionArgInit(config_name="named_file").args assert args["arg1"] == config1_value config1_value = "config1_value" - config = "[test]\n"\ - f"arg1='{config1_value}'" + config = "[test]\n" f"arg1='{config1_value}'" fs.create_file("named_file.toml", contents=config) test() @@ -74,14 +101,14 @@ def test_specified_file_as_path(self, fs): """ Test toml file can be used to initialise arguments """ + def test(arg1=None): # pylint: disable=unused-argument config_name = Path("named_file.toml") args = FunctionArgInit(config_name=config_name).args assert args["arg1"] == config1_value config1_value = "config1_value" - config = "[test]\n"\ - f"arg1='{config1_value}'" + config = "[test]\n" f"arg1='{config1_value}'" fs.create_file("named_file.toml", contents=config) test() @@ -89,11 +116,12 @@ def test_unsupported_format_raises_exception(self, fs): """ Test unsupported config file format raises an exception """ + def test(arg1=None): # pylint: disable=unused-argument config_name = Path("named_file.ini") FunctionArgInit(config_name=config_name) - with pytest.raises(RuntimeError): + with pytest.raises(UnsupportedFileFormatError): fs.create_file("named_file.ini") test() @@ -102,6 +130,7 @@ def test_missing_named_file_raises_exception(self, fs): # pylint: disable=unuse Test missing named config file raises an exception. When an alternate config file is specified, it MUST exist. """ + def test(arg1=None): # pylint: disable=unused-argument config_name = Path("missing_file.toml") FunctionArgInit(config_name=config_name) From 69f9eaf9d41129e0d770bfbb3f819a5ba114e25f Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:49:22 +0000 Subject: [PATCH 25/28] test: add ruff lint github action --- .github/workflows/lint.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..c6f2647 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +name: lint + +on: +- push +- workflow_call + +jobs: + build: + name: ruff + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup PDM + uses: pdm-project/setup-pdm@v3 + with: + python-version: 3.11 + cache: true + + - name: Install dependencies + run: pdm install -G test + + - name: Run Ruff check + run: pdm run ruff check . + + - name: Run Ruff format + run: pdm run ruff format . From e973e858fdc443ec666b42dc3748e9cfb689f1e7 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:49:43 +0000 Subject: [PATCH 26/28] chore: ruff format all source --- src/arg_init/_arg_init.py | 4 ++-- tests/test_arg_priority.py | 4 +--- tests/test_arguments.py | 8 ++------ tests/test_class_arg_init.py | 16 +++++++++++----- tests/test_env_priority.py | 12 ++++++------ tests/test_env_variants.py | 11 +++++++++-- tests/test_function_arg_init.py | 3 ++- tests/test_kwargs.py | 8 +++++--- tests/test_print.py | 8 ++------ 9 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/arg_init/_arg_init.py b/src/arg_init/_arg_init.py index 15ce2eb..0c20d07 100644 --- a/src/arg_init/_arg_init.py +++ b/src/arg_init/_arg_init.py @@ -183,8 +183,8 @@ def _read_config( config = read_config(config_name) logger.debug("Checking for section '%s' in config file", section_name) if config and section_name in config: - logger.debug("config=%s", config[section_name]) - return config[section_name] + logger.debug("config=%s", config[section_name]) + return config[section_name] logger.debug("No section '%s' data found", section_name) return {} logger.debug("skipping file based config based on priorities") diff --git a/tests/test_arg_priority.py b/tests/test_arg_priority.py index b44b56a..4cc24b6 100644 --- a/tests/test_arg_priority.py +++ b/tests/test_arg_priority.py @@ -44,9 +44,7 @@ def test_matrix(self, prefix, arg_value, envs, config, defaults, expected, fs): """ def test(arg1): # pylint: disable=unused-argument - args = FunctionArgInit( - env_prefix=prefix, defaults=defaults, priorities=ARG_PRIORITY - ).args + args = FunctionArgInit(env_prefix=prefix, defaults=defaults, priorities=ARG_PRIORITY).args assert args[expected.key] == expected.value if config: diff --git a/tests/test_arguments.py b/tests/test_arguments.py index cdf97b4..4022709 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -36,9 +36,7 @@ class TestArguments: (None, None, '{"test": {"arg1": ""}}', DEFAULTS, Expected("arg1", "")), ], ) - def test_logical_false_values( - self, arg_value, envs, config, defaults, expected, fs - ): + def test_logical_false_values(self, arg_value, envs, config, defaults, expected, fs): """ Priority Order 1. Test 0 argument @@ -49,9 +47,7 @@ def test_logical_false_values( """ def test(arg1): # pylint: disable=unused-argument - args = FunctionArgInit( - defaults=defaults, priority=PRIORITY_ORDER - ).args + args = FunctionArgInit(defaults=defaults, priority=PRIORITY_ORDER).args print(args[expected.key], expected.value) assert args[expected.key] == expected.value diff --git a/tests/test_class_arg_init.py b/tests/test_class_arg_init.py index da7e890..961f4b8 100644 --- a/tests/test_class_arg_init.py +++ b/tests/test_class_arg_init.py @@ -9,7 +9,7 @@ from arg_init import ClassArgInit -Expected = namedtuple('Expected', 'key value') +Expected = namedtuple("Expected", "key value") class TestClassArgInit: @@ -21,8 +21,10 @@ def test_class(self, fs): # pylint: disable=unused-argument """ Test ArgInit on a class method """ + class Test: """Test Class""" + def __init__(self, arg1): # pylint: disable=unused-argument ClassArgInit() assert self._arg1 == arg1_value # pylint: disable=no-member @@ -30,13 +32,14 @@ def __init__(self, arg1): # pylint: disable=unused-argument arg1_value = "arg1_value" Test(arg1_value) - def test_protect_attr_false_sets_attr(self, fs): # pylint: disable=unused-argument """ Test ArgInit on a class method """ + class Test: """Test Class""" + def __init__(self, arg1): # pylint: disable=unused-argument ClassArgInit(protect_attrs=False) assert self.arg1 == arg1_value # pylint: disable=no-member @@ -44,13 +47,14 @@ def __init__(self, arg1): # pylint: disable=unused-argument arg1_value = "arg1_value" Test(arg1_value) - def test_exception_raised_if_protected_attr_exists(self, fs): # pylint: disable=unused-argument """ Test exception raised if attempting to set an attribute that already exists """ + class Test: """Test Class""" + def __init__(self, arg1=None): # pylint: disable=unused-argument self._arg1 = "other_value" ClassArgInit() @@ -58,14 +62,15 @@ def __init__(self, arg1=None): # pylint: disable=unused-argument with pytest.raises(AttributeError): Test() - def test_exception_raised_if_non_protected_attr_exists(self, fs): # pylint: disable=unused-argument """ Test exception raised if attempting to set an attribute that already exists. Verify "_" is not used as a prefix to attr when protect_args=False. """ + class Test: """Test Class""" + def __init__(self, arg1=None): # pylint: disable=unused-argument self.arg1 = "other_value" ClassArgInit(protect_attrs=False) @@ -73,14 +78,15 @@ def __init__(self, arg1=None): # pylint: disable=unused-argument with pytest.raises(AttributeError): Test() - def test_set_attrs_false_does_not_set_attrs(self, fs): # pylint: disable=unused-argument """ Test exception raised if attempting to set an attribute that already exists. Verify "_" is not used as a prefix to attr when protect_args=False. """ + class Test: """Test Class""" + def __init__(self, arg1=None): # pylint: disable=unused-argument self.arg1 = "other_value" ClassArgInit(set_attrs=False) diff --git a/tests/test_env_priority.py b/tests/test_env_priority.py index 5bd397b..cd50d00 100644 --- a/tests/test_env_priority.py +++ b/tests/test_env_priority.py @@ -10,7 +10,7 @@ from arg_init import FunctionArgInit, ArgDefaults, Priority -Expected = namedtuple('Expected', 'key value') +Expected = namedtuple("Expected", "key value") # Common test defaults @@ -43,6 +43,7 @@ def test_priority(self, prefix, arg_value, envs, config, defaults, expected, fs) 3. Default is defined - Default is used 4. Nothing defined - None is used """ + def test(arg1): # pylint: disable=unused-argument args = FunctionArgInit(env_prefix=prefix, defaults=defaults).args assert args[expected.key] == expected.value @@ -54,7 +55,6 @@ def test(arg1): # pylint: disable=unused-argument mp.setenv(env, value) test(arg1=arg_value) - def test_function_default(self, fs): # pylint: disable=unused-argument """ Test function default is used if set and no arg passed in. @@ -67,11 +67,11 @@ def test(arg1="func_default"): # pylint: disable=unused-argument test() - def test_multiple_args(self, fs): # pylint: disable=unused-argument """ Test initialisation from args when no envs defined """ + def test(arg1, arg2): # pylint: disable=unused-argument """Test Class""" args = FunctionArgInit().args @@ -82,11 +82,11 @@ def test(arg1, arg2): # pylint: disable=unused-argument arg2_value = "arg2_value" test(arg1_value, arg2_value) - def test_multiple_envs(self, fs): # pylint: disable=unused-argument """ Test initialised from envs """ + def test(arg1, arg2): # pylint: disable=unused-argument """Test Class""" args = FunctionArgInit().args @@ -102,7 +102,6 @@ def test(arg1, arg2): # pylint: disable=unused-argument mp.setenv(env2, env2_value) test("arg1_value", "arg2_value") - def test_multiple_mixed(self, fs): # pylint: disable=unused-argument """ Test mixed initialisation @@ -110,9 +109,10 @@ def test_multiple_mixed(self, fs): # pylint: disable=unused-argument arg2 - env, arg = None arg3 - arg - env not set """ + def test(arg1, arg2, arg3): # pylint: disable=unused-argument """Test Class""" - args = FunctionArgInit().args + args = FunctionArgInit().args assert args["arg1"] == env1_value assert args["arg2"] == env2_value assert args["arg3"] == arg3_value diff --git a/tests/test_env_variants.py b/tests/test_env_variants.py index 2085cb6..ac5d7c9 100644 --- a/tests/test_env_variants.py +++ b/tests/test_env_variants.py @@ -8,7 +8,7 @@ from arg_init import FunctionArgInit, ArgDefaults -Expected = namedtuple('Expected', 'key value') +Expected = namedtuple("Expected", "key value") class TestEnvVariants: @@ -20,7 +20,13 @@ class TestEnvVariants: "prefix, arg_value, envs, defaults, expected", [ ("prefix", None, {"PREFIX_ARG1": "env1_value"}, None, Expected("arg1", "env1_value")), - ("prefix", None, {"ENV1": "env1_value"}, [ArgDefaults(name="arg1", alt_name="ENV1")], Expected("arg1", "env1_value")), + ( + "prefix", + None, + {"ENV1": "env1_value"}, + [ArgDefaults(name="arg1", alt_name="ENV1")], + Expected("arg1", "env1_value"), + ), ], ) def test_env_variants(self, prefix, arg_value, envs, defaults, expected, fs): # pylint: disable=unused-argument @@ -31,6 +37,7 @@ def test_env_variants(self, prefix, arg_value, envs, defaults, expected, fs): # 2. Default env_name (Prefix not used) - Env is used """ + def test(arg1): # pylint: disable=unused-argument args = FunctionArgInit(env_prefix=prefix, defaults=defaults).args assert args[expected.key] == expected.value diff --git a/tests/test_function_arg_init.py b/tests/test_function_arg_init.py index 9a952e6..40d4df9 100644 --- a/tests/test_function_arg_init.py +++ b/tests/test_function_arg_init.py @@ -7,7 +7,7 @@ from arg_init import FunctionArgInit -Expected = namedtuple('Expected', 'key value') +Expected = namedtuple("Expected", "key value") class TestFunctionArgInit: @@ -19,6 +19,7 @@ def test_function(self, fs): # pylint: disable=unused-argument """ Test FunctionArgInit """ + def test(arg1): # pylint: disable=unused-argument """Test Class""" arg_init = FunctionArgInit() diff --git a/tests/test_kwargs.py b/tests/test_kwargs.py index d5bbdf4..39da0f7 100644 --- a/tests/test_kwargs.py +++ b/tests/test_kwargs.py @@ -7,7 +7,7 @@ from arg_init import ClassArgInit, FunctionArgInit -Expected = namedtuple('Expected', 'key value') +Expected = namedtuple("Expected", "key value") class TestKwargs: @@ -19,6 +19,7 @@ def test_kwargs_not_used(self, fs): # pylint: disable=unused-argument """ Test kwargs are ignored if not explicity enabled """ + def test(arg1, **kwargs): # pylint: disable=unused-argument """Test Class""" args = FunctionArgInit(use_kwargs=True).args @@ -31,11 +32,11 @@ def test(arg1, **kwargs): # pylint: disable=unused-argument kwargs = {kwarg1: kwarg1_value} test(arg1_value, **kwargs) - def test_kwargs_used_for_function(self, fs): # pylint: disable=unused-argument """ Test kwargs are processed if enabled """ + def test(arg1, **kwargs): # pylint: disable=unused-argument """Test Class""" args = FunctionArgInit(use_kwargs=True).args @@ -48,13 +49,14 @@ def test(arg1, **kwargs): # pylint: disable=unused-argument kwargs = {kwarg1: kwarg1_value} test(arg1_value, **kwargs) - def test_kwargs_used_for_class(self, fs): # pylint: disable=unused-argument """ Test kwargs are processed if enabled """ + class Test: """Test Class""" + def __init__(self, arg1, **kwargs): # pylint: disable=unused-argument args = ClassArgInit(use_kwargs=True).args assert args["arg1"] == arg1_value diff --git a/tests/test_print.py b/tests/test_print.py index 6e943f9..4de38a9 100644 --- a/tests/test_print.py +++ b/tests/test_print.py @@ -51,12 +51,8 @@ def test_defaults_repr(self, fs): # pylint: disable=unused-argument Test repr() returns correct string """ - arg1_defaults = ArgDefaults( - name="arg1", default_value="default", alt_name="ENV" - ) + arg1_defaults = ArgDefaults(name="arg1", default_value="default", alt_name="ENV") defaults = [arg1_defaults] out = repr(defaults) - expected = ( - "" - ) + expected = "" assert expected in out From 15450a16e1a85ee692159a4b8b8bc1aa5c00e8ad Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Mon, 13 Nov 2023 22:25:12 +0000 Subject: [PATCH 27/28] docs: add Ruff badge --- readme.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/readme.md b/readme.md index 4386783..cb9a88e 100644 --- a/readme.md +++ b/readme.md @@ -3,6 +3,7 @@ [![tests][tests_badge]][tests_url] [![codecov][codecov_badge]][codecov_url] [![mypy][mypy_badge]][mypy_url] +[![Ruff][ruff_badge]][ruff_url] [![Docs][docs_badge]][docs_url] [![PyPI][pypi_badge]][pypi_url] [![PyPI - License][license_badge]][license_url] @@ -152,6 +153,8 @@ Please see the [documentation](https://srfoster65.github.io/arg_init/) for furth [codecov_url]: https://codecov.io/gh/srfoster65/arg_init [mypy_badge]: https://github.com/srfoster65/arg_init/actions/workflows/mypy.yml/badge.svg [mypy_url]: https://github.com/srfoster65/arg_init/actions/workflows/mypy.yml +[ruff_badge]: https://github.com/srfoster65/arg_init/actions/workflows/ruff.yml/badge.svg +[ruff_url]: https://github.com/srfoster65/arg_init/actions/workflows/ruff.yml [docs_badge]: https://github.com/srfoster65/arg_init/actions/workflows/docs.yml/badge.svg [docs_url]: https://srfoster65.github.io/arg_init/ [pypi_badge]: https://img.shields.io/pypi/v/arg-init?logo=python&logoColor=%23cccccc From 931e866797cd8192126468a6e9af18a8df63b810 Mon Sep 17 00:00:00 2001 From: srfoster65 <135555068+srfoster65@users.noreply.github.com> Date: Mon, 13 Nov 2023 22:51:05 +0000 Subject: [PATCH 28/28] docs: fix broken link to ruff badge --- readme.md | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/readme.md b/readme.md index cb9a88e..49814b0 100644 --- a/readme.md +++ b/readme.md @@ -53,28 +53,35 @@ There are two obvious solutions to this: The problem: How to avoid violating the DRY principle when an application can be invoked via a CLI or as a library. -If an application is to be called as a library then the defaults MUST be implemented in the application, not the CLI script. But ArgumentParser will pass in None values if no value is specified for an argument. This None value will be used in preference to function default! So defaults must be specified in ArgumentParser and the applicication. This is not a good design pattern. +If an application is to be called as a library then the defaults MUST be implemented in the application, not the CLI script. But ArgumentParser will pass in None values if no value is specified for an argument. This None value will be used in preference to function default! So defaults must be also be specified in ArgumentParser and the applicication. This is not a good design pattern. Providing an alternate means to specify a default value resolves this. +### Priority Order + **arg-init** supports customisable priority models. It is left to the user to select an appropriate priority sequence (or use the default option) for each specfic use case. -### Default Priority Order +#### Default Priority Order The default priority implemented is: -- **CONFIG_PRIORITY** +**CONFIG_PRIORITY** + 1. Config - 1. Env - 1. Arg - 1. Default + 2. Env + 3. Arg + 4. Default + +#### Predefined Priority Orders Two further predifined priority models are provided - **ENV_PRIORITY** - **ARG_PRIOIRTY** +The user may also define a custom priority order if the predefined options are not suitable. + ## Usage ### Simple Useage @@ -85,7 +92,7 @@ The following examples show how to use arg_init to initialise a class or functio from arg_init import ClassArgInit class MyApp: - def __init__(self, arg1=10): + def __init__(self, arg1=None): ClassArgInit() ... ``` @@ -93,7 +100,7 @@ class MyApp: ```python from arg_init import FunctionArgInit -def func(arg1=10): +def func(arg1=None): FunctionArgInit() ... ``` @@ -121,14 +128,11 @@ The example below shows how to use argument priority when resolving the values o from arg_init import FunctionArgInit, ARG_PRIOIRITY, ArgDefaults def func(arg1=None): - arg1_defaults = ArgDefaults(default_value=1) - args = FunctionArgInit(priority=ARG_PRIORITY, defaults={"arg1": arg1_defaults}).args + arg1_defaults = ArgDefaults("arg1", default_value=1) + args = FunctionArgInit(priority=ARG_PRIORITY, defaults=[arg1_defaults]).args ... ``` -Note: -As this example uses argument priority, a default **must** be provided via ArgDefaults if the default is not None. - ### Recommendation To avoid namespace clashes with environment variables, it is recommneded to always supply an env_prefix argument when initialising ClassArgInit/FunctionArgInit. All environment variables are expected to have this prefix e.g. with an env_prefix of "myapp", arg1 would map to the environment variable "MYAPP_ARG1". @@ -153,8 +157,8 @@ Please see the [documentation](https://srfoster65.github.io/arg_init/) for furth [codecov_url]: https://codecov.io/gh/srfoster65/arg_init [mypy_badge]: https://github.com/srfoster65/arg_init/actions/workflows/mypy.yml/badge.svg [mypy_url]: https://github.com/srfoster65/arg_init/actions/workflows/mypy.yml -[ruff_badge]: https://github.com/srfoster65/arg_init/actions/workflows/ruff.yml/badge.svg -[ruff_url]: https://github.com/srfoster65/arg_init/actions/workflows/ruff.yml +[ruff_badge]: https://github.com/srfoster65/arg_init/actions/workflows/lint.yml/badge.svg +[ruff_url]: https://github.com/srfoster65/arg_init/actions/workflows/lint.yml [docs_badge]: https://github.com/srfoster65/arg_init/actions/workflows/docs.yml/badge.svg [docs_url]: https://srfoster65.github.io/arg_init/ [pypi_badge]: https://img.shields.io/pypi/v/arg-init?logo=python&logoColor=%23cccccc