mirror of
https://github.com/ets-labs/python-dependency-injector.git
synced 2024-11-22 09:36:48 +03:00
274 Configuration provider redesign (#275)
* Get 1st stable version * Remove prototype module * Try fix copying * Add config itemselector example * Add doc blocks
This commit is contained in:
parent
459ff5fcf5
commit
ea1e79885c
|
@ -0,0 +1,47 @@
|
||||||
|
"""`Configuration` provider dynamic item selector.
|
||||||
|
|
||||||
|
Details: https://github.com/ets-labs/python-dependency-injector/issues/274
|
||||||
|
"""
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
|
from dependency_injector import providers
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class Foo:
|
||||||
|
option1: object
|
||||||
|
option2: object
|
||||||
|
|
||||||
|
|
||||||
|
config = providers.Configuration(default={
|
||||||
|
'target': 'A',
|
||||||
|
'items': {
|
||||||
|
'A': {
|
||||||
|
'option1': 60,
|
||||||
|
'option2': 80,
|
||||||
|
},
|
||||||
|
'B': {
|
||||||
|
'option1': 10,
|
||||||
|
'option2': 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
foo = providers.Factory(
|
||||||
|
Foo,
|
||||||
|
option1=config.items[config.target].option1,
|
||||||
|
option2=config.items[config.target].option2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
config.target.from_env('TARGET')
|
||||||
|
f = foo()
|
||||||
|
print(f.option1, f.option2)
|
||||||
|
|
||||||
|
|
||||||
|
# $ TARGET=A python configuration_itemselector.py
|
||||||
|
# 60 80
|
||||||
|
# $ TARGET=B python configuration_itemselector.py
|
||||||
|
# 10 20
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -89,10 +89,17 @@ cdef class CoroutineDelegate(Delegate):
|
||||||
|
|
||||||
|
|
||||||
# Configuration providers
|
# Configuration providers
|
||||||
|
cdef class ConfigurationOption(Provider):
|
||||||
|
cdef tuple __name
|
||||||
|
cdef object __root_ref
|
||||||
|
cdef dict __children
|
||||||
|
cdef object __cache
|
||||||
|
|
||||||
|
|
||||||
cdef class Configuration(Object):
|
cdef class Configuration(Object):
|
||||||
cdef str __name
|
cdef str __name
|
||||||
cdef dict __children
|
cdef dict __children
|
||||||
cdef list __linked
|
cdef object __weakref__
|
||||||
|
|
||||||
|
|
||||||
# Factory providers
|
# Factory providers
|
||||||
|
|
|
@ -9,6 +9,7 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
import threading
|
import threading
|
||||||
|
import weakref
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import asyncio
|
import asyncio
|
||||||
|
@ -1045,158 +1046,101 @@ cdef class CoroutineDelegate(Delegate):
|
||||||
super(CoroutineDelegate, self).__init__(coroutine)
|
super(CoroutineDelegate, self).__init__(coroutine)
|
||||||
|
|
||||||
|
|
||||||
cdef class Configuration(Object):
|
cdef class ConfigurationOption(Provider):
|
||||||
"""Configuration provider provides configuration options to the other providers.
|
"""Child configuration option provider.
|
||||||
|
|
||||||
.. code-block:: python
|
This provider should not be used directly. It is a part of the
|
||||||
|
:py:class:`Configuration` provider.
|
||||||
config = Configuration('config')
|
|
||||||
|
|
||||||
print(config.section1.option1()) # None
|
|
||||||
print(config.section1.option2()) # None
|
|
||||||
|
|
||||||
config.from_dict(
|
|
||||||
{
|
|
||||||
'section1': {
|
|
||||||
'option1': 1,
|
|
||||||
'option2': 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
print(config.section1.option1()) # 1
|
|
||||||
print(config.section1.option2()) # 2
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_NAME = 'config'
|
UNDEFINED = object()
|
||||||
|
|
||||||
def __init__(self, name=None, default=None):
|
|
||||||
"""Initializer.
|
|
||||||
|
|
||||||
:param name: Name of configuration unit.
|
|
||||||
:type name: str
|
|
||||||
|
|
||||||
:param default: Default values of configuration unit.
|
|
||||||
:type default: dict
|
|
||||||
"""
|
|
||||||
super(Configuration, self).__init__(default)
|
|
||||||
|
|
||||||
if name is None:
|
|
||||||
name = self.DEFAULT_NAME
|
|
||||||
|
|
||||||
|
def __init__(self, name, root):
|
||||||
self.__name = name
|
self.__name = name
|
||||||
self.__children = self._create_children(default)
|
self.__root_ref = weakref.ref(root)
|
||||||
self.__linked = list()
|
self.__children = {}
|
||||||
|
self.__cache = self.UNDEFINED
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
def __deepcopy__(self, memo):
|
def __deepcopy__(self, memo):
|
||||||
"""Create and return full copy of provider."""
|
cdef ConfigurationOption copied
|
||||||
cdef Configuration copied
|
|
||||||
|
|
||||||
copied = memo.get(id(self))
|
copied = memo.get(id(self))
|
||||||
if copied is not None:
|
if copied is not None:
|
||||||
return copied
|
return copied
|
||||||
|
|
||||||
copied = self.__class__(self.__name)
|
copied_name = deepcopy(self.__name, memo)
|
||||||
copied.__provides = deepcopy(self.__provides, memo)
|
|
||||||
copied.__children = deepcopy(self.__children, memo)
|
|
||||||
copied.__linked = deepcopy(self.__linked, memo)
|
|
||||||
|
|
||||||
self._copy_overridings(copied, memo)
|
root = self.__root_ref()
|
||||||
|
copied_root = memo.get(id(root))
|
||||||
|
if copied_root is None:
|
||||||
|
copied_root = deepcopy(root, memo)
|
||||||
|
|
||||||
|
copied = self.__class__(copied_name, copied_root)
|
||||||
|
copied.__children = deepcopy(self.__children, memo)
|
||||||
|
|
||||||
return copied
|
return copied
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return string representation of provider.
|
return represent_provider(provider=self, provides=self.get_name())
|
||||||
|
|
||||||
:rtype: str
|
def __getattr__(self, item):
|
||||||
"""
|
if item.startswith('__') and item.endswith('__'):
|
||||||
return represent_provider(provider=self, provides=self.__name)
|
|
||||||
|
|
||||||
def __getattr__(self, str name):
|
|
||||||
"""Return child configuration provider."""
|
|
||||||
if name.startswith('__') and name.endswith('__'):
|
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
'\'{cls}\' object has no attribute '
|
'\'{cls}\' object has no attribute '
|
||||||
'\'{attribute_name}\''.format(cls=self.__class__.__name__,
|
'\'{attribute_name}\''.format(cls=self.__class__.__name__,
|
||||||
attribute_name=name))
|
attribute_name=item))
|
||||||
|
|
||||||
child_provider = self.__children.get(name)
|
child = self.__children.get(item)
|
||||||
|
if child is None:
|
||||||
|
child_name = self.__name + (item,)
|
||||||
|
child = ConfigurationOption(child_name, self.__root_ref())
|
||||||
|
self.__children[item] = child
|
||||||
|
return child
|
||||||
|
|
||||||
if child_provider is None:
|
def __getitem__(self, item):
|
||||||
child_name = self._get_child_full_name(name)
|
child = self.__children.get(item)
|
||||||
child_provider = self.__class__(child_name)
|
if child is None:
|
||||||
|
child_name = self.__name + (item,)
|
||||||
|
child = ConfigurationOption(child_name, self.__root_ref())
|
||||||
|
self.__children[item] = child
|
||||||
|
return child
|
||||||
|
|
||||||
value = self.__call__()
|
cpdef object _provide(self, tuple args, dict kwargs):
|
||||||
if isinstance(value, dict):
|
"""Return new instance."""
|
||||||
child_value = value.get(name)
|
if self.__cache is not self.UNDEFINED:
|
||||||
child_provider.override(child_value)
|
return self.__cache
|
||||||
|
|
||||||
self.__children[name] = child_provider
|
root = self.__root_ref()
|
||||||
|
value = root.get(self._get_self_name())
|
||||||
|
self.__cache = value
|
||||||
|
return value
|
||||||
|
|
||||||
return child_provider
|
def _get_self_name(self):
|
||||||
|
return '.'.join(
|
||||||
|
segment() if is_provider(segment) else segment for segment in self.__name
|
||||||
|
)
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self):
|
||||||
"""Name of configuration unit."""
|
root = self.__root_ref()
|
||||||
return self.__name
|
return '.'.join((root.get_name(), self._get_self_name()))
|
||||||
|
|
||||||
def override(self, provider):
|
def override(self, value):
|
||||||
"""Override provider with another provider.
|
if isinstance(value, Provider):
|
||||||
|
raise Error('Configuration option can only be overridden by a value')
|
||||||
:param provider: Overriding provider.
|
root = self.__root_ref()
|
||||||
:type provider: :py:class:`Provider`
|
return root.set(self._get_self_name(), value)
|
||||||
|
|
||||||
:raise: :py:exc:`dependency_injector.errors.Error`
|
|
||||||
|
|
||||||
:return: Overriding context.
|
|
||||||
:rtype: :py:class:`OverridingContext`
|
|
||||||
"""
|
|
||||||
overriding_context = super(Configuration, self).override(provider)
|
|
||||||
|
|
||||||
for linked in self.__linked:
|
|
||||||
linked.override(provider)
|
|
||||||
|
|
||||||
if isinstance(provider, Configuration):
|
|
||||||
provider.link_provider(self)
|
|
||||||
|
|
||||||
value = self.__call__()
|
|
||||||
if not isinstance(value, dict):
|
|
||||||
return overriding_context
|
|
||||||
|
|
||||||
for name in value.keys():
|
|
||||||
child_provider = self.__children.get(name)
|
|
||||||
if child_provider is None:
|
|
||||||
continue
|
|
||||||
child_provider.override(value.get(name))
|
|
||||||
|
|
||||||
return overriding_context
|
|
||||||
|
|
||||||
def reset_last_overriding(self):
|
def reset_last_overriding(self):
|
||||||
"""Reset last overriding provider.
|
raise Error('Configuration option does not support this method')
|
||||||
|
|
||||||
:raise: :py:exc:`dependency_injector.errors.Error` if provider is not
|
|
||||||
overridden.
|
|
||||||
|
|
||||||
:rtype: None
|
|
||||||
"""
|
|
||||||
for child in self.__children.values():
|
|
||||||
try:
|
|
||||||
child.reset_last_overriding()
|
|
||||||
except Error:
|
|
||||||
pass
|
|
||||||
super(Configuration, self).reset_last_overriding()
|
|
||||||
|
|
||||||
def reset_override(self):
|
def reset_override(self):
|
||||||
"""Reset all overriding providers.
|
raise Error('Configuration option does not support this method')
|
||||||
|
|
||||||
:rtype: None
|
def reset_cache(self):
|
||||||
"""
|
self.__cache = self.UNDEFINED
|
||||||
for child in self.__children.values():
|
for child in self.__children.values():
|
||||||
child.reset_override()
|
child.reset_cache()
|
||||||
super(Configuration, self).reset_override()
|
|
||||||
|
|
||||||
def link_provider(self, provider):
|
|
||||||
"""Configuration link two configuration providers."""
|
|
||||||
self.__linked.append(<Configuration?>provider)
|
|
||||||
|
|
||||||
def update(self, value):
|
def update(self, value):
|
||||||
"""Set configuration options.
|
"""Set configuration options.
|
||||||
|
@ -1290,27 +1234,260 @@ cdef class Configuration(Object):
|
||||||
value = os.getenv(name, default)
|
value = os.getenv(name, default)
|
||||||
self.override(value)
|
self.override(value)
|
||||||
|
|
||||||
def _create_children(self, value):
|
|
||||||
children = dict()
|
|
||||||
|
|
||||||
if not isinstance(value, dict):
|
cdef class Configuration(Object):
|
||||||
return children
|
"""Configuration provider provides configuration options to the other providers.
|
||||||
|
|
||||||
for child_name, child_value in value.items():
|
.. code-block:: python
|
||||||
child_full_name = self._get_child_full_name(child_name)
|
config = Configuration('config')
|
||||||
child_provider = self.__class__(child_full_name, child_value)
|
print(config.section1.option1()) # None
|
||||||
children[child_name] = child_provider
|
print(config.section1.option2()) # None
|
||||||
|
config.from_dict(
|
||||||
|
{
|
||||||
|
'section1': {
|
||||||
|
'option1': 1,
|
||||||
|
'option2': 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
print(config.section1.option1()) # 1
|
||||||
|
print(config.section1.option2()) # 2
|
||||||
|
"""
|
||||||
|
|
||||||
return children
|
DEFAULT_NAME = 'config'
|
||||||
|
|
||||||
def _get_child_full_name(self, child_name):
|
def __init__(self, name=DEFAULT_NAME, default=None):
|
||||||
child_full_name = ''
|
self.__name = name
|
||||||
|
|
||||||
if self.__name:
|
value = {}
|
||||||
child_full_name += self.__name + '.'
|
if default is not None:
|
||||||
child_full_name += child_name
|
assert isinstance(default, dict), default
|
||||||
|
value = default.copy()
|
||||||
|
|
||||||
return child_full_name
|
self.__children = {}
|
||||||
|
|
||||||
|
super().__init__(value)
|
||||||
|
|
||||||
|
def __deepcopy__(self, memo):
|
||||||
|
cdef Configuration copied
|
||||||
|
|
||||||
|
copied = memo.get(id(self))
|
||||||
|
if copied is not None:
|
||||||
|
return copied
|
||||||
|
|
||||||
|
copied = self.__class__(self.__name, self.__provides)
|
||||||
|
memo[id(self)] = copied
|
||||||
|
|
||||||
|
copied.__children = deepcopy(self.__children, memo)
|
||||||
|
self._copy_overridings(copied, memo)
|
||||||
|
|
||||||
|
return copied
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return represent_provider(provider=self, provides=self.__name)
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
if item.startswith('__') and item.endswith('__'):
|
||||||
|
raise AttributeError(
|
||||||
|
'\'{cls}\' object has no attribute '
|
||||||
|
'\'{attribute_name}\''.format(cls=self.__class__.__name__,
|
||||||
|
attribute_name=item))
|
||||||
|
|
||||||
|
child = self.__children.get(item)
|
||||||
|
if child is None:
|
||||||
|
child = ConfigurationOption((item,), self)
|
||||||
|
self.__children[item] = child
|
||||||
|
return child
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
child = self.__children.get(item)
|
||||||
|
if child is None:
|
||||||
|
child = ConfigurationOption(item, self)
|
||||||
|
self.__children[item] = child
|
||||||
|
return child
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return self.__name
|
||||||
|
|
||||||
|
def get(self, selector):
|
||||||
|
"""Return configuration option.
|
||||||
|
|
||||||
|
:param selector: Selector string, e.g. "option1.option2"
|
||||||
|
:type selector: str
|
||||||
|
|
||||||
|
:return: Option value.
|
||||||
|
:rtype: Any
|
||||||
|
"""
|
||||||
|
keys = selector.split('.')
|
||||||
|
value = self.__call__()
|
||||||
|
|
||||||
|
while len(keys) > 0:
|
||||||
|
key = keys.pop(0)
|
||||||
|
value = value.get(key)
|
||||||
|
if value is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def set(self, selector, value):
|
||||||
|
"""Override configuration option.
|
||||||
|
|
||||||
|
:param selector: Selector string, e.g. "option1.option2"
|
||||||
|
:type selector: str
|
||||||
|
|
||||||
|
:param value: Overriding value
|
||||||
|
:type value: Any
|
||||||
|
|
||||||
|
:return: Overriding context.
|
||||||
|
:rtype: :py:class:`OverridingContext`
|
||||||
|
"""
|
||||||
|
keys = selector.split('.')
|
||||||
|
original_value = current_value = self.__call__()
|
||||||
|
|
||||||
|
while len(keys) > 0:
|
||||||
|
key = keys.pop(0)
|
||||||
|
if len(keys) == 0:
|
||||||
|
current_value[key] = value
|
||||||
|
break
|
||||||
|
temp_value = current_value.get(key, {})
|
||||||
|
current_value[key] = temp_value
|
||||||
|
current_value = temp_value
|
||||||
|
|
||||||
|
return self.override(original_value)
|
||||||
|
|
||||||
|
def override(self, provider):
|
||||||
|
"""Override provider with another provider.
|
||||||
|
|
||||||
|
:param provider: Overriding provider.
|
||||||
|
:type provider: :py:class:`Provider`
|
||||||
|
|
||||||
|
:raise: :py:exc:`dependency_injector.errors.Error`
|
||||||
|
|
||||||
|
:return: Overriding context.
|
||||||
|
:rtype: :py:class:`OverridingContext`
|
||||||
|
"""
|
||||||
|
context = super().override(provider)
|
||||||
|
self.reset_cache()
|
||||||
|
return context
|
||||||
|
|
||||||
|
def reset_last_overriding(self):
|
||||||
|
"""Reset last overriding provider.
|
||||||
|
|
||||||
|
:raise: :py:exc:`dependency_injector.errors.Error` if provider is not
|
||||||
|
overridden.
|
||||||
|
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
super().reset_last_overriding()
|
||||||
|
self.reset_cache()
|
||||||
|
|
||||||
|
def reset_override(self):
|
||||||
|
"""Reset all overriding providers.
|
||||||
|
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
super().reset_override()
|
||||||
|
self.reset_cache()
|
||||||
|
|
||||||
|
def reset_cache(self):
|
||||||
|
"""Reset children providers cache.
|
||||||
|
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
for child in self.__children.values():
|
||||||
|
child.reset_cache()
|
||||||
|
|
||||||
|
def update(self, value):
|
||||||
|
"""Set configuration options.
|
||||||
|
|
||||||
|
.. deprecated:: 3.11
|
||||||
|
|
||||||
|
Use :py:meth:`Configuration.override` instead.
|
||||||
|
|
||||||
|
:param value: Value of configuration option.
|
||||||
|
:type value: object | dict
|
||||||
|
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
self.override(value)
|
||||||
|
|
||||||
|
def from_ini(self, filepath):
|
||||||
|
"""Load configuration from the ini file.
|
||||||
|
|
||||||
|
Loaded configuration is merged recursively over existing configuration.
|
||||||
|
|
||||||
|
:param filepath: Path to the configuration file.
|
||||||
|
:type filepath: str
|
||||||
|
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
parser = _parse_ini_file(filepath)
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
for section in parser.sections():
|
||||||
|
config[section] = dict(parser.items(section))
|
||||||
|
|
||||||
|
current_config = self.__call__()
|
||||||
|
if not current_config:
|
||||||
|
current_config = {}
|
||||||
|
self.override(merge_dicts(current_config, config))
|
||||||
|
|
||||||
|
def from_yaml(self, filepath):
|
||||||
|
"""Load configuration from the yaml file.
|
||||||
|
|
||||||
|
Loaded configuration is merged recursively over existing configuration.
|
||||||
|
|
||||||
|
:param filepath: Path to the configuration file.
|
||||||
|
:type filepath: str
|
||||||
|
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
if yaml is None:
|
||||||
|
raise Error(
|
||||||
|
'Unable to load yaml configuration - PyYAML is not installed. '
|
||||||
|
'Install PyYAML or install Dependency Injector with yaml extras: '
|
||||||
|
'"pip install dependency-injector[yaml]"'
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filepath) as opened_file:
|
||||||
|
config = yaml.load(opened_file, yaml.Loader)
|
||||||
|
except IOError:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_config = self.__call__()
|
||||||
|
if not current_config:
|
||||||
|
current_config = {}
|
||||||
|
self.override(merge_dicts(current_config, config))
|
||||||
|
|
||||||
|
def from_dict(self, options):
|
||||||
|
"""Load configuration from the dictionary.
|
||||||
|
|
||||||
|
Loaded configuration is merged recursively over existing configuration.
|
||||||
|
|
||||||
|
:param options: Configuration options.
|
||||||
|
:type options: dict
|
||||||
|
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
current_config = self.__call__()
|
||||||
|
if not current_config:
|
||||||
|
current_config = {}
|
||||||
|
self.override(merge_dicts(current_config, options))
|
||||||
|
|
||||||
|
def from_env(self, name, default=None):
|
||||||
|
"""Load configuration value from the environment variable.
|
||||||
|
|
||||||
|
:param name: Name of the environment variable.
|
||||||
|
:type name: str
|
||||||
|
|
||||||
|
:param default: Default value that is used if environment variable does not exist.
|
||||||
|
:type default: str
|
||||||
|
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
value = os.getenv(name, default)
|
||||||
|
self.override(value)
|
||||||
|
|
||||||
|
|
||||||
cdef class Factory(Provider):
|
cdef class Factory(Provider):
|
||||||
|
|
|
@ -183,7 +183,7 @@ class ConfigTests(unittest.TestCase):
|
||||||
def test_repr_child(self):
|
def test_repr_child(self):
|
||||||
self.assertEqual(repr(self.config.a.b.c),
|
self.assertEqual(repr(self.config.a.b.c),
|
||||||
'<dependency_injector.providers.'
|
'<dependency_injector.providers.'
|
||||||
'Configuration({0}) at {1}>'.format(
|
'ConfigurationOption({0}) at {1}>'.format(
|
||||||
repr('config.a.b.c'),
|
repr('config.a.b.c'),
|
||||||
hex(id(self.config.a.b.c))))
|
hex(id(self.config.a.b.c))))
|
||||||
|
|
||||||
|
@ -565,6 +565,6 @@ class ConfigFromEnvTests(unittest.TestCase):
|
||||||
def test_with_children(self):
|
def test_with_children(self):
|
||||||
self.config.section1.value1.from_env('CONFIG_TEST_ENV')
|
self.config.section1.value1.from_env('CONFIG_TEST_ENV')
|
||||||
|
|
||||||
self.assertIsNone(self.config())
|
self.assertEqual(self.config(), {'section1': {'value1': 'test-value'}})
|
||||||
self.assertIsNone(self.config.section1())
|
self.assertEqual(self.config.section1(), {'value1': 'test-value'})
|
||||||
self.assertEqual(self.config.section1.value1(), 'test-value')
|
self.assertEqual(self.config.section1.value1(), 'test-value')
|
||||||
|
|
Loading…
Reference in New Issue
Block a user