Merge branch 'release/3.18.0' into master

This commit is contained in:
Roman Mogylatov 2020-06-25 18:06:58 -04:00
commit 5718140a82
20 changed files with 5674 additions and 3121 deletions

View File

@ -61,8 +61,8 @@ test-py3: build
check:
# Static analysis
flake8 --max-complexity=10 src/dependency_injector/
flake8 --max-complexity=10 examples/
flake8 src/dependency_injector/
flake8 examples/
# Code style analysis
pydocstyle src/dependency_injector/
pydocstyle examples/

View File

@ -7,6 +7,16 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly
follows `Semantic versioning`_
3.18.0
------
- Add ``Configuration.from_yaml()`` method to load configuration from the yaml file.
- Add ``Configuration.from_ini()`` method to load configuration from the ini file.
- Add ``Configuration.from_dict()`` method to load configuration from the dictionary.
- Add ``Configuration.from_env()`` method to load configuration from the environment variable.
- Add default value for ``name`` argument of ``Configuration`` provider.
- Add documentation for ``Configuration`` provider.
- Remove undocumented positional parameter of ``DependenciesContainer`` provider.
3.17.1
------
- Fix ``DynamicContainer`` deep-copying bug.

View File

@ -0,0 +1,88 @@
Configuration providers
-----------------------
.. currentmodule:: dependency_injector.providers
:py:class:`Configuration` provider provides configuration options to the other providers.
.. literalinclude:: ../../examples/providers/configuration/configuration.py
:language: python
:emphasize-lines: 7,12-13,18-25
:linenos:
It implements "use first, define later" principle.
Loading from ``ini`` file
~~~~~~~~~~~~~~~~~~~~~~~~~
:py:class:`Configuration` provider can load configuration from ``ini`` file using
:py:meth:`Configuration.from_ini`:
.. literalinclude:: ../../examples/providers/configuration/configuration_ini.py
:language: python
:lines: 6-
:emphasize-lines: 3
:linenos:
where ``examples/providers/configuration/config.ini`` is:
.. literalinclude:: ../../examples/providers/configuration/config.ini
:language: ini
:linenos:
Loading from ``yaml`` file
~~~~~~~~~~~~~~~~~~~~~~~~~~
:py:class:`Configuration` provider can load configuration from ``yaml`` file using
:py:meth:`Configuration.from_yaml`:
.. literalinclude:: ../../examples/providers/configuration/configuration_yaml.py
:language: python
:lines: 6-
:emphasize-lines: 3
:linenos:
where ``examples/providers/configuration/config.yml`` is:
.. literalinclude:: ../../examples/providers/configuration/config.yml
:language: ini
:linenos:
.. note::
Loading configuration from yaml requires ``PyYAML`` package. You can install
`Dependency Injector` with extras ``pip install dependency-injector[yaml]`` or install
``PyYAML`` separately ``pip install pyyaml``.
Loading from environment variable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:py:class:`Configuration` provider can load configuration from environment variable using
:py:meth:`Configuration.from_env`:
.. literalinclude:: ../../examples/providers/configuration/configuration_env.py
:language: python
:lines: 13-21
:emphasize-lines: 3-5
:linenos:
Loading from multiple sources
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:py:class:`Configuration` provider can load configuration from multiple sources. Loaded
configuration is merged recursively over existing configuration.
.. literalinclude:: ../../examples/providers/configuration/configuration_multiple.py
:language: python
:lines: 6-14
:emphasize-lines: 3-4
:linenos:
where ``examples/providers/configuration/config.local.yml`` is:
.. literalinclude:: ../../examples/providers/configuration/config.local.yml
:language: ini
:linenos:
.. disqus::

View File

@ -23,6 +23,7 @@ Providers package API docs - :py:mod:`dependency_injector.providers`
coroutine
object
list
configuration
dependency
overriding
custom

View File

@ -0,0 +1,3 @@
[aws]
access_key_id = KEY
secret_access_key = SECRET

View File

