From 156059e3762d09c81638c65b544f02b2b15b212c Mon Sep 17 00:00:00 2001 From: Silvan Melchior Date: Fri, 14 Jul 2023 16:57:44 +0200 Subject: [PATCH 1/6] rename most important classes --- confz/__init__.py | 28 +++++----- confz/change.py | 8 +-- confz/confz.py | 27 +++++---- confz/confz_source.py | 14 ++--- confz/exceptions.py | 6 +- confz/loaders/cl_arg_loader.py | 4 +- confz/loaders/data_loader.py | 4 +- confz/loaders/env_loader.py | 6 +- confz/loaders/file_loader.py | 34 ++++++------ confz/loaders/loader.py | 10 ++-- confz/loaders/register.py | 32 +++++------ confz/validate.py | 6 +- tests/loaders/test_cl_arg_loader.py | 14 ++--- tests/loaders/test_env_loader.py | 34 ++++++------ tests/loaders/test_file_loader.py | 86 ++++++++++++++--------------- tests/loaders/test_loader.py | 30 +++++----- tests/test_change.py | 16 +++--- tests/test_confz.py | 24 ++++---- tests/test_validate.py | 16 +++--- 19 files changed, 199 insertions(+), 200 deletions(-) diff --git a/confz/__init__.py b/confz/__init__.py index e6a1fd2..9c37ff3 100644 --- a/confz/__init__.py +++ b/confz/__init__.py @@ -1,26 +1,26 @@ from .change import depends_on -from .confz import ConfZ +from .confz import BaseConfig from .confz_source import ( - ConfZSources, - ConfZSource, - ConfZFileSource, - ConfZEnvSource, - ConfZCLArgSource, + ConfigSources, + ConfigSource, + FileSource, + EnvSource, + CLArgSource, FileFormat, - ConfZDataSource, + DataSource, ) from .validate import validate_all_configs __all__ = [ "depends_on", - "ConfZ", - "ConfZSources", - "ConfZSource", - "ConfZFileSource", - "ConfZEnvSource", - "ConfZCLArgSource", + "BaseConfig", + "ConfigSources", + "ConfigSource", + "FileSource", + "EnvSource", + "CLArgSource", "FileFormat", - "ConfZDataSource", + "DataSource", "validate_all_configs", ] diff --git a/confz/change.py b/confz/change.py index e2c6ee4..7e0b109 100644 --- a/confz/change.py +++ b/confz/change.py @@ -12,17 +12,17 @@ TYPE_CHECKING, ) -from .confz_source import ConfZSources +from .confz_source import ConfigSources if TYPE_CHECKING: - from .confz import ConfZ + from .confz import BaseConfig class SourceChangeManager(AbstractContextManager): """Config sources change context manager, allows to change config sources within a controlled context and resets everything afterwards.""" - def __init__(self, config_class: Type["ConfZ"], config_sources: ConfZSources): + def __init__(self, config_class: Type["BaseConfig"], config_sources: ConfigSources): self._config_class = config_class self._config_sources = config_sources self._backup_instance = None @@ -56,7 +56,7 @@ def __exit__(self, exc_type, exc_value, traceback): class Listener(Generic[T]): """Listener of config, will add singleton mechanism, aware of config changes.""" - def __init__(self, fn: Callable[[], T], config_classes: List[Type["ConfZ"]]): + def __init__(self, fn: Callable[[], T], config_classes: List[Type["BaseConfig"]]): if len(inspect.getfullargspec(fn).args) != 0: raise ValueError("Callable should not take any arguments") diff --git a/confz/confz.py b/confz/confz.py index 80d8829..1d4f43b 100644 --- a/confz/confz.py +++ b/confz/confz.py @@ -6,12 +6,12 @@ from pydantic import BaseModel from .change import SourceChangeManager -from .confz_source import ConfZSources -from .exceptions import ConfZException +from .confz_source import ConfigSources +from .exceptions import ConfigException from .loaders import get_loader -def _load_config(config_kwargs: dict, confz_sources: ConfZSources) -> dict: +def _load_config(config_kwargs: dict, confz_sources: ConfigSources) -> dict: config = config_kwargs.copy() if isinstance(confz_sources, list): for confz_source in confz_sources: @@ -24,15 +24,14 @@ def _load_config(config_kwargs: dict, confz_sources: ConfZSources) -> dict: # Metaclass of pydantic.BaseModel is not in __all__, so use type(BaseModel). -# ConfZ will be only class with this meta class. -# Both of these things confuse mypy and pylint, so had to disable multiple times. -class ConfZMetaclass(type(BaseModel)): # type: ignore - """ConfZ Meta Class, inheriting from the pydantic `BaseModel` MetaClass.""" +# This confuses mypy and pylint, so had to disable multiple times. +class BaseConfigMetaclass(type(BaseModel)): # type: ignore + """BaseConfig Meta Class, inheriting from the pydantic `BaseModel` MetaClass.""" # pylint: disable=no-self-argument,no-member - def __call__(cls, config_sources: Optional[ConfZSources] = None, **kwargs): - """Called every time an instance of any ConfZ object is created. Injects the - config value population and singleton mechanism.""" + def __call__(cls, config_sources: Optional[ConfigSources] = None, **kwargs): + """Called every time an instance of any BaseConfig object is created. Injects + the config value population and singleton mechanism.""" if config_sources is not None: config = _load_config(kwargs, config_sources) return super().__call__(**config) @@ -41,7 +40,7 @@ def __call__(cls, config_sources: Optional[ConfZSources] = None, **kwargs): # pylint: disable=access-member-before-definition # pylint: disable=attribute-defined-outside-init if len(kwargs) > 0: - raise ConfZException( + raise ConfigException( 'Singleton mechanism enabled ("CONFIG_SOURCES" is defined), so ' "keyword arguments are not supported" ) @@ -53,7 +52,7 @@ def __call__(cls, config_sources: Optional[ConfZSources] = None, **kwargs): return super().__call__(**kwargs) -class ConfZ(BaseModel, metaclass=ConfZMetaclass, frozen=True): +class BaseConfig(BaseModel, metaclass=BaseConfigMetaclass, frozen=True): """Base class, parent of every config class. Internally wraps :class:`BaseModel`of pydantic and behaves transparent except for two cases: @@ -65,7 +64,7 @@ class ConfZ(BaseModel, metaclass=ConfZMetaclass, frozen=True): In the latter case, a singleton mechanism is activated, returning the same config class instance every time the constructor is called.""" - CONFIG_SOURCES: ClassVar[Optional[ConfZSources]] = None #: Sources to use as input. + CONFIG_SOURCES: ClassVar[Optional[ConfigSources]] = None #: Sources to use. # type is ClassVar[Optional["ConfZ"]] (pydantic throws error with forward ref) confz_instance: ClassVar[Optional[Any]] = None #: *for internal use only* @@ -75,7 +74,7 @@ class instance every time the constructor is called.""" @classmethod def change_config_sources( - cls, config_sources: ConfZSources + cls, config_sources: ConfigSources ) -> AbstractContextManager: """Change the `CONFIG_SOURCES` class variable within a controlled context. Within this context, the sources will be different and the singleton reset. diff --git a/confz/confz_source.py b/confz/confz_source.py index b0c857e..99da876 100644 --- a/confz/confz_source.py +++ b/confz/confz_source.py @@ -6,11 +6,11 @@ @dataclass -class ConfZSource: - """Source configuration for :class:`~confz.ConfZ` models.""" +class ConfigSource: + """Source configuration for :class:`~confz.BaseConfig` models.""" -ConfZSources = Union[ConfZSource, List[ConfZSource]] +ConfigSources = Union[ConfigSource, List[ConfigSource]] class FileFormat(Enum): @@ -22,7 +22,7 @@ class FileFormat(Enum): @dataclass -class ConfZFileSource(ConfZSource): +class FileSource(ConfigSource): """Source config for files.""" file: Union[PathLike, str, bytes, None] = None @@ -49,7 +49,7 @@ class ConfZFileSource(ConfZSource): @dataclass -class ConfZEnvSource(ConfZSource): +class EnvSource(ConfigSource): """Source config for environment variables and .env files. On loading of the source, the dotenv file values (if available) are merged with the environment, with environment always taking precedence in case of name collusion. All loaded @@ -81,7 +81,7 @@ class ConfZEnvSource(ConfZSource): @dataclass -class ConfZCLArgSource(ConfZSource): +class CLArgSource(ConfigSource): """Source config for command line arguments. Command line arguments are case-sensitive. Dot-notation can be used to access nested configurations. Only command line arguments starting with two dashes (\\-\\-) are considered. Between @@ -100,7 +100,7 @@ class ConfZCLArgSource(ConfZSource): @dataclass -class ConfZDataSource(ConfZSource): +class DataSource(ConfigSource): """Source config for raw data, i.e. constants. This can be useful for unit-test together with :meth:`~confz.ConfZ.change_config_sources` to inject test data into the config.""" diff --git a/confz/exceptions.py b/confz/exceptions.py index 9c4a986..b7587dd 100644 --- a/confz/exceptions.py +++ b/confz/exceptions.py @@ -1,11 +1,11 @@ -class ConfZException(Exception): +class ConfigException(Exception): """The base exception. All other exceptions inherit from it.""" -class ConfZUpdateException(ConfZException): +class UpdateException(ConfigException): """Exception which is raised if could not merge different config sources.""" -class ConfZFileException(ConfZException): +class FileException(ConfigException): """Exception which is raised if something went wrong while reading a configuration file.""" diff --git a/confz/loaders/cl_arg_loader.py b/confz/loaders/cl_arg_loader.py index 60a640e..d9a2836 100644 --- a/confz/loaders/cl_arg_loader.py +++ b/confz/loaders/cl_arg_loader.py @@ -1,6 +1,6 @@ import sys -from confz.confz_source import ConfZCLArgSource +from confz.confz_source import CLArgSource from .loader import Loader @@ -8,7 +8,7 @@ class CLArgLoader(Loader): """Config loader for command line arguments.""" @classmethod - def populate_config(cls, config: dict, confz_source: ConfZCLArgSource): + def populate_config(cls, config: dict, confz_source: CLArgSource): cl_args = {} for idx, cl_arg in enumerate(sys.argv[1:]): if cl_arg.startswith("--") and idx + 2 < len(sys.argv): diff --git a/confz/loaders/data_loader.py b/confz/loaders/data_loader.py index 5ef1eb4..0e9909c 100644 --- a/confz/loaders/data_loader.py +++ b/confz/loaders/data_loader.py @@ -1,4 +1,4 @@ -from confz.confz_source import ConfZDataSource +from confz.confz_source import DataSource from .loader import Loader @@ -6,5 +6,5 @@ class DataLoader(Loader): """Config loader for fix data.""" @classmethod - def populate_config(cls, config: dict, confz_source: ConfZDataSource): + def populate_config(cls, config: dict, confz_source: DataSource): cls.update_dict_recursively(config, confz_source.data) diff --git a/confz/loaders/env_loader.py b/confz/loaders/env_loader.py index 2869120..74ba787 100644 --- a/confz/loaders/env_loader.py +++ b/confz/loaders/env_loader.py @@ -4,7 +4,7 @@ from dotenv import dotenv_values -from confz.confz_source import ConfZEnvSource +from confz.confz_source import EnvSource from .loader import Loader @@ -28,7 +28,7 @@ def _transform_remap( return map_out @classmethod - def _check_allowance(cls, var_name: str, confz_source: ConfZEnvSource) -> bool: + def _check_allowance(cls, var_name: str, confz_source: EnvSource) -> bool: if not confz_source.allow_all: if confz_source.allow is None: return False @@ -45,7 +45,7 @@ def _check_allowance(cls, var_name: str, confz_source: ConfZEnvSource) -> bool: return True @classmethod - def populate_config(cls, config: dict, confz_source: ConfZEnvSource): + def populate_config(cls, config: dict, confz_source: EnvSource): remap = cls._transform_remap(confz_source.remap) origin_env_vars: Dict[str, Any] = dict(os.environ) diff --git a/confz/loaders/file_loader.py b/confz/loaders/file_loader.py index 751b026..ed7cf08 100644 --- a/confz/loaders/file_loader.py +++ b/confz/loaders/file_loader.py @@ -9,8 +9,8 @@ import toml import yaml -from confz.confz_source import ConfZFileSource, FileFormat -from confz.exceptions import ConfZFileException +from confz.confz_source import FileSource, FileFormat +from confz.exceptions import FileException from .loader import Loader @@ -18,14 +18,14 @@ class FileLoader(Loader): """Config loader for config files.""" @classmethod - def _get_filename(cls, confz_source: ConfZFileSource) -> Path: + def _get_filename(cls, confz_source: FileSource) -> Path: if confz_source.file is not None: if isinstance(confz_source.file, bytes): - raise ConfZFileException("Can not detect filename from type bytes") + raise FileException("Can not detect filename from type bytes") file_path = Path(confz_source.file) elif confz_source.file_from_env is not None: if confz_source.file_from_env not in os.environ: - raise ConfZFileException( + raise FileException( f"Environment variable '{confz_source.file_from_env}' is not set." ) file_path = Path(os.environ[confz_source.file_from_env]) @@ -34,7 +34,7 @@ def _get_filename(cls, confz_source: ConfZFileSource) -> Path: try: file_path = Path(sys.argv[confz_source.file_from_cl]) except IndexError as e: - raise ConfZFileException( + raise FileException( f"Command-line argument number {confz_source.file_from_cl} " f"is not set." ) from e @@ -42,19 +42,19 @@ def _get_filename(cls, confz_source: ConfZFileSource) -> Path: try: idx = sys.argv.index(confz_source.file_from_cl) except ValueError as e: - raise ConfZFileException( + raise FileException( f"Command-line argument '{confz_source.file_from_cl}' " f"not found." ) from e try: file_path = Path(sys.argv[idx + 1]) except IndexError as e: - raise ConfZFileException( + raise FileException( f"Command-line argument '{confz_source.file_from_cl}' is not " f"set." ) from e else: - raise ConfZFileException("No file source set.") + raise FileException("No file source set.") if confz_source.folder is not None: file_path = Path(confz_source.folder) / file_path @@ -78,7 +78,7 @@ def _get_format( try: suffix_format = suffix_formats[suffix] except KeyError as e: - raise ConfZFileException( + raise FileException( f"File-ending '{suffix}' is not known. Supported are: " f"{', '.join(list(suffix_formats.keys()))}." ) from e @@ -98,7 +98,7 @@ def _parse_stream( elif file_format == FileFormat.TOML: file_content = toml.load(stream) else: - raise ConfZFileException(f"Unknown file format {file_format}.") + raise FileException(f"Unknown file format {file_format}.") return file_content @classmethod @@ -107,7 +107,7 @@ def _create_stream(cls, file_path: Path, file_encoding: str) -> Iterator[TextIO] try: stream = file_path.open(encoding=file_encoding) except OSError as e: - raise ConfZFileException( + raise FileException( f"Could not open config file '{file_path}'." ) from e with stream: @@ -115,10 +115,10 @@ def _create_stream(cls, file_path: Path, file_encoding: str) -> Iterator[TextIO] @classmethod def _populate_config_from_bytes( - cls, config: dict, data: bytes, confz_source: ConfZFileSource + cls, config: dict, data: bytes, confz_source: FileSource ): if confz_source.format is None: - raise ConfZFileException( + raise FileException( "The format needs to be defined if the " "configuration is passed as byte-string" ) @@ -128,7 +128,7 @@ def _populate_config_from_bytes( cls.update_dict_recursively(config, file_content) @classmethod - def populate_config(cls, config: dict, confz_source: ConfZFileSource): + def populate_config(cls, config: dict, confz_source: FileSource): if confz_source.file is not None and isinstance(confz_source.file, bytes): cls._populate_config_from_bytes( config=config, data=confz_source.file, confz_source=confz_source @@ -136,7 +136,7 @@ def populate_config(cls, config: dict, confz_source: ConfZFileSource): return try: file_path = cls._get_filename(confz_source) - except ConfZFileException as e: + except FileException as e: if confz_source.optional: return raise e @@ -144,7 +144,7 @@ def populate_config(cls, config: dict, confz_source: ConfZFileSource): try: with cls._create_stream(file_path, confz_source.encoding) as file_stream: file_content = cls._parse_stream(file_stream, file_format) - except ConfZFileException as e: + except FileException as e: if confz_source.optional: return raise e diff --git a/confz/loaders/loader.py b/confz/loaders/loader.py index 22c1a25..57f8a58 100644 --- a/confz/loaders/loader.py +++ b/confz/loaders/loader.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from typing import Dict, Any -from confz.exceptions import ConfZUpdateException +from confz.exceptions import UpdateException class Loader(ABC): @@ -14,12 +14,12 @@ def update_dict_recursively(cls, original_dict: Dict, update_dict: Dict): :param original_dict: The original dictionary to update in-place. :param update_dict: The new data. - :raises ConfZUpdateException: If dict keys contradict each other. + :raises UpdateException: If dict keys contradict each other. """ for key, value in update_dict.items(): if isinstance(value, dict) and key in original_dict: if not isinstance(original_dict[key], dict): - raise ConfZUpdateException( + raise UpdateException( f"Config variables contradict each other: " f"Key '{key}' is both a value and a nested dict." ) @@ -39,7 +39,7 @@ def transform_nested_dicts( Default value will no longer be set in a future release. :return: The transformed dictionary, splitting keys at the separator and creating a new dictionary out of it. - :raises ConfZUpdateException: If dict keys contradict each other. + :raises UpdateException: If dict keys contradict each other. """ dict_out: Dict[str, Any] = {} for key, value in dict_in.items(): @@ -53,7 +53,7 @@ def transform_nested_dicts( if inner_key not in dict_inner: dict_inner[inner_key] = {} elif not isinstance(dict_inner[inner_key], dict): - raise ConfZUpdateException( + raise UpdateException( f"Config variables contradict each other: Key " f"'{inner_key}' is both a value and a nested dict." ) diff --git a/confz/loaders/register.py b/confz/loaders/register.py index e93542a..affdbe6 100644 --- a/confz/loaders/register.py +++ b/confz/loaders/register.py @@ -1,39 +1,39 @@ from typing import Type, Dict from confz.confz_source import ( - ConfZSource, - ConfZFileSource, - ConfZEnvSource, - ConfZCLArgSource, - ConfZDataSource, + ConfigSource, + FileSource, + EnvSource, + CLArgSource, + DataSource, ) -from confz.exceptions import ConfZException +from confz.exceptions import ConfigException from .cl_arg_loader import CLArgLoader from .data_loader import DataLoader from .env_loader import EnvLoader from .file_loader import FileLoader from .loader import Loader -_loaders: Dict[Type[ConfZSource], Type[Loader]] = {} +_loaders: Dict[Type[ConfigSource], Type[Loader]] = {} -def get_loader(confz_source: Type[ConfZSource]): +def get_loader(confz_source: Type[ConfigSource]): if confz_source in _loaders: return _loaders[confz_source] - raise ConfZException(f"Unknown config source type '{confz_source}'") + raise ConfigException(f"Unknown config source type '{confz_source}'") -def register_loader(confz_source: Type[ConfZSource], loader: Type[Loader]): - """Register a :class:`~confz.ConfZSource` with a specific loader. Can be used to +def register_loader(confz_source: Type[ConfigSource], loader: Type[Loader]): + """Register a :class:`~confz.ConfigSource` with a specific loader. Can be used to extend `ConfZ` with own loaders. - :param confz_source: The :class:`~confz.ConfZSource` sub-type. + :param confz_source: The :class:`~confz.ConfigSource` sub-type. :param loader: The :class:`~confz.loaders.Loader` sub-type. """ _loaders[confz_source] = loader -register_loader(ConfZFileSource, FileLoader) -register_loader(ConfZEnvSource, EnvLoader) -register_loader(ConfZCLArgSource, CLArgLoader) -register_loader(ConfZDataSource, DataLoader) +register_loader(FileSource, FileLoader) +register_loader(EnvSource, EnvLoader) +register_loader(CLArgSource, CLArgLoader) +register_loader(DataSource, DataLoader) diff --git a/confz/validate.py b/confz/validate.py index 714ee75..a06f746 100644 --- a/confz/validate.py +++ b/confz/validate.py @@ -1,4 +1,4 @@ -from .confz import ConfZ +from .confz import BaseConfig def _get_sub_classes(cls): @@ -18,12 +18,12 @@ def validate_all_configs(include_listeners: bool = False): :param include_listeners: Whether all listeners (marked with :func:`~confz.depends_on`) should be included. - :raises ConfZException: If any config could not be loaded. + :raises ConfigException: If any config could not be loaded. """ config_classes = [] sync_listeners = [] async_listeners = [] - for config_class in _get_sub_classes(ConfZ): + for config_class in _get_sub_classes(BaseConfig): if config_class.CONFIG_SOURCES is not None: config_classes.append(config_class) if include_listeners and config_class.listeners is not None: diff --git a/tests/loaders/test_cl_arg_loader.py b/tests/loaders/test_cl_arg_loader.py index 2368828..ae160b5 100644 --- a/tests/loaders/test_cl_arg_loader.py +++ b/tests/loaders/test_cl_arg_loader.py @@ -1,13 +1,13 @@ import sys -from confz import ConfZ, ConfZCLArgSource +from confz import BaseConfig, CLArgSource -class InnerConfig(ConfZ): +class InnerConfig(BaseConfig): attr1: int -class OuterConfig(ConfZ): +class OuterConfig(BaseConfig): attr2: int inner: InnerConfig @@ -15,7 +15,7 @@ class OuterConfig(ConfZ): def test_default(monkeypatch): argv = sys.argv.copy() + ["--inner.attr1", "1", "--attr2", "2"] monkeypatch.setattr(sys, "argv", argv) - config = OuterConfig(config_sources=ConfZCLArgSource()) + config = OuterConfig(config_sources=CLArgSource()) assert config.inner.attr1 == 1 assert config.attr2 == 2 @@ -30,7 +30,7 @@ def test_prefix(monkeypatch): "100", ] monkeypatch.setattr(sys, "argv", argv) - config = OuterConfig(config_sources=ConfZCLArgSource(prefix="conf_")) + config = OuterConfig(config_sources=CLArgSource(prefix="conf_")) assert config.inner.attr1 == 1 assert config.attr2 == 2 @@ -38,7 +38,7 @@ def test_prefix(monkeypatch): def test_remap(monkeypatch): argv = sys.argv.copy() + ["--val1", "1", "--attr2", "2"] monkeypatch.setattr(sys, "argv", argv) - config = OuterConfig(config_sources=ConfZCLArgSource(remap={"val1": "inner.attr1"})) + config = OuterConfig(config_sources=CLArgSource(remap={"val1": "inner.attr1"})) assert config.inner.attr1 == 1 assert config.attr2 == 2 @@ -54,7 +54,7 @@ def test_nested_separator(monkeypatch): ] monkeypatch.setattr(sys, "argv", argv) config = OuterConfig( - config_sources=ConfZCLArgSource(prefix="conf_", nested_separator="__") + config_sources=CLArgSource(prefix="conf_", nested_separator="__") ) assert config.inner.attr1 == 1 assert config.attr2 == 2 diff --git a/tests/loaders/test_env_loader.py b/tests/loaders/test_env_loader.py index 5ba86d7..3fbb591 100644 --- a/tests/loaders/test_env_loader.py +++ b/tests/loaders/test_env_loader.py @@ -3,16 +3,16 @@ import pytest from pydantic import ValidationError -from confz import ConfZ, ConfZEnvSource +from confz import BaseConfig, EnvSource from tests.assets import ASSET_FOLDER -class InnerConfig(ConfZ): +class InnerConfig(BaseConfig): attr1_name: int attr_override: Optional[str] = None -class OuterConfig(ConfZ): +class OuterConfig(BaseConfig): attr2: int inner: InnerConfig @@ -21,7 +21,7 @@ def test_allow_all(monkeypatch): monkeypatch.setenv("ATTR2", "2") monkeypatch.setenv("INNER.ATTR1-NAME", "1") monkeypatch.setenv("INNER.ATTR-OVERRIDE", "secret") - config = OuterConfig(config_sources=ConfZEnvSource(allow_all=True)) + config = OuterConfig(config_sources=EnvSource(allow_all=True)) assert config.inner.attr1_name == 1 assert config.attr2 == 2 assert config.inner.attr_override == "secret" @@ -34,7 +34,7 @@ def test_allow_deny(monkeypatch): # works if all allowed config = OuterConfig( - config_sources=ConfZEnvSource(allow=["inner.attr1_name", "attr2"]) + config_sources=EnvSource(allow=["inner.attr1_name", "attr2"]) ) assert config.attr2 == 2 assert config.inner.attr1_name == 1 @@ -42,16 +42,16 @@ def test_allow_deny(monkeypatch): # raises error if not all allowed with pytest.raises(ValidationError): - OuterConfig(config_sources=ConfZEnvSource(allow=["attr2"])) + OuterConfig(config_sources=EnvSource(allow=["attr2"])) # raises error if none allowed with pytest.raises(ValidationError): - OuterConfig(config_sources=ConfZEnvSource()) + OuterConfig(config_sources=EnvSource()) # raises error if denied with pytest.raises(ValidationError): OuterConfig( - config_sources=ConfZEnvSource( + config_sources=EnvSource( allow=["inner.attr1_name", "attr2"], deny=["attr2"] ) ) @@ -63,14 +63,14 @@ def test_prefix(monkeypatch): # prefix works config = OuterConfig( - config_sources=ConfZEnvSource(allow_all=True, prefix="CONFIG_") + config_sources=EnvSource(allow_all=True, prefix="CONFIG_") ) assert config.attr2 == 2 assert config.inner.attr1_name == 1 # allow does not use prefix config = OuterConfig( - config_sources=ConfZEnvSource( + config_sources=EnvSource( allow=["inner.attr1_name", "attr2"], prefix="CONFIG_" ) ) @@ -80,7 +80,7 @@ def test_prefix(monkeypatch): # deny does not use prefix with pytest.raises(ValidationError): OuterConfig( - config_sources=ConfZEnvSource( + config_sources=EnvSource( allow_all=True, deny=["attr2"], prefix="CONFIG_" ) ) @@ -91,7 +91,7 @@ def test_remap(monkeypatch): monkeypatch.setenv("VAL1", "1") monkeypatch.setenv("VAL2", "2") config = OuterConfig( - config_sources=ConfZEnvSource( + config_sources=EnvSource( allow_all=True, remap={ "val1": "inner.attr1_name", @@ -106,7 +106,7 @@ def test_remap(monkeypatch): monkeypatch.setenv("CONFIG_VAL1", "3") monkeypatch.setenv("CONFIG_VAL2", "4") config = OuterConfig( - config_sources=ConfZEnvSource( + config_sources=EnvSource( allow_all=True, prefix="CONFIG_", remap={ @@ -123,7 +123,7 @@ def test_dotenv_loading(monkeypatch): monkeypatch.setenv("INNER.ATTR1_NAME", "21") monkeypatch.setenv("ATTR2", "1") config = OuterConfig( - config_sources=ConfZEnvSource(allow_all=True, file=ASSET_FOLDER / "config.env") + config_sources=EnvSource(allow_all=True, file=ASSET_FOLDER / "config.env") ) assert config.attr2 == 1 assert config.inner.attr1_name == 21 @@ -134,7 +134,7 @@ def test_dotenv_loading_missing_file(monkeypatch): monkeypatch.setenv("INNER.ATTR1_NAME", "21") monkeypatch.setenv("ATTR2", "1") config = OuterConfig( - config_sources=ConfZEnvSource(allow_all=True, file=ASSET_FOLDER / "idontexist") + config_sources=EnvSource(allow_all=True, file=ASSET_FOLDER / "idontexist") ) assert config.attr2 == 1 assert config.inner.attr1_name == 21 @@ -146,7 +146,7 @@ def test_dotenv_loading_from_bytes(monkeypatch): monkeypatch.setenv("ATTR2", "1") with (ASSET_FOLDER / "config.env").open("rb") as config_file: data = config_file.read() - config = OuterConfig(config_sources=ConfZEnvSource(allow_all=True, file=data)) + config = OuterConfig(config_sources=EnvSource(allow_all=True, file=data)) assert config.attr2 == 1 assert config.inner.attr1_name == 21 assert config.inner.attr_override == "2002" @@ -157,7 +157,7 @@ def test_separator(monkeypatch): monkeypatch.setenv("INNER__ATTR-OVERRIDE", "2002") monkeypatch.setenv("ATTR2", "1") config = OuterConfig( - config_sources=ConfZEnvSource(allow_all=True, nested_separator="__") + config_sources=EnvSource(allow_all=True, nested_separator="__") ) assert config.attr2 == 1 assert config.inner.attr1_name == 21 diff --git a/tests/loaders/test_file_loader.py b/tests/loaders/test_file_loader.py index 8d45a78..5ce5a32 100644 --- a/tests/loaders/test_file_loader.py +++ b/tests/loaders/test_file_loader.py @@ -3,22 +3,22 @@ import pytest -from confz import ConfZ, ConfZFileSource, FileFormat -from confz.exceptions import ConfZFileException +from confz import BaseConfig, FileSource, FileFormat +from confz.exceptions import FileException from confz.loaders.file_loader import FileLoader from tests.assets import ASSET_FOLDER -class InnerConfig(ConfZ): +class InnerConfig(BaseConfig): attr1: str -class OuterConfig(ConfZ): +class OuterConfig(BaseConfig): attr2: str inner: InnerConfig -class ListElementConfig(ConfZ): +class ListElementConfig(BaseConfig): key: str value: str @@ -29,7 +29,7 @@ class SecondOuterConfig(OuterConfig): def test_json_file(): config = OuterConfig( - config_sources=ConfZFileSource(file=ASSET_FOLDER / "config.json") + config_sources=FileSource(file=ASSET_FOLDER / "config.json") ) assert config.inner.attr1 == "1 🎉" assert config.attr2 == "2" @@ -37,7 +37,7 @@ def test_json_file(): def test_yaml_file(): config = OuterConfig( - config_sources=ConfZFileSource(file=ASSET_FOLDER / "config.yml") + config_sources=FileSource(file=ASSET_FOLDER / "config.yml") ) assert config.inner.attr1 == "1 🎉" assert config.attr2 == "2" @@ -46,8 +46,8 @@ def test_yaml_file(): def test_multiple_yaml_files_both_available(): config = OuterConfig( config_sources=[ - ConfZFileSource(file=ASSET_FOLDER / "config.yml"), - ConfZFileSource(file=ASSET_FOLDER / "config_2.yml"), + FileSource(file=ASSET_FOLDER / "config.yml"), + FileSource(file=ASSET_FOLDER / "config_2.yml"), ] ) assert config.inner.attr1 == "4 🎉" @@ -57,8 +57,8 @@ def test_multiple_yaml_files_both_available(): def test_multiple_yaml_files_one_is_optional_and_unavailable(): config = OuterConfig( config_sources=[ - ConfZFileSource(file=ASSET_FOLDER / "config.yml"), - ConfZFileSource( + FileSource(file=ASSET_FOLDER / "config.yml"), + FileSource( file=ASSET_FOLDER / "config_not_existing.yml", optional=True ), ] @@ -69,7 +69,7 @@ def test_multiple_yaml_files_one_is_optional_and_unavailable(): def test_toml_file(): config = SecondOuterConfig( - config_sources=ConfZFileSource(file=ASSET_FOLDER / "config.toml") + config_sources=FileSource(file=ASSET_FOLDER / "config.toml") ) assert config.inner.attr1 == "1 🎉" assert config.attr2 == "2" @@ -82,22 +82,22 @@ def test_toml_file(): def test_bytes_file(): json_dummy_content = b'{"inner":{"attr1": "2"}, "attr2": "5"}' config = OuterConfig( - config_sources=ConfZFileSource(file=json_dummy_content, format=FileFormat.JSON) + config_sources=FileSource(file=json_dummy_content, format=FileFormat.JSON) ) assert config.attr2 == "5" assert config.inner.attr1 == "2" - with pytest.raises(ConfZFileException): - OuterConfig(config_sources=ConfZFileSource(file=json_dummy_content)) + with pytest.raises(FileException): + OuterConfig(config_sources=FileSource(file=json_dummy_content)) def test_custom_file(): # does not recognize format per default - with pytest.raises(ConfZFileException): - OuterConfig(config_sources=ConfZFileSource(file=ASSET_FOLDER / "config.txt")) + with pytest.raises(FileException): + OuterConfig(config_sources=FileSource(file=ASSET_FOLDER / "config.txt")) # can specify format config = OuterConfig( - config_sources=ConfZFileSource( + config_sources=FileSource( file=ASSET_FOLDER / "config.txt", format=FileFormat.YAML ) ) @@ -107,15 +107,15 @@ def test_custom_file(): def test_file_path_from_bytes(): json_dummy_content = b'{"inner":{"attr1": "2"}, "attr2": "5"}' - source = ConfZFileSource(file=json_dummy_content, format=FileFormat.JSON) - with pytest.raises(ConfZFileException): + source = FileSource(file=json_dummy_content, format=FileFormat.JSON) + with pytest.raises(FileException): FileLoader._get_filename(source) def test_custom_file_str_path(): # can specify format config = OuterConfig( - config_sources=ConfZFileSource( + config_sources=FileSource( file=str(ASSET_FOLDER) + "/config.txt", format=FileFormat.YAML ) ) @@ -124,48 +124,48 @@ def test_custom_file_str_path(): def test_wrong_format(): - with pytest.raises(ConfZFileException): + with pytest.raises(FileException): OuterConfig( - config_sources=ConfZFileSource( + config_sources=FileSource( file=str(ASSET_FOLDER) + "/config.txt", format="wrong value" ) ) def test_invalid_file(): - with pytest.raises(ConfZFileException): + with pytest.raises(FileException): OuterConfig( - config_sources=ConfZFileSource(file=ASSET_FOLDER / "non_existing.json") + config_sources=FileSource(file=ASSET_FOLDER / "non_existing.json") ) def test_invalid_file_str(): - with pytest.raises(ConfZFileException): + with pytest.raises(FileException): OuterConfig( - config_sources=ConfZFileSource( + config_sources=FileSource( file=str(ASSET_FOLDER) + "/non_existing.json" ) ) def test_no_file(): - with pytest.raises(ConfZFileException): - OuterConfig(config_sources=ConfZFileSource()) + with pytest.raises(FileException): + OuterConfig(config_sources=FileSource()) def test_from_env(monkeypatch): env_var = "MY_CONFIG_FILE" # raises error if not set - with pytest.raises(ConfZFileException): + with pytest.raises(FileException): OuterConfig( - config_sources=ConfZFileSource(file_from_env=env_var, folder=ASSET_FOLDER) + config_sources=FileSource(file_from_env=env_var, folder=ASSET_FOLDER) ) # works if set monkeypatch.setenv(env_var, "config.json") config = OuterConfig( - config_sources=ConfZFileSource(file_from_env=env_var, folder=ASSET_FOLDER) + config_sources=FileSource(file_from_env=env_var, folder=ASSET_FOLDER) ) assert config.inner.attr1 == "1 🎉" assert config.attr2 == "2" @@ -178,7 +178,7 @@ def test_from_env_using_str_path(monkeypatch): monkeypatch.setenv(env_var, "config.json") assert_folder_str: str = str(ASSET_FOLDER) config = OuterConfig( - config_sources=ConfZFileSource(file_from_env=env_var, folder=assert_folder_str) + config_sources=FileSource(file_from_env=env_var, folder=assert_folder_str) ) assert config.inner.attr1 == "1 🎉" assert config.attr2 == "2" @@ -189,15 +189,15 @@ def test_from_cl_arg_idx(monkeypatch): cl_arg_idx = len(argv_backup) # raises error if not set - with pytest.raises(ConfZFileException): + with pytest.raises(FileException): OuterConfig( - config_sources=ConfZFileSource(file_from_cl=cl_arg_idx, folder=ASSET_FOLDER) + config_sources=FileSource(file_from_cl=cl_arg_idx, folder=ASSET_FOLDER) ) # works if set monkeypatch.setattr(sys, "argv", argv_backup + ["config.json"]) config = OuterConfig( - config_sources=ConfZFileSource(file_from_cl=cl_arg_idx, folder=ASSET_FOLDER) + config_sources=FileSource(file_from_cl=cl_arg_idx, folder=ASSET_FOLDER) ) assert config.inner.attr1 == "1 🎉" assert config.attr2 == "2" @@ -208,18 +208,18 @@ def test_from_cl_arg_name(monkeypatch): cl_arg_name = "--my_config_file" # raises error if not set - with pytest.raises(ConfZFileException): + with pytest.raises(FileException): OuterConfig( - config_sources=ConfZFileSource( + config_sources=FileSource( file_from_cl=cl_arg_name, folder=ASSET_FOLDER ) ) # raises error if missing value - with pytest.raises(ConfZFileException): + with pytest.raises(FileException): monkeypatch.setattr(sys, "argv", argv_backup + [cl_arg_name]) OuterConfig( - config_sources=ConfZFileSource( + config_sources=FileSource( file_from_cl=cl_arg_name, folder=ASSET_FOLDER ) ) @@ -227,7 +227,7 @@ def test_from_cl_arg_name(monkeypatch): # works if set monkeypatch.setattr(sys, "argv", argv_backup + [cl_arg_name, "config.json"]) config = OuterConfig( - config_sources=ConfZFileSource(file_from_cl=cl_arg_name, folder=ASSET_FOLDER) + config_sources=FileSource(file_from_cl=cl_arg_name, folder=ASSET_FOLDER) ) assert config.inner.attr1 == "1 🎉" assert config.attr2 == "2" @@ -237,8 +237,8 @@ def test_from_cl_arg_optional(): # if not set, should load the config file without errors config = OuterConfig( config_sources=[ - ConfZFileSource(file_from_cl="--my_config_file", optional=True), - ConfZFileSource(file=ASSET_FOLDER / "config.yml"), + FileSource(file_from_cl="--my_config_file", optional=True), + FileSource(file=ASSET_FOLDER / "config.yml"), ] ) assert config.inner.attr1 == "1 🎉" diff --git a/tests/loaders/test_loader.py b/tests/loaders/test_loader.py index de6fb91..aafa88c 100644 --- a/tests/loaders/test_loader.py +++ b/tests/loaders/test_loader.py @@ -2,27 +2,27 @@ import pytest -from confz import ConfZ, ConfZDataSource, ConfZSource, ConfZEnvSource -from confz.exceptions import ConfZUpdateException, ConfZException +from confz import BaseConfig, DataSource, ConfigSource, EnvSource +from confz.exceptions import UpdateException, ConfigException from confz.loaders import Loader, register_loader -class InnerConfig(ConfZ): +class InnerConfig(BaseConfig): attr1: int -class OuterConfig(ConfZ): +class OuterConfig(BaseConfig): attr2: int inner: InnerConfig @dataclass -class CustomSource(ConfZSource): +class CustomSource(ConfigSource): custom_attr: int @dataclass -class CustomSource2(ConfZSource): +class CustomSource2(ConfigSource): pass @@ -41,9 +41,9 @@ def populate_config(cls, config: dict, confz_source: CustomSource): def test_update_dict_recursively(): config = OuterConfig( config_sources=[ - ConfZDataSource(data={"inner": {"attr1": 1}, "attr2": 2}), - ConfZDataSource(data={"inner": {"attr1": 3}}), - ConfZDataSource(data={"attr2": 4}), + DataSource(data={"inner": {"attr1": 1}, "attr2": 2}), + DataSource(data={"inner": {"attr1": 3}}), + DataSource(data={"attr2": 4}), ] ) assert config.inner.attr1 == 3 @@ -51,17 +51,17 @@ def test_update_dict_recursively(): def test_dict_contradiction(monkeypatch): - with pytest.raises(ConfZUpdateException): + with pytest.raises(UpdateException): OuterConfig( config_sources=[ - ConfZDataSource(data={"inner": "something"}), - ConfZDataSource(data={"inner": {"attr1": 3}}), + DataSource(data={"inner": "something"}), + DataSource(data={"inner": {"attr1": 3}}), ] ) monkeypatch.setenv("INNER", "something") monkeypatch.setenv("INNER.ATTR1", "3") - with pytest.raises(ConfZUpdateException): - OuterConfig(config_sources=ConfZEnvSource(allow_all=True)) + with pytest.raises(UpdateException): + OuterConfig(config_sources=EnvSource(allow_all=True)) def test_own_loader(): @@ -73,5 +73,5 @@ def test_own_loader(): def test_unregistered_source(): InnerConfig(attr1=2) - with pytest.raises(ConfZException): + with pytest.raises(ConfigException): InnerConfig(config_sources=CustomSource2(), attr1=2) diff --git a/tests/test_change.py b/tests/test_change.py index d15956e..0ca64ba 100644 --- a/tests/test_change.py +++ b/tests/test_change.py @@ -1,18 +1,18 @@ import pytest -from confz import ConfZ, ConfZDataSource, depends_on +from confz import BaseConfig, DataSource, depends_on -class Config1(ConfZ): +class Config1(BaseConfig): attr: int - CONFIG_SOURCES = ConfZDataSource(data={"attr": 1}) + CONFIG_SOURCES = DataSource(data={"attr": 1}) -class Config2(ConfZ): +class Config2(BaseConfig): attr: int - CONFIG_SOURCES = ConfZDataSource(data={"attr": 2}) + CONFIG_SOURCES = DataSource(data={"attr": 2}) def test_change_sources(): @@ -22,7 +22,7 @@ def test_change_sources(): assert config_before is Config1() # can change source and singleton - new_source = ConfZDataSource(data={"attr": 10}) + new_source = DataSource(data={"attr": 10}) with Config1.change_config_sources(new_source): assert Config1().attr == 10 assert Config1() is Config1() @@ -61,8 +61,8 @@ def my_fn2(): assert my_fn2() is config_before2 # can change source and singleton - new_source1 = ConfZDataSource(data={"attr": 10}) - new_source2 = ConfZDataSource(data={"attr": 20}) + new_source1 = DataSource(data={"attr": 10}) + new_source2 = DataSource(data={"attr": 20}) with Config2.change_config_sources(new_source2): # unrelated functions not changed diff --git a/tests/test_confz.py b/tests/test_confz.py index 9bf2d25..b9b2d8f 100644 --- a/tests/test_confz.py +++ b/tests/test_confz.py @@ -1,15 +1,15 @@ import pytest from pydantic import ValidationError -from confz import ConfZ, ConfZDataSource -from confz.exceptions import ConfZException +from confz import BaseConfig, DataSource +from confz.exceptions import ConfigException -class InnerConfig(ConfZ): +class InnerConfig(BaseConfig): attr1: int -class OuterConfig(ConfZ): +class OuterConfig(BaseConfig): attr2: int inner: InnerConfig @@ -17,7 +17,7 @@ class OuterConfig(ConfZ): class ParentConfig1(OuterConfig): attr3: int - CONFIG_SOURCES = ConfZDataSource( + CONFIG_SOURCES = DataSource( data={"inner": {"attr1": 1}, "attr2": 2, "attr3": 3} ) @@ -25,7 +25,7 @@ class ParentConfig1(OuterConfig): class ParentConfig2(OuterConfig): attr4: int - CONFIG_SOURCES = ConfZDataSource( + CONFIG_SOURCES = DataSource( data={"inner": {"attr1": 1}, "attr2": 2, "attr4": 4} ) @@ -56,7 +56,7 @@ def test_class_var(): assert config1.attr2 == 2 # kwargs do not work - with pytest.raises(ConfZException): + with pytest.raises(ConfigException): ParentConfig1(attr3=3) # singleton @@ -70,19 +70,19 @@ def test_class_var(): def test_init_arg(): # assert that uses sources config = OuterConfig( - config_sources=ConfZDataSource(data={"inner": {"attr1": 1}, "attr2": 20}) + config_sources=DataSource(data={"inner": {"attr1": 1}, "attr2": 20}) ) assert config.attr2 == 20 # no singleton assert ParentConfig1() is ParentConfig1() config1 = ParentConfig1( - config_sources=ConfZDataSource( + config_sources=DataSource( data={"inner": {"attr1": 1}, "attr2": 2, "attr3": 3} ) ) config2 = ParentConfig1( - config_sources=ConfZDataSource( + config_sources=DataSource( data={"inner": {"attr1": 1}, "attr2": 2, "attr3": 3} ) ) @@ -92,10 +92,10 @@ def test_init_arg(): # uses kwargs with pytest.raises(ValidationError): ParentConfig3( - config_sources=ConfZDataSource(data={"inner": {"attr1": 1}, "attr2": 2}) + config_sources=DataSource(data={"inner": {"attr1": 1}, "attr2": 2}) ) config = ParentConfig3( attr5=5, - config_sources=ConfZDataSource(data={"inner": {"attr1": 1}, "attr2": 2}), + config_sources=DataSource(data={"inner": {"attr1": 1}, "attr2": 2}), ) assert config.attr5 == 5 diff --git a/tests/test_validate.py b/tests/test_validate.py index 69955d1..cd27c8c 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -1,39 +1,39 @@ import pytest from pydantic import ValidationError -from confz import ConfZ, ConfZDataSource, validate_all_configs, depends_on +from confz import BaseConfig, DataSource, validate_all_configs, depends_on def test_validate(): # works with new configs - class NewInner(ConfZ): + class NewInner(BaseConfig): attr1: int - class NewOuter(ConfZ): + class NewOuter(BaseConfig): inner: NewInner attr2: int - CONFIG_SOURCES = ConfZDataSource(data={"inner": {"attr1": 1}, "attr2": 2}) + CONFIG_SOURCES = DataSource(data={"inner": {"attr1": 1}, "attr2": 2}) validate_all_configs() # detects missing data - class NewOuter2(ConfZ): + class NewOuter2(BaseConfig): inner: NewInner attr2: int - CONFIG_SOURCES = ConfZDataSource(data={"attr2": 2}) + CONFIG_SOURCES = DataSource(data={"attr2": 2}) with pytest.raises(ValidationError): validate_all_configs() # adjust config sources so successive test don't fail because of NewOuter2 - NewOuter2.CONFIG_SOURCES = ConfZDataSource(data={"inner": {"attr1": 1}, "attr2": 2}) + NewOuter2.CONFIG_SOURCES = DataSource(data={"inner": {"attr1": 1}, "attr2": 2}) @pytest.mark.asyncio async def test_listeners(): - class EmptyConfig(ConfZ): + class EmptyConfig(BaseConfig): CONFIG_SOURCES = [] called_sync = False From 9e8189cc708b52dc428d3c36f150c0208c16aa3a Mon Sep 17 00:00:00 2001 From: Silvan Melchior Date: Fri, 14 Jul 2023 17:08:55 +0200 Subject: [PATCH 2/6] adjust docs --- LICENSE | 2 +- README.md | 40 +++++++++++++-------------- docs/source/conf.py | 2 +- docs/source/reference/confz.rst | 2 +- docs/source/reference/exceptions.rst | 6 ++-- docs/source/reference/sources.rst | 12 ++++---- docs/source/usage/confz_class.rst | 27 +++++++++--------- docs/source/usage/context_manager.rst | 8 +++--- docs/source/usage/extensions.rst | 8 +++--- docs/source/usage/sources_loaders.rst | 12 ++++---- 10 files changed, 59 insertions(+), 60 deletions(-) diff --git a/LICENSE b/LICENSE index 37c50d9..3373d75 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Zühlke +Copyright (c) 2023 Zühlke Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b47cf2f..2d4daa2 100644 --- a/README.md +++ b/README.md @@ -35,19 +35,19 @@ pip install confz The first step of using `ConfZ` is to declare your config classes and sources, for example in `config.py`: ```python -from confz import ConfZ, ConfZFileSource +from confz import BaseConfig, FileSource from pydantic import SecretStr, AnyUrl -class DBConfig(ConfZ): +class DBConfig(BaseConfig): user: str password: SecretStr -class APIConfig(ConfZ): +class APIConfig(BaseConfig): host: AnyUrl port: int db: DBConfig - CONFIG_SOURCES = ConfZFileSource(file='/path/to/config.yml') + CONFIG_SOURCES = FileSource(file='/path/to/config.yml') ``` Thanks to [pydantic](https://pydantic-docs.helpmanual.io/), you can use a wide variety of @@ -84,11 +84,11 @@ and accessing for example `APIConfig().db.user` directly. `ConfZ` is highly flexible in defining the source of your config. Do you have multiple environments? No Problem: ```python -from confz import ConfZ, ConfZFileSource +from confz import BaseConfig, FileSource -class MyConfig(ConfZ): +class MyConfig(BaseConfig): ... - CONFIG_SOURCES = ConfZFileSource( + CONFIG_SOURCES = FileSource( folder='/path/to/config/folder', file_from_env='ENVIRONMENT' ) @@ -100,13 +100,13 @@ You can also provide a list as config source and read for example from environme from command line arguments: ```python -from confz import ConfZ, ConfZEnvSource, ConfZCLArgSource +from confz import BaseConfig, EnvSource, CLArgSource -class MyConfig(ConfZ): +class MyConfig(BaseConfig): ... CONFIG_SOURCES = [ - ConfZEnvSource(allow_all=True, file=".env.local"), - ConfZCLArgSource(prefix='conf_') + EnvSource(allow_all=True, file=".env.local"), + CLArgSource(prefix='conf_') ] ``` @@ -121,20 +121,20 @@ In some scenarios, the config should not be a global singleton, but loaded expli Instead of defining `CONFIG_SOURCES` as class variable, the sources can also be defined in the constructor directly: ```python -from confz import ConfZ, ConfZFileSource, ConfZEnvSource +from confz import BaseConfig, FileSource, EnvSource -class MyConfig(ConfZ): +class MyConfig(BaseConfig): number: int text: str -config1 = MyConfig(config_sources=ConfZFileSource(file='/path/to/config.yml')) -config2 = MyConfig(config_sources=ConfZEnvSource(prefix='CONF_', allow=['text']), number=1) +config1 = MyConfig(config_sources=FileSource(file='/path/to/config.yml')) +config2 = MyConfig(config_sources=EnvSource(prefix='CONF_', allow=['text']), number=1) config3 = MyConfig(number=1, text='hello world') ``` As can be seen, additional keyword-arguments can be provided as well. -**Note:** If neither class variable `CONFIG_SOURCES` nor constructor argument `config_sources` is provided, `ConfZ` +**Note:** If neither class variable `CONFIG_SOURCES` nor constructor argument `config_sources` is provided, `BaseConfig` behaves like a regular _pydantic_ class. ### Change Config Values @@ -144,15 +144,15 @@ In some scenarios, you might want to change your config values, for example with manager to temporarily change your config: ```python -from confz import ConfZ, ConfZFileSource, ConfZDataSource +from confz import BaseConfig, FileSource, DataSource -class MyConfig(ConfZ): +class MyConfig(BaseConfig): number: int - CONFIG_SOURCES = ConfZFileSource(file="/path/to/config.yml") + CONFIG_SOURCES = FileSource(file="/path/to/config.yml") print(MyConfig().number) # will print the value from the config-file -new_source = ConfZDataSource(data={'number': 42}) +new_source = DataSource(data={'number': 42}) with MyConfig.change_config_sources(new_source): print(MyConfig().number) # will print '42' diff --git a/docs/source/conf.py b/docs/source/conf.py index 59fc828..0e6eabc 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -18,7 +18,7 @@ # -- Project information ----------------------------------------------------- project = "ConfZ" -copyright = f"2021, Zühlke" +copyright = f"2023, Zühlke" author = "Zühlke" diff --git a/docs/source/reference/confz.rst b/docs/source/reference/confz.rst index 5377575..711202b 100644 --- a/docs/source/reference/confz.rst +++ b/docs/source/reference/confz.rst @@ -1,7 +1,7 @@ ConfZ ===== -.. autoclass:: confz.ConfZ +.. autoclass:: confz.BaseConfig .. autofunction:: confz.depends_on diff --git a/docs/source/reference/exceptions.rst b/docs/source/reference/exceptions.rst index 25c8fa5..6a4f040 100644 --- a/docs/source/reference/exceptions.rst +++ b/docs/source/reference/exceptions.rst @@ -1,8 +1,8 @@ Exceptions ========== -.. autoexception:: confz.exceptions.ConfZException +.. autoexception:: confz.exceptions.ConfigException -.. autoexception:: confz.exceptions.ConfZUpdateException +.. autoexception:: confz.exceptions.UpdateException -.. autoexception:: confz.exceptions.ConfZFileException +.. autoexception:: confz.exceptions.FileException diff --git a/docs/source/reference/sources.rst b/docs/source/reference/sources.rst index ad3846f..5c561e8 100644 --- a/docs/source/reference/sources.rst +++ b/docs/source/reference/sources.rst @@ -1,21 +1,21 @@ Sources ======= -.. autoclass:: confz.ConfZSource +.. autoclass:: confz.ConfigSource :exclude-members: __init__ -.. autoclass:: confz.ConfZFileSource +.. autoclass:: confz.FileSource :exclude-members: __init__ -.. autoclass:: confz.ConfZEnvSource +.. autoclass:: confz.EnvSource :exclude-members: __init__ -.. autoclass:: confz.ConfZCLArgSource +.. autoclass:: confz.CLArgSource :exclude-members: __init__ -.. autoclass:: confz.ConfZDataSource +.. autoclass:: confz.DataSource :exclude-members: __init__ -.. autodata:: confz.ConfZSources +.. autodata:: confz.ConfigSources .. autoclass:: confz.FileFormat diff --git a/docs/source/usage/confz_class.rst b/docs/source/usage/confz_class.rst index eacc8bc..0fb5196 100644 --- a/docs/source/usage/confz_class.rst +++ b/docs/source/usage/confz_class.rst @@ -4,18 +4,18 @@ The ConfZ Class Raw Class --------- -Per default, the :class:`~confz.ConfZ` class behaves like `BaseModel` of pydantic and allows to specify your config with +Per default, the :class:`~confz.BaseConfig` class behaves like `BaseModel` of pydantic and allows to specify your config with typehints, either using standard Python types or more `advanced ones `_: ->>> from confz import ConfZ +>>> from confz import BaseConfig >>> from pydantic import SecretStr, AnyUrl ->>> class DBConfig(ConfZ): +>>> class DBConfig(BaseConfig): ... user: str ... password: SecretStr ->>> class APIConfig(ConfZ): +>>> class APIConfig(BaseConfig): ... host: AnyUrl ... port: int ... db: DBConfig @@ -59,7 +59,7 @@ Sources as Keyword ------------------ In most cases, we would not want to provide the config as keyword arguments. Instead, we can provide -:class:`~confz.ConfZSources` as argument `config_sources` and :class:`~confz.ConfZ` will load them. For example, +:class:`~confz.ConfigSources` as argument `config_sources` and :class:`~confz.BaseConfig` will load them. For example, if we have a config file in yaml format like this: .. code-block:: yaml @@ -72,9 +72,8 @@ if we have a config file in yaml format like this: We can load this file as follows: ->>> from pathlib import Path ->>> from confz import ConfZFileSource ->>> APIConfig(config_sources=ConfZFileSource(file="/path/to/config.yaml")) +>>> from confz import FileSource +>>> APIConfig(config_sources=FileSource(file="/path/to/config.yaml")) APIConfig( host=AnyUrl('http://my-host.com', scheme='http', host='my-host.com', tld='com', host_type='domain'), port=1234, @@ -88,19 +87,19 @@ Sources as Class Variable ------------------------- Defining config sources as keyword argument still requires you to explicitly instantiate your config class and passing -it to all corresponding software components. :class:`~confz.ConfZ` provides an alternative to this by defining your +it to all corresponding software components. :class:`~confz.BaseConfig` provides an alternative to this by defining your source as a class variable `CONFIG_SOURCES`: ->>> class DBConfig(ConfZ): +>>> class DBConfig(BaseConfig): ... user: str ... password: SecretStr ->>> class APIConfig(ConfZ): +>>> class APIConfig(BaseConfig): ... host: AnyUrl ... port: int ... db: DBConfig ... -... CONFIG_SOURCES = ConfZFileSource(file="/path/to/config.yaml") +... CONFIG_SOURCES = FileSource(file="/path/to/config.yaml") From now on, your config values are accessible from anywhere within your code by just importing ``APIConfig`` and instantiating it: @@ -123,9 +122,9 @@ set. Early Loading ^^^^^^^^^^^^^ -:class:`~confz.ConfZ` could also load your config sources directly during class creation. However, this yields unwanted +:class:`~confz.BaseConfig` could also load your config sources directly during class creation. However, this yields unwanted side effects like reading files and command line arguments during import of your config classes, which should be -avoided. Thus, :class:`~confz.ConfZ` loads your config the first time you instantiate the class. +avoided. Thus, :class:`~confz.BaseConfig` loads your config the first time you instantiate the class. If at this point the config class cannot populate all mandatory fields, pydantic will raise an error. To make sure this does not happen in an inconvenient moment, you can also manually load all configs at the beginning of your diff --git a/docs/source/usage/context_manager.rst b/docs/source/usage/context_manager.rst index 4911bc6..7f7abd6 100644 --- a/docs/source/usage/context_manager.rst +++ b/docs/source/usage/context_manager.rst @@ -7,18 +7,18 @@ In some scenarios, you might want to change your config values, for example with `CONFIG_SOURCES` class variable, this is not directly possible: >>> from pathlib import Path ->>> from confz import ConfZ, ConfZFileSource, ConfZDataSource +>>> from confz import BaseConfig, FileSource, DataSource ->>> class MyConfig(ConfZ): +>>> class MyConfig(BaseConfig): ... number: int -... CONFIG_SOURCES = ConfZFileSource(file="/path/to/config.yml") +... CONFIG_SOURCES = FileSource(file="/path/to/config.yml") >>> print(MyConfig().number) 1 To overcome this, every config class provides a context manager to temporarily change your config: ->>> new_source = ConfZDataSource(data={"number": 42}) +>>> new_source = DataSource(data={"number": 42}) >>> with MyConfig.change_config_sources(new_source): ... print(MyConfig().number) ... diff --git a/docs/source/usage/extensions.rst b/docs/source/usage/extensions.rst index a71ff7b..a3d0302 100644 --- a/docs/source/usage/extensions.rst +++ b/docs/source/usage/extensions.rst @@ -10,12 +10,12 @@ config:: import sys from dataclasses import dataclass - from confz import ConfZSource + from confz import ConfigSource from confz.loaders import Loader, register_loader @dataclass - class CustomSource(ConfZSource): + class CustomSource(ConfigSource): platform: str = None # Write the current platform into a config variable with this name version: str = None # Write the current python version into a config variable with this name @@ -34,8 +34,8 @@ config:: Now, any config class can use this new source: ->>> from confz import ConfZ ->>> class MyConfig(ConfZ): +>>> from confz import BaseConfig +>>> class MyConfig(BaseConfig): ... attr1: str ... attr2: str diff --git a/docs/source/usage/sources_loaders.rst b/docs/source/usage/sources_loaders.rst index 99119eb..7b4a552 100644 --- a/docs/source/usage/sources_loaders.rst +++ b/docs/source/usage/sources_loaders.rst @@ -6,21 +6,21 @@ Config Sources and Loaders Config Sources -------------- -A :class:`~confz.ConfZSource` is a dataclass describing how to load config values from a certain source. It serves +A :class:`~confz.ConfigSource` is a dataclass describing how to load config values from a certain source. It serves as instructions for the corresponding :class:`~confz.loaders.Loader`. There are multiple config sources which support a heterogeneous set of use-cases: -- :class:`~confz.ConfZFileSource` allows to load config data from files. Currently, yaml- , json-, and toml-format is supported. +- :class:`~confz.FileSource` allows to load config data from files. Currently, yaml- , json-, and toml-format is supported. The filename can either be passed directly, via environment variable or via command line argument. The latter cases allow to easily configure multiple environments by having a separate file for each environment. -- :class:`~confz.ConfZEnvSource` allows to load config data from environment variables and .env files. It supports to +- :class:`~confz.EnvSource` allows to load config data from environment variables and .env files. It supports to select the corresponding variables with allow- and deny-lists and with an optional prefix and optional custom separator for nested variables. The variable names are - either inferred from the config name (see :class:`~confz.confz_source.ConfZEnvSource` for the rules) or can be explicitly mapped. -- :class:`~confz.ConfZCLArgSource` allows to load config data from command line arguments. An optional prefix allows + either inferred from the config name (see :class:`~confz.confz_source.EnvSource` for the rules) or can be explicitly mapped. +- :class:`~confz.CLArgSource` allows to load config data from command line arguments. An optional prefix allows to select only parts of the arguments. Optional custom separator for nested command line arguments is also supported. The argument names are either inferred from the config name or can be explicitly mapped. -- :class:`~confz.ConfZDataSource` allows to define constant config data. This can be very useful for unit tests, see +- :class:`~confz.DataSource` allows to define constant config data. This can be very useful for unit tests, see :ref:`context_manager`. Furthermore, it is possible to define your own sources with a corresponding loader, see :ref:`extending-confz`. From 88eaf158d68d2791d33cfe1dbbdf13b3f23a77ae Mon Sep 17 00:00:00 2001 From: Silvan Melchior Date: Fri, 14 Jul 2023 17:15:56 +0200 Subject: [PATCH 3/6] more renames of files and variables --- confz/__init__.py | 4 +- confz/{confz.py => base_config.py} | 16 +++--- confz/change.py | 4 +- confz/{confz_source.py => config_source.py} | 0 confz/loaders/cl_arg_loader.py | 16 +++--- confz/loaders/data_loader.py | 6 +-- confz/loaders/env_loader.py | 36 ++++++------- confz/loaders/file_loader.py | 60 ++++++++++----------- confz/loaders/loader.py | 4 +- confz/loaders/register.py | 16 +++--- confz/validate.py | 2 +- docs/source/usage/extensions.rst | 6 +-- 12 files changed, 85 insertions(+), 85 deletions(-) rename confz/{confz.py => base_config.py} (88%) rename confz/{confz_source.py => config_source.py} (100%) diff --git a/confz/__init__.py b/confz/__init__.py index 9c37ff3..7d6062d 100644 --- a/confz/__init__.py +++ b/confz/__init__.py @@ -1,6 +1,6 @@ from .change import depends_on -from .confz import BaseConfig -from .confz_source import ( +from .base_config import BaseConfig +from .config_source import ( ConfigSources, ConfigSource, FileSource, diff --git a/confz/confz.py b/confz/base_config.py similarity index 88% rename from confz/confz.py rename to confz/base_config.py index 1d4f43b..8682051 100644 --- a/confz/confz.py +++ b/confz/base_config.py @@ -6,20 +6,20 @@ from pydantic import BaseModel from .change import SourceChangeManager -from .confz_source import ConfigSources +from .config_source import ConfigSources from .exceptions import ConfigException from .loaders import get_loader -def _load_config(config_kwargs: dict, confz_sources: ConfigSources) -> dict: +def _load_config(config_kwargs: dict, config_sources: ConfigSources) -> dict: config = config_kwargs.copy() - if isinstance(confz_sources, list): - for confz_source in confz_sources: - loader = get_loader(type(confz_source)) - loader.populate_config(config, confz_source) + if isinstance(config_sources, list): + for config_source in config_sources: + loader = get_loader(type(config_source)) + loader.populate_config(config, config_source) else: - loader = get_loader(type(confz_sources)) - loader.populate_config(config, confz_sources) + loader = get_loader(type(config_sources)) + loader.populate_config(config, config_sources) return config diff --git a/confz/change.py b/confz/change.py index 7e0b109..1cdb4d6 100644 --- a/confz/change.py +++ b/confz/change.py @@ -12,10 +12,10 @@ TYPE_CHECKING, ) -from .confz_source import ConfigSources +from .config_source import ConfigSources if TYPE_CHECKING: - from .confz import BaseConfig + from .base_config import BaseConfig class SourceChangeManager(AbstractContextManager): diff --git a/confz/confz_source.py b/confz/config_source.py similarity index 100% rename from confz/confz_source.py rename to confz/config_source.py diff --git a/confz/loaders/cl_arg_loader.py b/confz/loaders/cl_arg_loader.py index d9a2836..d3e3647 100644 --- a/confz/loaders/cl_arg_loader.py +++ b/confz/loaders/cl_arg_loader.py @@ -1,6 +1,6 @@ import sys -from confz.confz_source import CLArgSource +from confz.config_source import CLArgSource from .loader import Loader @@ -8,24 +8,24 @@ class CLArgLoader(Loader): """Config loader for command line arguments.""" @classmethod - def populate_config(cls, config: dict, confz_source: CLArgSource): + def populate_config(cls, config: dict, config_source: CLArgSource): cl_args = {} for idx, cl_arg in enumerate(sys.argv[1:]): if cl_arg.startswith("--") and idx + 2 < len(sys.argv): cl_name = cl_arg[2:] cl_value = sys.argv[idx + 2] - if confz_source.prefix is not None: - if not cl_name.startswith(confz_source.prefix): + if config_source.prefix is not None: + if not cl_name.startswith(config_source.prefix): continue - cl_name = cl_name[len(confz_source.prefix) :] + cl_name = cl_name[len(config_source.prefix):] - if confz_source.remap is not None and cl_name in confz_source.remap: - cl_name = confz_source.remap[cl_name] + if config_source.remap is not None and cl_name in config_source.remap: + cl_name = config_source.remap[cl_name] cl_args[cl_name] = cl_value cl_args = cls.transform_nested_dicts( - cl_args, separator=confz_source.nested_separator + cl_args, separator=config_source.nested_separator ) cls.update_dict_recursively(config, cl_args) diff --git a/confz/loaders/data_loader.py b/confz/loaders/data_loader.py index 0e9909c..6104a53 100644 --- a/confz/loaders/data_loader.py +++ b/confz/loaders/data_loader.py @@ -1,4 +1,4 @@ -from confz.confz_source import DataSource +from confz.config_source import DataSource from .loader import Loader @@ -6,5 +6,5 @@ class DataLoader(Loader): """Config loader for fix data.""" @classmethod - def populate_config(cls, config: dict, confz_source: DataSource): - cls.update_dict_recursively(config, confz_source.data) + def populate_config(cls, config: dict, config_source: DataSource): + cls.update_dict_recursively(config, config_source.data) diff --git a/confz/loaders/env_loader.py b/confz/loaders/env_loader.py index 74ba787..5a1b6e0 100644 --- a/confz/loaders/env_loader.py +++ b/confz/loaders/env_loader.py @@ -4,7 +4,7 @@ from dotenv import dotenv_values -from confz.confz_source import EnvSource +from confz.config_source import EnvSource from .loader import Loader @@ -28,48 +28,48 @@ def _transform_remap( return map_out @classmethod - def _check_allowance(cls, var_name: str, confz_source: EnvSource) -> bool: - if not confz_source.allow_all: - if confz_source.allow is None: + def _check_allowance(cls, var_name: str, config_source: EnvSource) -> bool: + if not config_source.allow_all: + if config_source.allow is None: return False - allow_list = [cls._transform_name(var) for var in confz_source.allow] + allow_list = [cls._transform_name(var) for var in config_source.allow] if var_name not in allow_list: return False - if confz_source.deny is not None: - deny_list = [cls._transform_name(var) for var in confz_source.deny] + if config_source.deny is not None: + deny_list = [cls._transform_name(var) for var in config_source.deny] if var_name in deny_list: return False return True @classmethod - def populate_config(cls, config: dict, confz_source: EnvSource): - remap = cls._transform_remap(confz_source.remap) + def populate_config(cls, config: dict, config_source: EnvSource): + remap = cls._transform_remap(config_source.remap) origin_env_vars: Dict[str, Any] = dict(os.environ) - if confz_source.file is not None: - if not isinstance(confz_source.file, bytes): + if config_source.file is not None: + if not isinstance(config_source.file, bytes): origin_env_vars = { - **dotenv_values(confz_source.file), + **dotenv_values(config_source.file), **origin_env_vars, } else: - byte_stream = io.BytesIO(confz_source.file) + byte_stream = io.BytesIO(config_source.file) stream = io.TextIOWrapper(byte_stream, encoding="utf-8") origin_env_vars = {**dotenv_values(None, stream), **origin_env_vars} env_vars = {} for env_var in origin_env_vars: var_name = env_var - if confz_source.prefix is not None: - if not var_name.startswith(confz_source.prefix): + if config_source.prefix is not None: + if not var_name.startswith(config_source.prefix): continue - var_name = var_name[len(confz_source.prefix) :] + var_name = var_name[len(config_source.prefix):] var_name = cls._transform_name(var_name) - if not cls._check_allowance(var_name, confz_source): + if not cls._check_allowance(var_name, config_source): continue if remap is not None and var_name in remap: @@ -78,6 +78,6 @@ def populate_config(cls, config: dict, confz_source: EnvSource): env_vars[var_name] = origin_env_vars[env_var] env_vars = cls.transform_nested_dicts( - env_vars, separator=confz_source.nested_separator + env_vars, separator=config_source.nested_separator ) cls.update_dict_recursively(config, env_vars) diff --git a/confz/loaders/file_loader.py b/confz/loaders/file_loader.py index ed7cf08..53ad083 100644 --- a/confz/loaders/file_loader.py +++ b/confz/loaders/file_loader.py @@ -9,7 +9,7 @@ import toml import yaml -from confz.confz_source import FileSource, FileFormat +from confz.config_source import FileSource, FileFormat from confz.exceptions import FileException from .loader import Loader @@ -18,46 +18,46 @@ class FileLoader(Loader): """Config loader for config files.""" @classmethod - def _get_filename(cls, confz_source: FileSource) -> Path: - if confz_source.file is not None: - if isinstance(confz_source.file, bytes): + def _get_filename(cls, config_source: FileSource) -> Path: + if config_source.file is not None: + if isinstance(config_source.file, bytes): raise FileException("Can not detect filename from type bytes") - file_path = Path(confz_source.file) - elif confz_source.file_from_env is not None: - if confz_source.file_from_env not in os.environ: + file_path = Path(config_source.file) + elif config_source.file_from_env is not None: + if config_source.file_from_env not in os.environ: raise FileException( - f"Environment variable '{confz_source.file_from_env}' is not set." + f"Environment variable '{config_source.file_from_env}' is not set." ) - file_path = Path(os.environ[confz_source.file_from_env]) - elif confz_source.file_from_cl is not None: - if isinstance(confz_source.file_from_cl, int): + file_path = Path(os.environ[config_source.file_from_env]) + elif config_source.file_from_cl is not None: + if isinstance(config_source.file_from_cl, int): try: - file_path = Path(sys.argv[confz_source.file_from_cl]) + file_path = Path(sys.argv[config_source.file_from_cl]) except IndexError as e: raise FileException( - f"Command-line argument number {confz_source.file_from_cl} " + f"Command-line argument number {config_source.file_from_cl} " f"is not set." ) from e else: try: - idx = sys.argv.index(confz_source.file_from_cl) + idx = sys.argv.index(config_source.file_from_cl) except ValueError as e: raise FileException( - f"Command-line argument '{confz_source.file_from_cl}' " + f"Command-line argument '{config_source.file_from_cl}' " f"not found." ) from e try: file_path = Path(sys.argv[idx + 1]) except IndexError as e: raise FileException( - f"Command-line argument '{confz_source.file_from_cl}' is not " + f"Command-line argument '{config_source.file_from_cl}' is not " f"set." ) from e else: raise FileException("No file source set.") - if confz_source.folder is not None: - file_path = Path(confz_source.folder) / file_path + if config_source.folder is not None: + file_path = Path(config_source.folder) / file_path return file_path @@ -115,37 +115,37 @@ def _create_stream(cls, file_path: Path, file_encoding: str) -> Iterator[TextIO] @classmethod def _populate_config_from_bytes( - cls, config: dict, data: bytes, confz_source: FileSource + cls, config: dict, data: bytes, config_source: FileSource ): - if confz_source.format is None: + if config_source.format is None: raise FileException( "The format needs to be defined if the " "configuration is passed as byte-string" ) byte_stream = io.BytesIO(data) - text_stream = io.TextIOWrapper(byte_stream, encoding=confz_source.encoding) - file_content = cls._parse_stream(text_stream, confz_source.format) + text_stream = io.TextIOWrapper(byte_stream, encoding=config_source.encoding) + file_content = cls._parse_stream(text_stream, config_source.format) cls.update_dict_recursively(config, file_content) @classmethod - def populate_config(cls, config: dict, confz_source: FileSource): - if confz_source.file is not None and isinstance(confz_source.file, bytes): + def populate_config(cls, config: dict, config_source: FileSource): + if config_source.file is not None and isinstance(config_source.file, bytes): cls._populate_config_from_bytes( - config=config, data=confz_source.file, confz_source=confz_source + config=config, data=config_source.file, config_source=config_source ) return try: - file_path = cls._get_filename(confz_source) + file_path = cls._get_filename(config_source) except FileException as e: - if confz_source.optional: + if config_source.optional: return raise e - file_format = cls._get_format(file_path, confz_source.format) + file_format = cls._get_format(file_path, config_source.format) try: - with cls._create_stream(file_path, confz_source.encoding) as file_stream: + with cls._create_stream(file_path, config_source.encoding) as file_stream: file_content = cls._parse_stream(file_stream, file_format) except FileException as e: - if confz_source.optional: + if config_source.optional: return raise e cls.update_dict_recursively(config, file_content) diff --git a/confz/loaders/loader.py b/confz/loaders/loader.py index 57f8a58..24c67e3 100644 --- a/confz/loaders/loader.py +++ b/confz/loaders/loader.py @@ -65,9 +65,9 @@ def transform_nested_dicts( @classmethod @abstractmethod - def populate_config(cls, config: dict, confz_source): + def populate_config(cls, config: dict, config_source): """Populate the config-dict with new config arguments based on the source. :param config: Config dictionary, gets extended with new arguments - :param confz_source: Source configuration. + :param config_source: Source configuration. """ diff --git a/confz/loaders/register.py b/confz/loaders/register.py index affdbe6..9401f39 100644 --- a/confz/loaders/register.py +++ b/confz/loaders/register.py @@ -1,6 +1,6 @@ from typing import Type, Dict -from confz.confz_source import ( +from confz.config_source import ( ConfigSource, FileSource, EnvSource, @@ -17,20 +17,20 @@ _loaders: Dict[Type[ConfigSource], Type[Loader]] = {} -def get_loader(confz_source: Type[ConfigSource]): - if confz_source in _loaders: - return _loaders[confz_source] - raise ConfigException(f"Unknown config source type '{confz_source}'") +def get_loader(config_source: Type[ConfigSource]): + if config_source in _loaders: + return _loaders[config_source] + raise ConfigException(f"Unknown config source type '{config_source}'") -def register_loader(confz_source: Type[ConfigSource], loader: Type[Loader]): +def register_loader(config_source: Type[ConfigSource], loader: Type[Loader]): """Register a :class:`~confz.ConfigSource` with a specific loader. Can be used to extend `ConfZ` with own loaders. - :param confz_source: The :class:`~confz.ConfigSource` sub-type. + :param config_source: The :class:`~confz.ConfigSource` sub-type. :param loader: The :class:`~confz.loaders.Loader` sub-type. """ - _loaders[confz_source] = loader + _loaders[config_source] = loader register_loader(FileSource, FileLoader) diff --git a/confz/validate.py b/confz/validate.py index a06f746..3901a4d 100644 --- a/confz/validate.py +++ b/confz/validate.py @@ -1,4 +1,4 @@ -from .confz import BaseConfig +from .base_config import BaseConfig def _get_sub_classes(cls): diff --git a/docs/source/usage/extensions.rst b/docs/source/usage/extensions.rst index a3d0302..a8652d7 100644 --- a/docs/source/usage/extensions.rst +++ b/docs/source/usage/extensions.rst @@ -22,10 +22,10 @@ config:: class CustomLoader(Loader): @classmethod - def populate_config(cls, config: dict, confz_source: CustomSource): + def populate_config(cls, config: dict, config_source: CustomSource): config_update = { - confz_source.platform: sys.platform, - confz_source.version: f"{sys.version_info[0]}.{sys.version_info[1]}" + config_source.platform: sys.platform, + config_source.version: f"{sys.version_info[0]}.{sys.version_info[1]}" } cls.update_dict_recursively(config, config_update) From 9bda8aed9555fb1746e13e459b76534c2881a7fe Mon Sep 17 00:00:00 2001 From: Silvan Melchior Date: Fri, 14 Jul 2023 17:18:45 +0200 Subject: [PATCH 4/6] formatting --- confz/loaders/cl_arg_loader.py | 2 +- confz/loaders/env_loader.py | 2 +- confz/loaders/file_loader.py | 4 +--- pyproject.toml | 2 -- tests/loaders/test_env_loader.py | 16 ++++------------ tests/loaders/test_file_loader.py | 28 +++++++--------------------- tests/test_confz.py | 16 ++++------------ 7 files changed, 18 insertions(+), 52 deletions(-) diff --git a/confz/loaders/cl_arg_loader.py b/confz/loaders/cl_arg_loader.py index d3e3647..3257b8b 100644 --- a/confz/loaders/cl_arg_loader.py +++ b/confz/loaders/cl_arg_loader.py @@ -18,7 +18,7 @@ def populate_config(cls, config: dict, config_source: CLArgSource): if config_source.prefix is not None: if not cl_name.startswith(config_source.prefix): continue - cl_name = cl_name[len(config_source.prefix):] + cl_name = cl_name[len(config_source.prefix) :] if config_source.remap is not None and cl_name in config_source.remap: cl_name = config_source.remap[cl_name] diff --git a/confz/loaders/env_loader.py b/confz/loaders/env_loader.py index 5a1b6e0..8a70a62 100644 --- a/confz/loaders/env_loader.py +++ b/confz/loaders/env_loader.py @@ -66,7 +66,7 @@ def populate_config(cls, config: dict, config_source: EnvSource): if config_source.prefix is not None: if not var_name.startswith(config_source.prefix): continue - var_name = var_name[len(config_source.prefix):] + var_name = var_name[len(config_source.prefix) :] var_name = cls._transform_name(var_name) if not cls._check_allowance(var_name, config_source): diff --git a/confz/loaders/file_loader.py b/confz/loaders/file_loader.py index 53ad083..0fc6538 100644 --- a/confz/loaders/file_loader.py +++ b/confz/loaders/file_loader.py @@ -107,9 +107,7 @@ def _create_stream(cls, file_path: Path, file_encoding: str) -> Iterator[TextIO] try: stream = file_path.open(encoding=file_encoding) except OSError as e: - raise FileException( - f"Could not open config file '{file_path}'." - ) from e + raise FileException(f"Could not open config file '{file_path}'.") from e with stream: yield stream diff --git a/pyproject.toml b/pyproject.toml index 266fbb7..d4409f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,8 +45,6 @@ types-toml = "^0.10.8" [tool.pylint.messages_control] disable = [ - "wrong-hanging-indentation", # to work together with black - "bad-whitespace", # to work together with black "missing-module-docstring", "missing-class-docstring", "missing-function-docstring", diff --git a/tests/loaders/test_env_loader.py b/tests/loaders/test_env_loader.py index 3fbb591..6da667e 100644 --- a/tests/loaders/test_env_loader.py +++ b/tests/loaders/test_env_loader.py @@ -33,9 +33,7 @@ def test_allow_deny(monkeypatch): monkeypatch.setenv("INNER.ATTR-OVERRIDE", "secret") # works if all allowed - config = OuterConfig( - config_sources=EnvSource(allow=["inner.attr1_name", "attr2"]) - ) + config = OuterConfig(config_sources=EnvSource(allow=["inner.attr1_name", "attr2"])) assert config.attr2 == 2 assert config.inner.attr1_name == 1 assert config.inner.attr_override is None @@ -62,17 +60,13 @@ def test_prefix(monkeypatch): monkeypatch.setenv("CONFIG_ATTR2", "2") # prefix works - config = OuterConfig( - config_sources=EnvSource(allow_all=True, prefix="CONFIG_") - ) + config = OuterConfig(config_sources=EnvSource(allow_all=True, prefix="CONFIG_")) assert config.attr2 == 2 assert config.inner.attr1_name == 1 # allow does not use prefix config = OuterConfig( - config_sources=EnvSource( - allow=["inner.attr1_name", "attr2"], prefix="CONFIG_" - ) + config_sources=EnvSource(allow=["inner.attr1_name", "attr2"], prefix="CONFIG_") ) assert config.attr2 == 2 assert config.inner.attr1_name == 1 @@ -80,9 +74,7 @@ def test_prefix(monkeypatch): # deny does not use prefix with pytest.raises(ValidationError): OuterConfig( - config_sources=EnvSource( - allow_all=True, deny=["attr2"], prefix="CONFIG_" - ) + config_sources=EnvSource(allow_all=True, deny=["attr2"], prefix="CONFIG_") ) diff --git a/tests/loaders/test_file_loader.py b/tests/loaders/test_file_loader.py index 5ce5a32..799b79c 100644 --- a/tests/loaders/test_file_loader.py +++ b/tests/loaders/test_file_loader.py @@ -28,17 +28,13 @@ class SecondOuterConfig(OuterConfig): def test_json_file(): - config = OuterConfig( - config_sources=FileSource(file=ASSET_FOLDER / "config.json") - ) + config = OuterConfig(config_sources=FileSource(file=ASSET_FOLDER / "config.json")) assert config.inner.attr1 == "1 🎉" assert config.attr2 == "2" def test_yaml_file(): - config = OuterConfig( - config_sources=FileSource(file=ASSET_FOLDER / "config.yml") - ) + config = OuterConfig(config_sources=FileSource(file=ASSET_FOLDER / "config.yml")) assert config.inner.attr1 == "1 🎉" assert config.attr2 == "2" @@ -58,9 +54,7 @@ def test_multiple_yaml_files_one_is_optional_and_unavailable(): config = OuterConfig( config_sources=[ FileSource(file=ASSET_FOLDER / "config.yml"), - FileSource( - file=ASSET_FOLDER / "config_not_existing.yml", optional=True - ), + FileSource(file=ASSET_FOLDER / "config_not_existing.yml", optional=True), ] ) assert config.inner.attr1 == "1 🎉" @@ -134,17 +128,13 @@ def test_wrong_format(): def test_invalid_file(): with pytest.raises(FileException): - OuterConfig( - config_sources=FileSource(file=ASSET_FOLDER / "non_existing.json") - ) + OuterConfig(config_sources=FileSource(file=ASSET_FOLDER / "non_existing.json")) def test_invalid_file_str(): with pytest.raises(FileException): OuterConfig( - config_sources=FileSource( - file=str(ASSET_FOLDER) + "/non_existing.json" - ) + config_sources=FileSource(file=str(ASSET_FOLDER) + "/non_existing.json") ) @@ -210,18 +200,14 @@ def test_from_cl_arg_name(monkeypatch): # raises error if not set with pytest.raises(FileException): OuterConfig( - config_sources=FileSource( - file_from_cl=cl_arg_name, folder=ASSET_FOLDER - ) + config_sources=FileSource(file_from_cl=cl_arg_name, folder=ASSET_FOLDER) ) # raises error if missing value with pytest.raises(FileException): monkeypatch.setattr(sys, "argv", argv_backup + [cl_arg_name]) OuterConfig( - config_sources=FileSource( - file_from_cl=cl_arg_name, folder=ASSET_FOLDER - ) + config_sources=FileSource(file_from_cl=cl_arg_name, folder=ASSET_FOLDER) ) # works if set diff --git a/tests/test_confz.py b/tests/test_confz.py index b9b2d8f..6fb09a8 100644 --- a/tests/test_confz.py +++ b/tests/test_confz.py @@ -17,17 +17,13 @@ class OuterConfig(BaseConfig): class ParentConfig1(OuterConfig): attr3: int - CONFIG_SOURCES = DataSource( - data={"inner": {"attr1": 1}, "attr2": 2, "attr3": 3} - ) + CONFIG_SOURCES = DataSource(data={"inner": {"attr1": 1}, "attr2": 2, "attr3": 3}) class ParentConfig2(OuterConfig): attr4: int - CONFIG_SOURCES = DataSource( - data={"inner": {"attr1": 1}, "attr2": 2, "attr4": 4} - ) + CONFIG_SOURCES = DataSource(data={"inner": {"attr1": 1}, "attr2": 2, "attr4": 4}) class ParentConfig3(OuterConfig): @@ -77,14 +73,10 @@ def test_init_arg(): # no singleton assert ParentConfig1() is ParentConfig1() config1 = ParentConfig1( - config_sources=DataSource( - data={"inner": {"attr1": 1}, "attr2": 2, "attr3": 3} - ) + config_sources=DataSource(data={"inner": {"attr1": 1}, "attr2": 2, "attr3": 3}) ) config2 = ParentConfig1( - config_sources=DataSource( - data={"inner": {"attr1": 1}, "attr2": 2, "attr3": 3} - ) + config_sources=DataSource(data={"inner": {"attr1": 1}, "attr2": 2, "attr3": 3}) ) assert config1 == config2 assert config1 is not config2 From 82801db2f1f8687385a69358f9ecc7f4d33544d6 Mon Sep 17 00:00:00 2001 From: Silvan Melchior Date: Fri, 14 Jul 2023 17:25:08 +0200 Subject: [PATCH 5/6] fix missing renaming --- docs/source/conf.py | 2 +- docs/source/usage/sources_loaders.rst | 2 +- tests/loaders/test_loader.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 0e6eabc..372e26a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -71,6 +71,6 @@ autodoc_member_order = "bysource" -autodoc_type_aliases = {"ConfZSources": "confz.confz_source.ConfZSources"} +autodoc_type_aliases = {"ConfigSources": "confz.config_source.ConfigSources"} intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} diff --git a/docs/source/usage/sources_loaders.rst b/docs/source/usage/sources_loaders.rst index 7b4a552..941c9d5 100644 --- a/docs/source/usage/sources_loaders.rst +++ b/docs/source/usage/sources_loaders.rst @@ -16,7 +16,7 @@ There are multiple config sources which support a heterogeneous set of use-cases allow to easily configure multiple environments by having a separate file for each environment. - :class:`~confz.EnvSource` allows to load config data from environment variables and .env files. It supports to select the corresponding variables with allow- and deny-lists and with an optional prefix and optional custom separator for nested variables. The variable names are - either inferred from the config name (see :class:`~confz.confz_source.EnvSource` for the rules) or can be explicitly mapped. + either inferred from the config name (see :class:`~confz.config_source.EnvSource` for the rules) or can be explicitly mapped. - :class:`~confz.CLArgSource` allows to load config data from command line arguments. An optional prefix allows to select only parts of the arguments. Optional custom separator for nested command line arguments is also supported. The argument names are either inferred from the config name or can be explicitly mapped. diff --git a/tests/loaders/test_loader.py b/tests/loaders/test_loader.py index aafa88c..c5dc919 100644 --- a/tests/loaders/test_loader.py +++ b/tests/loaders/test_loader.py @@ -28,9 +28,9 @@ class CustomSource2(ConfigSource): class CustomLoader(Loader): @classmethod - def populate_config(cls, config: dict, confz_source: CustomSource): + def populate_config(cls, config: dict, config_source: CustomSource): assert config == {"kwarg_2": 2} - assert confz_source.custom_attr == 1 + assert config_source.custom_attr == 1 config["attr2"] = 2 config["inner"] = {"attr1": 1} From 46e93ccd123a70e4d66a52281ae8d9d0fb1b523f Mon Sep 17 00:00:00 2001 From: Silvan Melchior Date: Fri, 14 Jul 2023 17:53:16 +0200 Subject: [PATCH 6/6] migration guide --- README.md | 2 ++ confz/base_config.py | 2 +- docs/source/index.rst | 1 + docs/source/migration_guide.rst | 35 +++++++++++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 docs/source/migration_guide.rst diff --git a/README.md b/README.md index 2d4daa2..4822bac 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ It furthermore supports you in common use cases like: * Config changes for unit tests * Custom config sources +**UPDATE**: ConfZ 2 is here, with support for pydantic 2 and improved naming conventions. +Check out the [migration guide](https://confz.readthedocs.io/en/latest/migration_guide.html). ## :package: Installation diff --git a/confz/base_config.py b/confz/base_config.py index 8682051..21a2af5 100644 --- a/confz/base_config.py +++ b/confz/base_config.py @@ -53,7 +53,7 @@ def __call__(cls, config_sources: Optional[ConfigSources] = None, **kwargs): class BaseConfig(BaseModel, metaclass=BaseConfigMetaclass, frozen=True): - """Base class, parent of every config class. Internally wraps :class:`BaseModel`of + """Base class, parent of every config class. Internally wraps :class:`BaseModel` of pydantic and behaves transparent except for two cases: - If the constructor gets `config_sources` as kwarg, these sources are used as diff --git a/docs/source/index.rst b/docs/source/index.rst index fc6c3c8..e75a6ab 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -39,6 +39,7 @@ Contents :maxdepth: 2 usage/usage + migration_guide reference/reference diff --git a/docs/source/migration_guide.rst b/docs/source/migration_guide.rst new file mode 100644 index 0000000..87e7816 --- /dev/null +++ b/docs/source/migration_guide.rst @@ -0,0 +1,35 @@ +Migration Guide +=============== + +This guide helps you to easily migrate to ConfZ 2, which supports pydantic 2 and +improves naming conventions. + +New Class Names +--------------- + +We renamed many classes to better reflect their purpose instead of being tied to the +package name. The following table summarizes all changes. Please make sure you adjust +your imports accordingly. + +==================== =============== +ConfZ v1 ConfZ v2 +==================== =============== +ConfZ BaseConfig +ConfZSource ConfigSource +ConfZSources ConfigSources +ConfZCLArgSource CLArgSource +ConfZDataSource DataSource +ConfZEnvSource EnvSource +ConfZFileSource FileSource +ConfZException ConfigException +ConfZUpdateException UpdateException +ConfZFileException FileException +==================== =============== + +Pydantic v2 +----------- + +Once initialized, a confz BaseConfig class behaves mostly like a regular pydantic +BaseModel class. Pydantic 2 comes with quite some changes, which might affect your code, +depending on the used functionalities. Check out the +`migration guide `_.