Skip to content

Commit

Permalink
Merge pull request #75 from Zuehlke/rename-classes
Browse files Browse the repository at this point in the history
Rename classes
  • Loading branch information
silvanmelchior authored Jul 14, 2023
2 parents 897fa23 + 46e93cc commit 9f82f80
Show file tree
Hide file tree
Showing 32 changed files with 364 additions and 362 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand Down
42 changes: 22 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -35,19 +37,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
Expand Down Expand Up @@ -84,11 +86,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'
)
Expand All @@ -100,13 +102,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_')
]
```

Expand All @@ -121,20 +123,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
Expand All @@ -144,15 +146,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'

Expand Down
30 changes: 15 additions & 15 deletions confz/__init__.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
from .change import depends_on
from .confz import ConfZ
from .confz_source import (
ConfZSources,
ConfZSource,
ConfZFileSource,
ConfZEnvSource,
ConfZCLArgSource,
from .base_config import BaseConfig
from .config_source import (
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",
]
41 changes: 20 additions & 21 deletions confz/confz.py → confz/base_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,32 @@
from pydantic import BaseModel

from .change import SourceChangeManager
from .confz_source import ConfZSources
from .exceptions import ConfZException
from .config_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, 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


# 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)
Expand All @@ -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"
)
Expand All @@ -53,8 +52,8 @@ def __call__(cls, config_sources: Optional[ConfZSources] = None, **kwargs):
return super().__call__(**kwargs)


class ConfZ(BaseModel, metaclass=ConfZMetaclass, frozen=True):
"""Base class, parent of every config class. Internally wraps :class:`BaseModel`of
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:
- If the constructor gets `config_sources` as kwarg, these sources are used as
Expand All @@ -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*
Expand All @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions confz/change.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@
TYPE_CHECKING,
)

from .confz_source import ConfZSources
from .config_source import ConfigSources

if TYPE_CHECKING:
from .confz import ConfZ
from .base_config 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
Expand Down Expand Up @@ -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")

Expand Down
14 changes: 7 additions & 7 deletions confz/confz_source.py → confz/config_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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."""
Expand Down
6 changes: 3 additions & 3 deletions confz/exceptions.py
Original file line number Diff line number Diff line change
@@ -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."""
16 changes: 8 additions & 8 deletions confz/loaders/cl_arg_loader.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import sys

from confz.confz_source import ConfZCLArgSource
from confz.config_source import CLArgSource
from .loader import Loader


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, 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)
6 changes: 3 additions & 3 deletions confz/loaders/data_loader.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from confz.confz_source import ConfZDataSource
from confz.config_source import DataSource
from .loader import Loader


class DataLoader(Loader):
"""Config loader for fix data."""

@classmethod
def populate_config(cls, config: dict, confz_source: ConfZDataSource):
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)
Loading

0 comments on commit 9f82f80

Please sign in to comment.