@ -0,0 +1,3 @@
aws:
access_key_id: "LOCAL-KEY"
secret_access_key: "LOCAL-SECRET"

View File

@ -0,0 +1,3 @@
aws:
access_key_id: "KEY"
secret_access_key: "SECRET"

View File

@ -0,0 +1,26 @@
"""`Configuration` provider example."""
import boto3
from dependency_injector import providers
config = providers.Configuration()
s3_client_factory = providers.Factory(
boto3.client,
's3',
aws_access_key_id=config.aws.access_key_id,
aws_secret_access_key=config.aws.secret_access_key,
)
if __name__ == '__main__':
config.from_dict(
{
'aws': {
'access_key_id': 'KEY',
'secret_access_key': 'SECRET',
},
},
)
s3_client = s3_client_factory()

View File

@ -0,0 +1,21 @@
"""`Configuration` provider values loading example."""
import os
from dependency_injector import providers
# Emulate environment variables
os.environ['AWS_ACCESS_KEY_ID'] = 'KEY'
os.environ['AWS_SECRET_ACCESS_KEY'] = 'SECRET'
config = providers.Configuration()
config.aws.access_key_id.from_env('AWS_ACCESS_KEY_ID')
config.aws.secret_access_key.from_env('AWS_SECRET_ACCESS_KEY')
config.optional.from_env('UNDEFINED', 'default_value')
assert config.aws.access_key_id() == 'KEY'
assert config.aws.secret_access_key() == 'SECRET'
assert config.optional() == 'default_value'

View File

@ -0,0 +1,13 @@
"""`Configuration` provider values loading example."""
from dependency_injector import providers
config = providers.Configuration()
config.from_ini('examples/providers/configuration/config.ini')
assert config() == {'aws': {'access_key_id': 'KEY', 'secret_access_key': 'SECRET'}}
assert config.aws() == {'access_key_id': 'KEY', 'secret_access_key': 'SECRET'}
assert config.aws.access_key_id() == 'KEY'
assert config.aws.secret_access_key() == 'SECRET'

View File

@ -0,0 +1,14 @@
"""`Configuration` provider values loading example."""
from dependency_injector import providers
config = providers.Configuration()
config.from_yaml('examples/providers/configuration/config.yml')
config.from_yaml('examples/providers/configuration/config.local.yml')
assert config() == {'aws': {'access_key_id': 'LOCAL-KEY', 'secret_access_key': 'LOCAL-SECRET'}}
assert config.aws() == {'access_key_id': 'LOCAL-KEY', 'secret_access_key': 'LOCAL-SECRET'}
assert config.aws.access_key_id() == 'LOCAL-KEY'
assert config.aws.secret_access_key() == 'LOCAL-SECRET'

View File

@ -0,0 +1,13 @@
"""`Configuration` provider values loading example."""
from dependency_injector import providers
config = providers.Configuration()
config.from_yaml('examples/providers/configuration/config.yml')
assert config() == {'aws': {'access_key_id': 'KEY', 'secret_access_key': 'SECRET'}}
assert config.aws() == {'access_key_id': 'KEY', 'secret_access_key': 'SECRET'}
assert config.aws.access_key_id() == 'KEY'
assert config.aws.secret_access_key() == 'SECRET'

3
setup.cfg Normal file
View File

@ -0,0 +1,3 @@
[flake8]
max_line_length = 99
max_complexity = 10

View File

