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 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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