@ -39,13 +39,15 @@ setup(name='dependency-injector',
maintainer_email='rmogilatov@gmail.com',
url='https://github.com/ets-labs/python-dependency-injector',
download_url='https://pypi.python.org/pypi/dependency_injector',
install_requires=requirements,
packages=[
'dependency_injector',
],
package_dir={
'': 'src',
},
package_data={
'dependency_injector': ['*.pxd'],
},
ext_modules=[
Extension('dependency_injector.containers',
['src/dependency_injector/containers.c'],
@ -56,8 +58,11 @@ setup(name='dependency-injector',
define_macros=list(defined_macros.items()),
extra_compile_args=['-O2']),
],
package_data={
'dependency_injector': ['*.pxd'],
install_requires=requirements,
extras_require={
'yaml': [
'pyyaml',
],
},
zip_safe=True,
license='BSD New',

View File

@ -1,6 +1,6 @@
"""Dependency injector top-level package."""
__version__ = '3.17.1'
__version__ = '3.18.0'
"""Version number that follows semantic versioning.
:type: str

File diff suppressed because it is too large Load Diff

View File

@ -3,8 +3,8 @@
Powered by Cython.
"""
import copy
import os
import sys
import types
import threading
@ -21,6 +21,15 @@ else:
else:
_is_coroutine_marker = True
try:
import ConfigParser as iniconfigparser
except ImportError:
import configparser as iniconfigparser
try:
import yaml
except ImportError:
yaml = None
from .errors import (
Error,
@ -503,14 +512,10 @@ cdef class DependenciesContainer(Object):
use_case.execute()
"""
def __init__(self, provides=None, **dependencies):
def __init__(self, **dependencies):
"""Initializer."""
self.__providers = dependencies
if provides:
self._override_providers(container=provides)
super(DependenciesContainer, self).__init__(provides)
super(DependenciesContainer, self).__init__(None)
def __deepcopy__(self, memo):
"""Create and return full copy of provider."""
@ -997,10 +1002,7 @@ cdef class CoroutineDelegate(Delegate):
cdef class Configuration(Object):
"""Configuration provider.
Configuration provider helps with implementing late static binding of
configuration options - use first, define later.
"""Configuration provider provides configuration options to the other providers.
.. code-block:: python
@ -1009,14 +1011,22 @@ cdef class Configuration(Object):
print(config.section1.option1()) # None
print(config.section1.option2()) # None
config.override({'section1': {'option1': 1,
'option2': 2}})
config.from_dict(
{
'section1': {
'option1': 1,
'option2': 2,
},
},
)
print(config.section1.option1()) # 1
print(config.section1.option2()) # 2
"""
def __init__(self, name, default=None):
DEFAULT_NAME = 'config'
def __init__(self, name=None, default=None):
"""Initializer.
:param name: Name of configuration unit.
@ -1027,6 +1037,9 @@ cdef class Configuration(Object):
"""
super(Configuration, self).__init__(default)
if name is None:
name = self.DEFAULT_NAME
self.__name = name
self.__children = self._create_children(default)
self.__linked = list()
@ -1155,6 +1168,82 @@ cdef class Configuration(Object):
"""
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 = iniconfigparser.ConfigParser()
parser.read([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]"'
)
with open(filepath) as opened_file:
config = yaml.load(opened_file, yaml.Loader)
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)
def _create_children(self, value):
children = dict()
@ -2119,7 +2208,6 @@ cdef class Container(Provider):
deepcopy(self.container, memo),
**deepcopy(self.overriding_providers, memo),
)
# self._copy_overridings(copied, memo)
return copied
@ -2321,3 +2409,24 @@ def __add_sys_streams(memo):
memo[id(sys.stdin)] = sys.stdin
memo[id(sys.stdout)] = sys.stdout
memo[id(sys.stderr)] = sys.stderr
def merge_dicts(dict1, dict2):
"""Merge dictionaries recursively.
:param dict1: Dictionary 1
:type dict1: dict
:param dict2: Dictionary 2
:type dict2: dict
:return: New resulting dictionary
:rtype: dict
"""
for key, value in dict1.items():
if key in dict2:
if isinstance(value, dict) and isinstance(dict2[key], dict):
dict2[key] = merge_dicts(value, dict2[key])
result = dict1.copy()
result.update(dict2)
return result

View File

@ -367,11 +367,3 @@ class DependenciesContainerTests(unittest.TestCase):
self.assertFalse(dependency.overridden)
self.assertFalse(dependency.overridden)
def test_init_with_container_and_providers(self):
provider = providers.DependenciesContainer(
self.container, dependency=providers.Dependency())
dependency = provider.dependency
self.assertTrue(dependency.overridden)
self.assertIs(dependency.last_overriding, self.container.dependency)

View File

@ -1,8 +1,13 @@
"""Dependency injector config providers unit tests."""
import contextlib
import os
import sys
import tempfile
import unittest2 as unittest
from dependency_injector import containers, providers
from dependency_injector import containers, providers, errors
class ConfigTests(unittest.TestCase):
@ -13,6 +18,10 @@ class ConfigTests(unittest.TestCase):
def tearDown(self):
del self.config
def test_default_name(self):
config = providers.Configuration()
self.assertEqual(config.get_name(), 'config')
def test_providers_are_providers(self):
self.assertTrue(providers.is_provider(self.config.a))
self.assertTrue(providers.is_provider(self.config.a.b))
@ -246,3 +255,246 @@ class ConfigLinkingTests(unittest.TestCase):
self.assertEqual(services.config(), {'value': 'services2'})
self.assertEqual(services.config.value(), 'services2')
self.assertEqual(services.value_getter(), 'services2')
class ConfigFromIniTests(unittest.TestCase):
def setUp(self):
self.config = providers.Configuration(name='config')
_, self.config_file_1 = tempfile.mkstemp()
with open(self.config_file_1, 'w') as config_file:
config_file.write(
'[section1]\n'
'value1=1\n'
'\n'
'[section2]\n'
'value2=2\n'
)
_, self.config_file_2 = tempfile.mkstemp()
with open(self.config_file_2, 'w') as config_file:
config_file.write(
'[section1]\n'
'value1=11\n'
'value11=11\n'
'[section3]\n'
'value3=3\n'
)
def tearDown(self):
del self.config
os.unlink(self.config_file_1)
os.unlink(self.config_file_2)
def test(self):
self.config.from_ini(self.config_file_1)
self.assertEqual(self.config(), {'section1': {'value1': '1'}, 'section2': {'value2': '2'}})
self.assertEqual(self.config.section1(), {'value1': '1'})
self.assertEqual(self.config.section1.value1(), '1')
self.assertEqual(self.config.section2(), {'value2': '2'})
self.assertEqual(self.config.section2.value2(), '2')
def test_merge(self):
self.config.from_ini(self.config_file_1)
self.config.from_ini(self.config_file_2)
self.assertEqual(
self.config(),
{
'section1': {
'value1': '11',
'value11': '11',
},
'section2': {
'value2': '2',
},
'section3': {
'value3': '3',
},
},
)
self.assertEqual(self.config.section1(), {'value1': '11', 'value11': '11'})
self.assertEqual(self.config.section1.value1(), '11')
self.assertEqual(self.config.section1.value11(), '11')
self.assertEqual(self.config.section2(), {'value2': '2'})
self.assertEqual(self.config.section2.value2(), '2')
self.assertEqual(self.config.section3(), {'value3': '3'})
self.assertEqual(self.config.section3.value3(), '3')
class ConfigFromYamlTests(unittest.TestCase):
def setUp(self):
self.config = providers.Configuration(name='config')
_, self.config_file_1 = tempfile.mkstemp()
with open(self.config_file_1, 'w') as config_file:
config_file.write(
'section1:\n'
' value1: 1\n'
'\n'
'section2:\n'
' value2: 2\n'
)
_, self.config_file_2 = tempfile.mkstemp()
with open(self.config_file_2, 'w') as config_file:
config_file.write(
'section1:\n'
' value1: 11\n'
' value11: 11\n'
'section3:\n'
' value3: 3\n'
)
def tearDown(self):
del self.config
os.unlink(self.config_file_1)
os.unlink(self.config_file_2)
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test(self):
self.config.from_yaml(self.config_file_1)
self.assertEqual(self.config(), {'section1': {'value1': 1}, 'section2': {'value2': 2}})
self.assertEqual(self.config.section1(), {'value1': 1})
self.assertEqual(self.config.section1.value1(), 1)
self.assertEqual(self.config.section2(), {'value2': 2})
self.assertEqual(self.config.section2.value2(), 2)
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_merge(self):
self.config.from_yaml(self.config_file_1)
self.config.from_yaml(self.config_file_2)
self.assertEqual(
self.config(),
{
'section1': {
'value1': 11,
'value11': 11,
},
'section2': {
'value2': 2,
},
'section3': {
'value3': 3,
},
},
)
self.assertEqual(self.config.section1(), {'value1': 11, 'value11': 11})
self.assertEqual(self.config.section1.value1(), 11)
self.assertEqual(self.config.section1.value11(), 11)
self.assertEqual(self.config.section2(), {'value2': 2})
self.assertEqual(self.config.section2.value2(), 2)
self.assertEqual(self.config.section3(), {'value3': 3})
self.assertEqual(self.config.section3.value3(), 3)
def test_no_yaml_installed(self):
@contextlib.contextmanager
def no_yaml_module():
yaml = providers.yaml
providers.yaml = None
yield
providers.yaml = yaml
with no_yaml_module():
with self.assertRaises(errors.Error) as error:
self.config.from_yaml(self.config_file_1)
self.assertEqual(
error.exception.args[0],
'Unable to load yaml configuration - PyYAML is not installed. '
'Install PyYAML or install Dependency Injector with yaml extras: '
'"pip install dependency-injector[yaml]"',
)
class ConfigFromDict(unittest.TestCase):
def setUp(self):
self.config = providers.Configuration(name='config')
self.config_options_1 = {
'section1': {
'value1': '1',
},
'section2': {
'value2': '2',
},
}
self.config_options_2 = {
'section1': {
'value1': '11',
'value11': '11',
},
'section3': {
'value3': '3',
},
}
def test(self):
self.config.from_dict(self.config_options_1)
self.assertEqual(self.config(), {'section1': {'value1': '1'}, 'section2': {'value2': '2'}})
self.assertEqual(self.config.section1(), {'value1': '1'})
self.assertEqual(self.config.section1.value1(), '1')
self.assertEqual(self.config.section2(), {'value2': '2'})
self.assertEqual(self.config.section2.value2(), '2')
def test_merge(self):
self.config.from_dict(self.config_options_1)
self.config.from_dict(self.config_options_2)
self.assertEqual(
self.config(),
{
'section1': {
'value1': '11',
'value11': '11',
},
'section2': {
'value2': '2',
},
'section3': {
'value3': '3',
},
},
)
self.assertEqual(self.config.section1(), {'value1': '11', 'value11': '11'})
self.assertEqual(self.config.section1.value1(), '11')
self.assertEqual(self.config.section1.value11(), '11')
self.assertEqual(self.config.section2(), {'value2': '2'})
self.assertEqual(self.config.section2.value2(), '2')
self.assertEqual(self.config.section3(), {'value3': '3'})
self.assertEqual(self.config.section3.value3(), '3')
class ConfigFromEnvTests(unittest.TestCase):
def setUp(self):
self.config = providers.Configuration(name='config')
os.environ['CONFIG_TEST_ENV'] = 'test-value'
def tearDown(self):
del self.config
del os.environ['CONFIG_TEST_ENV']
def test(self):
self.config.from_env('CONFIG_TEST_ENV')
self.assertEqual(self.config(), 'test-value')
def test_default(self):
self.config.from_env('UNDEFINED_ENV', 'default-value')
self.assertEqual(self.config(), 'default-value')
def test_with_children(self):
self.config.section1.value1.from_env('CONFIG_TEST_ENV')
self.assertIsNone(self.config())
self.assertIsNone(self.config.section1())
self.assertEqual(self.config.section1.value1(), 'test-value')

View File

@ -5,6 +5,8 @@ envlist=
[testenv]
deps=
unittest2
extras=
yaml
commands=
unit2 discover -s tests/unit -p test_*_py3.py
@ -27,6 +29,9 @@ commands=
commands=
unit2 discover -s tests/unit -p test_*_py2_py3.py
[testenv:py34]
extras=
[testenv:pypy]
commands=
unit2 discover -s tests/unit -p test_*_py2_py3.py