Merge branch 'release/4.11.0' into master

This commit is contained in:
Roman Mogylatov 2021-01-27 07:50:18 -05:00
commit 78479c65e6
16 changed files with 15902 additions and 10605 deletions

126
.github/workflows/publishing.yml vendored Normal file
View File

@ -0,0 +1,126 @@
name: Publishing
on:
push:
tags:
- '*'
jobs:
tests:
name: Run tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.9
- run: pip install tox
- run: tox
env:
TOXENV: 3.9
linters:
name: Run linters
runs-on: ubuntu-latest
strategy:
matrix:
toxenv: [flake8, pydocstyle, mypy, pylint]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.9
- run: pip install tox
- run: tox
env:
TOXENV: ${{ matrix.toxenv }}
build-sdist:
name: Build source tarball
needs: [tests, linters]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.9
- run: python setup.py sdist
- uses: actions/upload-artifact@v2
with:
path: ./dist/*
build-wheels:
name: Build wheels
needs: [tests, linters]
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.9
- run: pip install cibuildwheel==1.8.0
- name: Install Visual C++ for Python 2.7 on Windows
if: runner.os == 'Windows'
run: |
choco install vcpython27 -f -y
- run: cibuildwheel --output-dir wheelhouse
- uses: actions/upload-artifact@v2
with:
path: ./wheelhouse/*.whl
build-wheels-linux-aarch64:
name: Build wheels (ubuntu-latest-aarch64)
needs: [tests, linters]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- uses: actions/setup-python@v2
with:
python-version: 3.9
- run: pip install cibuildwheel==1.8.0
- run: cibuildwheel --archs aarch64 --output-dir wheelhouse
- uses: actions/upload-artifact@v2
with:
path: ./wheelhouse/*.whl
publish:
name: Publish on PyPI
needs: [build-sdist, build-wheels, build-wheels-linux-aarch64]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v2
with:
name: artifact
path: dist
- uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
publish-docs:
name: Publish docs
needs: [publish]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.9
- run: pip install -r requirements-doc.txt
- run: pip install awscli
- run: pip install -e .
- run: (cd docs && make clean html)
- run: |
aws s3 sync docs/_build/html s3://python-dependency-injector-docs --delete
aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_CLOUDFRONT_DISTRIBUTION_ID }} --path "/*" > /dev/null
echo "Cache invalidation triggered"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}

55
.github/workflows/tests-and-linters.yml vendored Normal file
View File

@ -0,0 +1,55 @@
name: Tests and linters
on: [push, pull_request, workflow_dispatch]
jobs:
test-on-different-versions:
name: Run tests
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, pypy2, pypy3]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- run: pip install tox
- run: tox
env:
TOXENV: ${{ matrix.python-version }}
test-coverage:
name: Run tests with coverage
runs-on: ubuntu-latest
env:
DEPENDENCY_INJECTOR_DEBUG_MODE: 1
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.9
- run: pip install tox cython
- run: make cythonize
- run: tox
env:
TOXENV: coveralls
linters:
name: Run linters
runs-on: ubuntu-latest
strategy:
matrix:
toxenv: [flake8, pydocstyle, mypy, pylint]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.9
- run: pip install tox
- run: tox
env:
TOXENV: ${{ matrix.toxenv }}

View File

@ -1,132 +0,0 @@
os: linux
dist: xenial
language: python
jobs:
include:
- python: 3.9
env: TOXENV=coveralls DEPENDENCY_INJECTOR_DEBUG_MODE=1
install:
- pip install tox
- pip install cython
- make cythonize
script: tox
- python: 3.6
env: TOXENV=pylint
install: pip install tox
script: tox
- python: 3.6
env: TOXENV=flake8
install: pip install tox
script: tox
- python: 3.6
env: TOXENV=pydocstyle
install: pip install tox
script: tox
- python: 3.6
env: TOXENV=mypy
install: pip install tox
script: tox
- python: 2.7
env: TOXENV=py27
install: pip install tox
script: tox
- python: 3.4
env: TOXENV=py34
install: pip install tox
script: tox
- python: 3.5
env: TOXENV=py35
install: pip install tox
script: tox
- python: 3.6
env: TOXENV=py36
install: pip install tox
script: tox
- python: 3.7
env: TOXENV=py37
install: pip install tox
script: tox
- python: 3.8
env: TOXENV=py38
install: pip install tox
script: tox
- python: 3.9
env: TOXENV=py39
install: pip install tox
script: tox
- python: pypy
env: TOXENV=pypy
install: pip install tox
script: tox
- python: pypy3
env: TOXENV=pypy3
install: pip install tox
script: tox
- python: 3.8
if: tag IS present
env: TWINE_USERNAME=__token__
install: pip install pip --upgrade
script: python setup.py sdist
after_success:
- python3 -m pip install twine
- python3 -m twine upload dist/*
- services: docker
if: tag IS present
env: TWINE_USERNAME=__token__
install: python3 -m pip install cibuildwheel==1.6.3
script: python3 -m cibuildwheel --output-dir wheelhouse
after_success:
- python3 -m pip install --upgrade --upgrade-strategy eager twine
- python3 -m twine upload wheelhouse/*.whl
- services: docker
arch: arm64
if: tag IS present
env: TWINE_USERNAME=__token__
install: python3 -m pip install cibuildwheel==1.6.3
script: python3 -m cibuildwheel --output-dir wheelhouse
after_success:
- python3 -m pip install --upgrade --upgrade-strategy eager twine
- python3 -m twine upload wheelhouse/*.whl
- os: osx
if: tag IS present
language: shell
osx_image: xcode10.2
env: TWINE_USERNAME=__token__
install: python3 -m pip install cibuildwheel==1.6.3
script: python3 -m cibuildwheel --output-dir wheelhouse
after_success:
- python3 -m pip install --upgrade --upgrade-strategy eager twine
- python3 -m twine upload wheelhouse/*.whl
- os: windows
if: tag IS present
language: shell
env: TWINE_USERNAME=__token__
before_install:
- choco install python --version 3.8.6
- export PATH="/c/Python38:/c/Python38/Scripts:$PATH"
- ln -s /c/Python38/python.exe /c/Python38/python3.exe
install:
- python3 -m pip install certifi cibuildwheel==1.6.3
- export SSL_CERT_FILE=`python3 -c "import certifi;print(certifi.where())"`
- echo $SSL_CERT_FILE
script: python -m cibuildwheel --output-dir wheelhouse
after_success:
- python -m pip install --upgrade --upgrade-strategy eager twine
- python -m twine upload wheelhouse/*.whl
- python: 3.8
if: branch = master
install:
- pip install -r requirements-doc.txt
- pip install awscli
- pip install -e .
script: (cd docs && make clean html)
after_success:
- aws s3 sync docs/_build/html s3://python-dependency-injector-docs --delete
- aws cloudfront create-invalidation --distribution-id ${AWS_CLOUDFRONT_DISTRIBUTION_ID} --path "/*" > /dev/null
- echo "Cache invalidation triggered"
echo "Result: OK"
- python -m twine upload wheelhouse/*.whl
notifications:
slack:
rooms:
secure: CdWDgKnfYW7vvvoH3nS3yg3TcNZiYLRUyEp6ukQ4rQiiuR4+ltuvyGyFJWgP8r7VVJ9yHkB0jebCKWLUMsAEt1my33B6eMDEVefovpkdh2eJjGswmm80brt0EJULpgwPOtB1U47Mwca8L5jDW4KSv9RypUFRgn8eHDoWw6LKf5g=

View File

@ -35,8 +35,8 @@
:target: https://pypi.org/project/dependency-injector/ :target: https://pypi.org/project/dependency-injector/
:alt: Wheel :alt: Wheel
.. image:: https://api.travis-ci.com/ets-labs/python-dependency-injector.svg?branch=master .. image:: https://img.shields.io/github/workflow/status/ets-labs/python-dependency-injector/Tests%20and%20linters/master
:target: https://travis-ci.com/github/ets-labs/python-dependency-injector :target: https://github.com/ets-labs/python-dependency-injector/actions
:alt: Build Status :alt: Build Status
.. image:: https://coveralls.io/repos/github/ets-labs/python-dependency-injector/badge.svg?branch=master .. image:: https://coveralls.io/repos/github/ets-labs/python-dependency-injector/badge.svg?branch=master

View File

@ -50,8 +50,8 @@ Dependency Injector --- Dependency injection framework for Python
:target: https://pypi.org/project/dependency-injector/ :target: https://pypi.org/project/dependency-injector/
:alt: Wheel :alt: Wheel
.. image:: https://api.travis-ci.com/ets-labs/python-dependency-injector.svg?branch=master .. image:: https://img.shields.io/github/workflow/status/ets-labs/python-dependency-injector/Tests%20and%20linters/master
:target: https://travis-ci.com/github/ets-labs/python-dependency-injector :target: https://github.com/ets-labs/python-dependency-injector/actions
:alt: Build Status :alt: Build Status
.. image:: https://coveralls.io/repos/github/ets-labs/python-dependency-injector/badge.svg?branch=master .. image:: https://coveralls.io/repos/github/ets-labs/python-dependency-injector/badge.svg?branch=master

View File

@ -7,6 +7,27 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly From version 0.7.6 *Dependency Injector* framework strictly
follows `Semantic versioning`_ follows `Semantic versioning`_
4.11.0
------
- Add ``loader`` argument to the configuration provider ``Configuration.from_yaml(..., loader=...)``
to override the default YAML loader.
Many thanks to `Stefano Frazzetto <https://github.com/StefanoFrazzetto>`_ for suggesting an improvement.
- Make security improvement: change default YAML loader to the custom ``yaml.SafeLoader`` with a support
of environment variables interpolation.
Many thanks to `Stefano Frazzetto <https://github.com/StefanoFrazzetto>`_ for suggesting an improvement.
- Update configuration provider ``.from_*()`` methods to raise an exception in strict mode if
configuration file does not exist or configuration data is undefined.
Many thanks to `Stefano Frazzetto <https://github.com/StefanoFrazzetto>`_ for suggesting an improvement.
- Add ``required`` argument to the configuration provider ``.from_*()`` methods to specify
mandatory configuration sources.
Many thanks to `Stefano Frazzetto <https://github.com/StefanoFrazzetto>`_ for suggesting an improvement.
- Fix a bug with asynchronous injections: async providers do not work with async dependencies.
See issue: `#368 <https://github.com/ets-labs/python-dependency-injector/issues/368>`_.
Thanks `@kolypto <https://github.com/kolypto>`_ for the bug report.
- Refactor asynchronous injections.
- Add extra tests for asynchronous injections.
- Migrate CI to Github Actions.
4.10.3 4.10.3
------ ------
- Fix a bug in the ``Configuration`` provider: strict mode didn't work when provider - Fix a bug in the ``Configuration`` provider: strict mode didn't work when provider

View File

@ -21,6 +21,10 @@ Configuration provider
It implements the principle "use first, define later". It implements the principle "use first, define later".
.. contents::
:local:
:backlinks: none
Loading from an INI file Loading from an INI file
------------------------ ------------------------
@ -57,9 +61,19 @@ where ``examples/providers/configuration/config.yml`` is:
.. literalinclude:: ../../examples/providers/configuration/config.yml .. literalinclude:: ../../examples/providers/configuration/config.yml
:language: ini :language: ini
:py:meth:`Configuration.from_yaml` method supports environment variables interpolation. Use :py:meth:`Configuration.from_yaml` method uses custom version of ``yaml.SafeLoader``.
``${ENV_NAME}`` format in the configuration file to substitute value of the environment
variable ``ENV_NAME``. The loader supports environment variables interpolation. Use ``${ENV_NAME}`` format
in the configuration file to substitute value of the environment variable ``ENV_NAME``.
You can also specify a YAML loader as an argument:
.. code-block:: python
import yaml
container.config.from_yaml('config.yml', loader=yaml.UnsafeLoader)
.. note:: .. note::
@ -113,6 +127,43 @@ where ``examples/providers/configuration/config.local.yml`` is:
.. literalinclude:: ../../examples/providers/configuration/config.local.yml .. literalinclude:: ../../examples/providers/configuration/config.local.yml
:language: ini :language: ini
Mandatory and optional sources
------------------------------
By default, methods ``.from_yaml()`` and ``.from_ini()`` ignore errors if configuration file does not exist.
You can use this to specify optional configuration files.
If configuration file is mandatory, use ``required`` argument. Configuration provider will raise an error
if required file does not exist.
You can also use ``required`` argument when loading configuration from dictionaries and environment variables.
Mandatory YAML file:
.. code-block:: python
container.config.from_yaml('config.yaml', required=True)
Mandatory INI file:
.. code-block:: python
container.config.from_ini('config.ini', required=True)
Mandatory dictionary:
.. code-block:: python
container.config.from_dict(config_dict, required=True)
Mandatory environment variable:
.. code-block:: python
container.config.api_key.from_env('API_KEY', required=True)
See also: :ref:`configuration-strict-mode`.
Specifying the value type Specifying the value type
------------------------- -------------------------
@ -143,6 +194,8 @@ With the ``.as_(callback, *args, **kwargs)`` you can specify a function that wil
before the injection. The value from the config will be passed as a first argument. The returned before the injection. The value from the config will be passed as a first argument. The returned
value will be injected. Parameters ``*args`` and ``**kwargs`` are handled as any other injections. value will be injected. Parameters ``*args`` and ``**kwargs`` are handled as any other injections.
.. _configuration-strict-mode:
Strict mode and required options Strict mode and required options
-------------------------------- --------------------------------
@ -154,7 +207,57 @@ on access to any undefined option.
:lines: 3- :lines: 3-
:emphasize-lines: 12 :emphasize-lines: 12
You can also use ``.required()`` option modifier when making an injection. Methods ``.from_*()`` in strict mode raise an exception if configuration file does not exist or
configuration data is undefined:
.. code-block:: python
:emphasize-lines: 10,15,20,25
class Container(containers.DeclarativeContainer):
config = providers.Configuration(strict=True)
if __name__ == '__main__':
container = Container()
try:
container.config.from_yaml('does-not_exist.yml') # raise exception
except FileNotFoundError:
...
try:
container.config.from_ini('does-not_exist.ini') # raise exception
except FileNotFoundError:
...
try:
container.config.from_env('UNDEFINED_ENV_VAR') # raise exception
except ValueError:
...
try:
container.config.from_dict({}) # raise exception
except ValueError:
...
You can override ``.from_*()`` methods behaviour in strict mode using ``required`` argument:
.. code-block:: python
class Container(containers.DeclarativeContainer):
config = providers.Configuration(strict=True)
if __name__ == '__main__':
container = Container()
container.config.from_yaml('config.yml')
container.config.from_yaml('config.local.yml', required=False)
You can also use ``.required()`` option modifier when making an injection. It does not require to switch
configuration provider to strict mode.
.. literalinclude:: ../../examples/providers/configuration/configuration_required.py .. literalinclude:: ../../examples/providers/configuration/configuration_required.py
:language: python :language: python

View File

@ -1,6 +1,6 @@
"""Top-level package.""" """Top-level package."""
__version__ = '4.10.3' __version__ = '4.11.0'
"""Version number. """Version number.
:type: str :type: str

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ try:
except ImportError: except ImportError:
asyncio = None asyncio = None
import functools
import inspect import inspect
cimport cython cimport cython
@ -419,18 +420,27 @@ cdef inline object __provide_keyword_args(
cdef inline object __awaitable_args_kwargs_future(object args, list awaitables): cdef inline object __awaitable_args_kwargs_future(object args, list awaitables):
future_result = asyncio.Future() future_result = asyncio.Future()
args_future = asyncio.Future() args_ready = asyncio.gather(*[value for _, value in awaitables])
args_future.set_result((future_result, args, awaitables)) args_ready.add_done_callback(
functools.partial(
args_ready = asyncio.gather(args_future, *[value for _, value in awaitables]) __async_prepare_args_kwargs_callback,
args_ready.add_done_callback(__async_prepare_args_kwargs_callback) future_result,
args,
awaitables,
),
)
asyncio.ensure_future(args_ready) asyncio.ensure_future(args_ready)
return future_result return future_result
cdef inline void __async_prepare_args_kwargs_callback(object future): cdef inline void __async_prepare_args_kwargs_callback(
(future_result, args, awaitables), *awaited = future.result() object future_result,
object args,
object awaitables,
object future,
):
awaited = future.result()
for value, (key, _) in zip(awaited, awaitables): for value, (key, _) in zip(awaited, awaitables):
args[key] = value args[key] = value
future_result.set_result(args) future_result.set_result(args)
@ -460,17 +470,20 @@ cdef inline object __provide_attributes(tuple attributes, int attributes_len):
cdef inline object __async_inject_attributes(future_instance, future_attributes): cdef inline object __async_inject_attributes(future_instance, future_attributes):
future_result = asyncio.Future() future_result = asyncio.Future()
future = asyncio.Future() attributes_ready = asyncio.gather(future_instance, future_attributes)
future.set_result(future_result) attributes_ready.add_done_callback(
functools.partial(
attributes_ready = asyncio.gather(future, future_instance, future_attributes) __async_inject_attributes_callback,
attributes_ready.add_done_callback(__async_inject_attributes_callback) future_result,
),
)
asyncio.ensure_future(attributes_ready) asyncio.ensure_future(attributes_ready)
return future_result return future_result
cdef inline void __async_inject_attributes_callback(future):
future_result, instance, attributes = future.result() cdef inline void __async_inject_attributes_callback(object future_result, object future):
instance, attributes = future.result()
__inject_attributes(instance, attributes) __inject_attributes(instance, attributes)
future_result.set_result(instance) future_result.set_result(instance)
@ -516,11 +529,14 @@ cdef inline object __call(
future_result = asyncio.Future() future_result = asyncio.Future()
future = asyncio.Future() args_kwargs_ready = asyncio.gather(args, kwargs)
future.set_result((future_result, call)) args_kwargs_ready.add_done_callback(
functools.partial(
args_kwargs_ready = asyncio.gather(future, args, kwargs) __async_call_callback,
args_kwargs_ready.add_done_callback(__async_call_callback) future_result,
call,
),
)
asyncio.ensure_future(args_kwargs_ready) asyncio.ensure_future(args_kwargs_ready)
return future_result return future_result
@ -528,12 +544,22 @@ cdef inline object __call(
return call(*args, **kwargs) return call(*args, **kwargs)
cdef inline void __async_call_callback(object future): cdef inline void __async_call_callback(object future_result, object call, object future):
(future_result, call), args, kwargs = future.result() args, kwargs = future.result()
result = call(*args, **kwargs) result = call(*args, **kwargs)
if __isawaitable(result):
result = asyncio.ensure_future(result)
result.add_done_callback(functools.partial(__async_result_callback, future_result))
return
future_result.set_result(result) future_result.set_result(result)
cdef inline object __async_result_callback(object future_result, object future):
future_result.set_result(future.result())
cdef inline object __callable_call(Callable self, tuple args, dict kwargs): cdef inline object __callable_call(Callable self, tuple args, dict kwargs):
return __call( return __call(
self.__provides, self.__provides,

View File

@ -20,6 +20,11 @@ from typing import (
overload, overload,
) )
try:
import yaml
except ImportError:
yaml = None
from . import resources from . import resources
@ -149,10 +154,10 @@ class ConfigurationOption(Provider[Any]):
def required(self) -> ConfigurationOption: ... def required(self) -> ConfigurationOption: ...
def is_required(self) -> bool: ... def is_required(self) -> bool: ...
def update(self, value: Any) -> None: ... def update(self, value: Any) -> None: ...
def from_ini(self, filepath: Union[Path, str]) -> None: ... def from_ini(self, filepath: Union[Path, str], required: bool = False) -> None: ...
def from_yaml(self, filepath: Union[Path, str]) -> None: ... def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any]=None) -> None: ...
def from_dict(self, options: _Dict[str, Any]) -> None: ... def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ...
def from_env(self, name: str, default: Optional[Any] = None) -> None: ... def from_env(self, name: str, default: Optional[Any] = None, required: bool = False) -> None: ...
class TypedConfigurationOption(Callable[T]): class TypedConfigurationOption(Callable[T]):
@ -170,10 +175,10 @@ class Configuration(Object[Any]):
def set(self, selector: str, value: Any) -> OverridingContext: ... def set(self, selector: str, value: Any) -> OverridingContext: ...
def reset_cache(self) -> None: ... def reset_cache(self) -> None: ...
def update(self, value: Any) -> None: ... def update(self, value: Any) -> None: ...
def from_ini(self, filepath: Union[Path, str]) -> None: ... def from_ini(self, filepath: Union[Path, str], required: bool = False) -> None: ...
def from_yaml(self, filepath: Union[Path, str]) -> None: ... def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any]=None) -> None: ...
def from_dict(self, options: _Dict[str, Any]) -> None: ... def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ...
def from_env(self, name: str, default: Optional[Any] = None) -> None: ... def from_env(self, name: str, default: Optional[Any] = None, required: bool = False) -> None: ...
class Factory(Provider[T]): class Factory(Provider[T]):
@ -373,3 +378,9 @@ def deepcopy(instance: Any, memo: Optional[_Dict[Any, Any]] = None): Any: ...
def merge_dicts(dict1: _Dict[Any, Any], dict2: _Dict[Any, Any]) -> _Dict[Any, Any]: ... def merge_dicts(dict1: _Dict[Any, Any], dict2: _Dict[Any, Any]) -> _Dict[Any, Any]: ...
if yaml:
class YamlLoader(yaml.SafeLoader): ...
else:
class YamlLoader: ...

View File

@ -3,6 +3,7 @@
from __future__ import absolute_import from __future__ import absolute_import
import copy import copy
import errno
import functools import functools
import inspect import inspect
import os import os
@ -53,14 +54,6 @@ else: # pragma: no cover
copy.deepcopy(obj.im_self, memo), copy.deepcopy(obj.im_self, memo),
obj.im_class) obj.im_class)
if yaml:
yaml_env_marker_pattern = re.compile(r'\$\{([^}^{]+)\}')
def yaml_env_marker_constructor(_, node):
""""Replace environment variable marker with its value."""
return os.path.expandvars(node.value)
yaml.add_implicit_resolver('!path', yaml_env_marker_pattern)
yaml.add_constructor('!path', yaml_env_marker_constructor)
if sys.version_info[0] == 3: if sys.version_info[0] == 3:
class EnvInterpolation(iniconfigparser.BasicInterpolation): class EnvInterpolation(iniconfigparser.BasicInterpolation):
@ -72,23 +65,48 @@ if sys.version_info[0] == 3:
def _parse_ini_file(filepath): def _parse_ini_file(filepath):
parser = iniconfigparser.ConfigParser(interpolation=EnvInterpolation()) parser = iniconfigparser.ConfigParser(interpolation=EnvInterpolation())
parser.read(filepath) with open(filepath) as config_file:
parser.read_file(config_file)
return parser return parser
else: else:
import StringIO import StringIO
def _parse_ini_file(filepath): def _parse_ini_file(filepath):
parser = iniconfigparser.ConfigParser() parser = iniconfigparser.ConfigParser()
try: with open(filepath) as config_file:
with open(filepath) as config_file: config_string = os.path.expandvars(config_file.read())
config_string = os.path.expandvars(config_file.read()) parser.readfp(StringIO.StringIO(config_string))
except IOError: return parser
return parser
else:
parser.readfp(StringIO.StringIO(config_string))
return parser
if yaml:
# TODO: use SafeLoader without env interpolation by default in version 5.*
yaml_env_marker_pattern = re.compile(r'\$\{([^}^{]+)\}')
def yaml_env_marker_constructor(_, node):
""""Replace environment variable marker with its value."""
return os.path.expandvars(node.value)
yaml.add_implicit_resolver('!path', yaml_env_marker_pattern)
yaml.add_constructor('!path', yaml_env_marker_constructor)
class YamlLoader(yaml.SafeLoader):
"""Custom YAML loader.
Inherits ``yaml.SafeLoader`` and add environment variables interpolation.
"""
YamlLoader.add_implicit_resolver('!path', yaml_env_marker_pattern, None)
YamlLoader.add_constructor('!path', yaml_env_marker_constructor)
else:
class YamlLoader:
"""Custom YAML loader.
Inherits ``yaml.SafeLoader`` and add environment variables interpolation.
"""
UNDEFINED = object()
cdef int ASYNC_MODE_UNDEFINED = 0 cdef int ASYNC_MODE_UNDEFINED = 0
cdef int ASYNC_MODE_ENABLED = 1 cdef int ASYNC_MODE_ENABLED = 1
cdef int ASYNC_MODE_DISABLED = 2 cdef int ASYNC_MODE_DISABLED = 2
@ -1170,14 +1188,12 @@ cdef class ConfigurationOption(Provider):
:py:class:`Configuration` provider. :py:class:`Configuration` provider.
""" """
UNDEFINED = object()
def __init__(self, name, root, required=False): def __init__(self, name, root, required=False):
self.__name = name self.__name = name
self.__root_ref = weakref.ref(root) self.__root_ref = weakref.ref(root)
self.__children = {} self.__children = {}
self.__required = required self.__required = required
self.__cache = self.UNDEFINED self.__cache = UNDEFINED
super().__init__() super().__init__()
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
@ -1226,7 +1242,7 @@ cdef class ConfigurationOption(Provider):
cpdef object _provide(self, tuple args, dict kwargs): cpdef object _provide(self, tuple args, dict kwargs):
"""Return new instance.""" """Return new instance."""
if self.__cache is not self.UNDEFINED: if self.__cache is not UNDEFINED:
return self.__cache return self.__cache
root = self.__root_ref() root = self.__root_ref()
@ -1278,7 +1294,7 @@ cdef class ConfigurationOption(Provider):
raise Error('Configuration option does not support this method') raise Error('Configuration option does not support this method')
def reset_cache(self): def reset_cache(self):
self.__cache = self.UNDEFINED self.__cache = UNDEFINED
for child in self.__children.values(): for child in self.__children.values():
child.reset_cache() child.reset_cache()
@ -1296,7 +1312,7 @@ cdef class ConfigurationOption(Provider):
""" """
self.override(value) self.override(value)
def from_ini(self, filepath): def from_ini(self, filepath, required=UNDEFINED):
"""Load configuration from the ini file. """Load configuration from the ini file.
Loaded configuration is merged recursively over existing configuration. Loaded configuration is merged recursively over existing configuration.
@ -1304,9 +1320,20 @@ cdef class ConfigurationOption(Provider):
:param filepath: Path to the configuration file. :param filepath: Path to the configuration file.
:type filepath: str :type filepath: str
:param required: When required is True, raise an exception if file does not exist.
:type required: bool
:rtype: None :rtype: None
""" """
parser = _parse_ini_file(filepath) try:
parser = _parse_ini_file(filepath)
except IOError as exception:
if required is not False \
and (self._is_strict_mode_enabled() or required is True) \
and exception.errno in (errno.ENOENT, errno.EISDIR):
exception.strerror = 'Unable to load configuration file {0}'.format(exception.strerror)
raise
return
config = {} config = {}
for section in parser.sections(): for section in parser.sections():
@ -1317,7 +1344,7 @@ cdef class ConfigurationOption(Provider):
current_config = {} current_config = {}
self.override(merge_dicts(current_config, config)) self.override(merge_dicts(current_config, config))
def from_yaml(self, filepath): def from_yaml(self, filepath, required=UNDEFINED, loader=None):
"""Load configuration from the yaml file. """Load configuration from the yaml file.
Loaded configuration is merged recursively over existing configuration. Loaded configuration is merged recursively over existing configuration.
@ -1325,6 +1352,12 @@ cdef class ConfigurationOption(Provider):
:param filepath: Path to the configuration file. :param filepath: Path to the configuration file.
:type filepath: str :type filepath: str
:param required: When required is True, raise an exception if file does not exist.
:type required: bool
:param loader: YAML loader, :py:class:`YamlLoader` is used if not specified.
:type loader: ``yaml.Loader``
:rtype: None :rtype: None
""" """
if yaml is None: if yaml is None:
@ -1334,10 +1367,19 @@ cdef class ConfigurationOption(Provider):
'"pip install dependency-injector[yaml]"' '"pip install dependency-injector[yaml]"'
) )
if loader is None:
loader = YamlLoader
try: try:
with open(filepath) as opened_file: with open(filepath) as opened_file:
config = yaml.load(opened_file, yaml.Loader) config = yaml.load(opened_file, loader)
except IOError: except IOError as exception:
if required is not False \
and (self._is_strict_mode_enabled() or required is True) \
and exception.errno in (errno.ENOENT, errno.EISDIR):
exception.strerror = 'Unable to load configuration file {0}'.format(exception.strerror)
raise
return return
current_config = self.__call__() current_config = self.__call__()
@ -1345,7 +1387,7 @@ cdef class ConfigurationOption(Provider):
current_config = {} current_config = {}
self.override(merge_dicts(current_config, config)) self.override(merge_dicts(current_config, config))
def from_dict(self, options): def from_dict(self, options, required=UNDEFINED):
"""Load configuration from the dictionary. """Load configuration from the dictionary.
Loaded configuration is merged recursively over existing configuration. Loaded configuration is merged recursively over existing configuration.
@ -1353,27 +1395,59 @@ cdef class ConfigurationOption(Provider):
:param options: Configuration options. :param options: Configuration options.
:type options: dict :type options: dict
:param required: When required is True, raise an exception if dictionary is empty.
:type required: bool
:rtype: None :rtype: None
""" """
current_config = self.__call__() if required is not False \
if not current_config: and (self._is_strict_mode_enabled() or required is True) \
and not options:
raise ValueError('Can not use empty dictionary')
try:
current_config = self.__call__()
except Error:
current_config = {} current_config = {}
else:
if not current_config:
current_config = {}
self.override(merge_dicts(current_config, options)) self.override(merge_dicts(current_config, options))
def from_env(self, name, default=None): def from_env(self, name, default=UNDEFINED, required=UNDEFINED):
"""Load configuration value from the environment variable. """Load configuration value from the environment variable.
:param name: Name of the environment variable. :param name: Name of the environment variable.
:type name: str :type name: str
:param default: Default value that is used if environment variable does not exist. :param default: Default value that is used if environment variable does not exist.
:type default: str :type default: object
:param required: When required is True, raise an exception if environment variable is undefined.
:type required: bool
:rtype: None :rtype: None
""" """
value = os.getenv(name, default) value = os.environ.get(name, default)
if value is UNDEFINED:
if required is not False \
and (self._is_strict_mode_enabled() or required is True):
raise ValueError('Environment variable "{0}" is undefined'.format(name))
value = None
self.override(value) self.override(value)
def _is_strict_mode_enabled(self):
cdef Configuration root
root = self.__root_ref()
if not root:
return False
return root.__strict
cdef class TypedConfigurationOption(Callable): cdef class TypedConfigurationOption(Callable):
@ -1403,7 +1477,6 @@ cdef class Configuration(Object):
""" """
DEFAULT_NAME = 'config' DEFAULT_NAME = 'config'
UNDEFINED = object()
def __init__(self, name=DEFAULT_NAME, default=None, strict=False): def __init__(self, name=DEFAULT_NAME, default=None, strict=False):
self.__name = name self.__name = name
@ -1474,17 +1547,17 @@ cdef class Configuration(Object):
value = self.__call__() value = self.__call__()
if value is None: if value is None:
if self.__strict or required: if self._is_strict_mode_enabled() or required:
raise Error('Undefined configuration option "{0}.{1}"'.format(self.__name, selector)) raise Error('Undefined configuration option "{0}.{1}"'.format(self.__name, selector))
return None return None
keys = selector.split('.') keys = selector.split('.')
while len(keys) > 0: while len(keys) > 0:
key = keys.pop(0) key = keys.pop(0)
value = value.get(key, self.UNDEFINED) value = value.get(key, UNDEFINED)
if value is self.UNDEFINED: if value is UNDEFINED:
if self.__strict or required: if self._is_strict_mode_enabled() or required:
raise Error('Undefined configuration option "{0}.{1}"'.format(self.__name, selector)) raise Error('Undefined configuration option "{0}.{1}"'.format(self.__name, selector))
return None return None
@ -1572,7 +1645,7 @@ cdef class Configuration(Object):
""" """
self.override(value) self.override(value)
def from_ini(self, filepath): def from_ini(self, filepath, required=UNDEFINED):
"""Load configuration from the ini file. """Load configuration from the ini file.
Loaded configuration is merged recursively over existing configuration. Loaded configuration is merged recursively over existing configuration.
@ -1580,9 +1653,20 @@ cdef class Configuration(Object):
:param filepath: Path to the configuration file. :param filepath: Path to the configuration file.
:type filepath: str :type filepath: str
:param required: When required is True, raise an exception if file does not exist.
:type required: bool
:rtype: None :rtype: None
""" """
parser = _parse_ini_file(filepath) try:
parser = _parse_ini_file(filepath)
except IOError as exception:
if required is not False \
and (self._is_strict_mode_enabled() or required is True) \
and exception.errno in (errno.ENOENT, errno.EISDIR):
exception.strerror = 'Unable to load configuration file {0}'.format(exception.strerror)
raise
return
config = {} config = {}
for section in parser.sections(): for section in parser.sections():
@ -1593,7 +1677,7 @@ cdef class Configuration(Object):
current_config = {} current_config = {}
self.override(merge_dicts(current_config, config)) self.override(merge_dicts(current_config, config))
def from_yaml(self, filepath): def from_yaml(self, filepath, required=UNDEFINED, loader=None):
"""Load configuration from the yaml file. """Load configuration from the yaml file.
Loaded configuration is merged recursively over existing configuration. Loaded configuration is merged recursively over existing configuration.
@ -1601,6 +1685,12 @@ cdef class Configuration(Object):
:param filepath: Path to the configuration file. :param filepath: Path to the configuration file.
:type filepath: str :type filepath: str
:param required: When required is True, raise an exception if file does not exist.
:type required: bool
:param loader: YAML loader, :py:class:`YamlLoader` is used if not specified.
:type loader: ``yaml.Loader``
:rtype: None :rtype: None
""" """
if yaml is None: if yaml is None:
@ -1610,10 +1700,18 @@ cdef class Configuration(Object):
'"pip install dependency-injector[yaml]"' '"pip install dependency-injector[yaml]"'
) )
if loader is None:
loader = YamlLoader
try: try:
with open(filepath) as opened_file: with open(filepath) as opened_file:
config = yaml.load(opened_file, yaml.Loader) config = yaml.load(opened_file, loader)
except IOError: except IOError as exception:
if required is not False \
and (self._is_strict_mode_enabled() or required is True) \
and exception.errno in (errno.ENOENT, errno.EISDIR):
exception.strerror = 'Unable to load configuration file {0}'.format(exception.strerror)
raise
return return
current_config = self.__call__() current_config = self.__call__()
@ -1621,7 +1719,7 @@ cdef class Configuration(Object):
current_config = {} current_config = {}
self.override(merge_dicts(current_config, config)) self.override(merge_dicts(current_config, config))
def from_dict(self, options): def from_dict(self, options, required=UNDEFINED):
"""Load configuration from the dictionary. """Load configuration from the dictionary.
Loaded configuration is merged recursively over existing configuration. Loaded configuration is merged recursively over existing configuration.
@ -1629,27 +1727,48 @@ cdef class Configuration(Object):
:param options: Configuration options. :param options: Configuration options.
:type options: dict :type options: dict
:param required: When required is True, raise an exception if dictionary is empty.
:type required: bool
:rtype: None :rtype: None
""" """
if required is not False \
and (self._is_strict_mode_enabled() or required is True) \
and not options:
raise ValueError('Can not use empty dictionary')
current_config = self.__call__() current_config = self.__call__()
if not current_config: if not current_config:
current_config = {} current_config = {}
self.override(merge_dicts(current_config, options)) self.override(merge_dicts(current_config, options))
def from_env(self, name, default=None): def from_env(self, name, default=UNDEFINED, required=UNDEFINED):
"""Load configuration value from the environment variable. """Load configuration value from the environment variable.
:param name: Name of the environment variable. :param name: Name of the environment variable.
:type name: str :type name: str
:param default: Default value that is used if environment variable does not exist. :param default: Default value that is used if environment variable does not exist.
:type default: str :type default: object
:param required: When required is True, raise an exception if environment variable is undefined.
:type required: bool
:rtype: None :rtype: None
""" """
value = os.getenv(name, default) value = os.environ.get(name, default)
if value is UNDEFINED:
if required is not False \
and (self._is_strict_mode_enabled() or required is True):
raise ValueError('Environment variable "{0}" is undefined'.format(name))
value = None
self.override(value) self.override(value)
def _is_strict_mode_enabled(self):
return self.__strict
cdef class Factory(Provider): cdef class Factory(Provider):
r"""Factory provider creates new instance on every call. r"""Factory provider creates new instance on every call.

View File

@ -236,6 +236,51 @@ class FactoryTests(AsyncTestCase):
self.assertIsNot(service1.client, service2.client) self.assertIsNot(service1.client, service2.client)
def test_async_instance_and_sync_attributes_injection(self):
class ContainerWithAttributes(containers.DeclarativeContainer):
resource1 = providers.Resource(init_resource, providers.Object(RESOURCE1))
client = providers.Factory(
Client,
resource1,
resource2=None,
)
client.add_attributes(resource2=providers.Object(RESOURCE2))
service = providers.Factory(
Service,
client=None,
)
service.add_attributes(client=client)
container = ContainerWithAttributes()
client1 = self._run(container.client())
client2 = self._run(container.client())
self.assertIsInstance(client1, Client)
self.assertIs(client1.resource1, RESOURCE1)
self.assertIs(client1.resource2, RESOURCE2)
self.assertIsInstance(client2, Client)
self.assertIs(client2.resource1, RESOURCE1)
self.assertIs(client2.resource2, RESOURCE2)
service1 = self._run(container.service())
service2 = self._run(container.service())
self.assertIsInstance(service1, Service)
self.assertIsInstance(service1.client, Client)
self.assertIs(service1.client.resource1, RESOURCE1)
self.assertIs(service1.client.resource2, RESOURCE2)
self.assertIsInstance(service2, Service)
self.assertIsInstance(service2.client, Client)
self.assertIs(service2.client.resource1, RESOURCE1)
self.assertIs(service2.client.resource2, RESOURCE2)
self.assertIsNot(service1.client, service2.client)
class FactoryAggregateTests(AsyncTestCase): class FactoryAggregateTests(AsyncTestCase):
@ -816,3 +861,24 @@ class AsyncTypingStubTests(AsyncTestCase):
self.assertIs(service2.client.resource2, RESOURCE2) self.assertIs(service2.client.resource2, RESOURCE2)
self.assertIsNot(service1.client, service2.client) self.assertIsNot(service1.client, service2.client)
class AsyncProvidersWithAsyncDependenciesTests(AsyncTestCase):
def test_injections(self):
# See: https://github.com/ets-labs/python-dependency-injector/issues/368
async def async_db_provider():
return {'db': 'ok'}
async def async_service(db=None):
return {'service': 'ok', 'db': db}
class Container(containers.DeclarativeContainer):
db = providers.Factory(async_db_provider)
service = providers.Singleton(async_service, db=db)
container = Container()
service = self._run(container.service())
self.assertEquals(service, {'service': 'ok', 'db': {'db': 'ok'}})

View File

@ -9,6 +9,10 @@ import tempfile
import unittest2 as unittest import unittest2 as unittest
from dependency_injector import containers, providers, errors from dependency_injector import containers, providers, errors
try:
import yaml
except ImportError:
yaml = None
class ConfigTests(unittest.TestCase): class ConfigTests(unittest.TestCase):
@ -395,6 +399,16 @@ class ConfigFromIniTests(unittest.TestCase):
self.assertEqual(self.config.section2(), {'value2': '2'}) self.assertEqual(self.config.section2(), {'value2': '2'})
self.assertEqual(self.config.section2.value2(), '2') self.assertEqual(self.config.section2.value2(), '2')
def test_option(self):
self.config.option.from_ini(self.config_file_1)
self.assertEqual(self.config(), {'option': {'section1': {'value1': '1'}, 'section2': {'value2': '2'}}})
self.assertEqual(self.config.option(), {'section1': {'value1': '1'}, 'section2': {'value2': '2'}})
self.assertEqual(self.config.option.section1(), {'value1': '1'})
self.assertEqual(self.config.option.section1.value1(), '1')
self.assertEqual(self.config.option.section2(), {'value2': '2'})
self.assertEqual(self.config.option.section2.value2(), '2')
def test_merge(self): def test_merge(self):
self.config.from_ini(self.config_file_1) self.config.from_ini(self.config_file_1)
self.config.from_ini(self.config_file_2) self.config.from_ini(self.config_file_2)
@ -422,6 +436,43 @@ class ConfigFromIniTests(unittest.TestCase):
self.assertEqual(self.config.section3(), {'value3': '3'}) self.assertEqual(self.config.section3(), {'value3': '3'})
self.assertEqual(self.config.section3.value3(), '3') self.assertEqual(self.config.section3.value3(), '3')
def test_file_does_not_exist(self):
self.config.from_ini('./does_not_exist.ini')
self.assertEqual(self.config(), {})
def test_file_does_not_exist_strict_mode(self):
self.config = providers.Configuration(strict=True)
with self.assertRaises(IOError):
self.config.from_ini('./does_not_exist.ini')
def test_option_file_does_not_exist(self):
self.config.option.from_ini('does_not_exist.ini')
self.assertIsNone(self.config.option.undefined())
def test_option_file_does_not_exist_strict_mode(self):
self.config = providers.Configuration(strict=True)
with self.assertRaises(IOError):
self.config.option.from_ini('./does_not_exist.ini')
def test_required_file_does_not_exist(self):
with self.assertRaises(IOError):
self.config.from_ini('./does_not_exist.ini', required=True)
def test_required_option_file_does_not_exist(self):
with self.assertRaises(IOError):
self.config.option.from_ini('./does_not_exist.ini', required=True)
def test_not_required_file_does_not_exist_strict_mode(self):
self.config = providers.Configuration(strict=True)
self.config.from_ini('./does_not_exist.ini', required=False)
self.assertEqual(self.config(), {})
def test_not_required_option_file_does_not_exist_strict_mode(self):
self.config = providers.Configuration(strict=True)
self.config.option.from_ini('./does_not_exist.ini', required=False)
with self.assertRaises(errors.Error):
self.config.option()
class ConfigFromIniWithEnvInterpolationTests(unittest.TestCase): class ConfigFromIniWithEnvInterpolationTests(unittest.TestCase):
@ -525,6 +576,51 @@ class ConfigFromYamlTests(unittest.TestCase):
self.assertEqual(self.config.section3(), {'value3': 3}) self.assertEqual(self.config.section3(), {'value3': 3})
self.assertEqual(self.config.section3.value3(), 3) self.assertEqual(self.config.section3.value3(), 3)
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_file_does_not_exist(self):
self.config.from_yaml('./does_not_exist.yml')
self.assertEqual(self.config(), {})
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_file_does_not_exist_strict_mode(self):
self.config = providers.Configuration(strict=True)
with self.assertRaises(IOError):
self.config.from_yaml('./does_not_exist.yml')
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_option_file_does_not_exist(self):
self.config.option.from_yaml('./does_not_exist.yml')
self.assertIsNone(self.config.option())
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_option_file_does_not_exist_strict_mode(self):
self.config = providers.Configuration(strict=True)
with self.assertRaises(IOError):
self.config.option.from_yaml('./does_not_exist.yml')
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_required_file_does_not_exist(self):
with self.assertRaises(IOError):
self.config.from_yaml('./does_not_exist.yml', required=True)
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_required_option_file_does_not_exist(self):
with self.assertRaises(IOError):
self.config.option.from_yaml('./does_not_exist.yml', required=True)
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_not_required_file_does_not_exist_strict_mode(self):
self.config = providers.Configuration(strict=True)
self.config.from_yaml('./does_not_exist.yml', required=False)
self.assertEqual(self.config(), {})
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_not_required_option_file_does_not_exist_strict_mode(self):
self.config = providers.Configuration(strict=True)
self.config.option.from_yaml('./does_not_exist.yml', required=False)
with self.assertRaises(errors.Error):
self.config.option()
def test_no_yaml_installed(self): def test_no_yaml_installed(self):
@contextlib.contextmanager @contextlib.contextmanager
def no_yaml_module(): def no_yaml_module():
@ -581,6 +677,51 @@ class ConfigFromYamlWithEnvInterpolationTests(unittest.TestCase):
self.assertEqual(self.config.section1(), {'value1': 'test-value'}) self.assertEqual(self.config.section1(), {'value1': 'test-value'})
self.assertEqual(self.config.section1.value1(), 'test-value') self.assertEqual(self.config.section1.value1(), 'test-value')
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_option_env_variable_interpolation(self):
self.config.option.from_yaml(self.config_file)
self.assertEqual(
self.config.option(),
{
'section1': {
'value1': 'test-value',
},
},
)
self.assertEqual(self.config.option.section1(), {'value1': 'test-value'})
self.assertEqual(self.config.option.section1.value1(), 'test-value')
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_env_variable_interpolation_custom_loader(self):
self.config.from_yaml(self.config_file, loader=yaml.UnsafeLoader)
self.assertEqual(
self.config(),
{
'section1': {
'value1': 'test-value',
},
},
)
self.assertEqual(self.config.section1(), {'value1': 'test-value'})
self.assertEqual(self.config.section1.value1(), 'test-value')
@unittest.skipIf(sys.version_info[:2] == (3, 4), 'PyYAML does not support Python 3.4')
def test_option_env_variable_interpolation_custom_loader(self):
self.config.option.from_yaml(self.config_file, loader=yaml.UnsafeLoader)
self.assertEqual(
self.config.option(),
{
'section1': {
'value1': 'test-value',
},
},
)
self.assertEqual(self.config.option.section1(), {'value1': 'test-value'})
self.assertEqual(self.config.option.section1.value1(), 'test-value')
class ConfigFromDict(unittest.TestCase): class ConfigFromDict(unittest.TestCase):
@ -641,6 +782,43 @@ class ConfigFromDict(unittest.TestCase):
self.assertEqual(self.config.section3(), {'value3': '3'}) self.assertEqual(self.config.section3(), {'value3': '3'})
self.assertEqual(self.config.section3.value3(), '3') self.assertEqual(self.config.section3.value3(), '3')
def test_empty_dict(self):
self.config.from_dict({})
self.assertEqual(self.config(), {})
def test_option_empty_dict(self):
self.config.option.from_dict({})
self.assertEqual(self.config.option(), {})
def test_empty_dict_in_strict_mode(self):
self.config = providers.Configuration(strict=True)
with self.assertRaises(ValueError):
self.config.from_dict({})
def test_option_empty_dict_in_strict_mode(self):
self.config = providers.Configuration(strict=True)
with self.assertRaises(ValueError):
self.config.option.from_dict({})
def test_required_empty_dict(self):
with self.assertRaises(ValueError):
self.config.from_dict({}, required=True)
def test_required_option_empty_dict(self):
with self.assertRaises(ValueError):
self.config.option.from_dict({}, required=True)
def test_not_required_empty_dict_strict_mode(self):
self.config = providers.Configuration(strict=True)
self.config.from_dict({}, required=False)
self.assertEqual(self.config(), {})
def test_not_required_option_empty_dict_strict_mode(self):
self.config = providers.Configuration(strict=True)
self.config.option.from_dict({}, required=False)
self.assertEqual(self.config.option(), {})
self.assertEqual(self.config(), {'option': {}})
class ConfigFromEnvTests(unittest.TestCase): class ConfigFromEnvTests(unittest.TestCase):
@ -656,13 +834,77 @@ class ConfigFromEnvTests(unittest.TestCase):
self.config.from_env('CONFIG_TEST_ENV') self.config.from_env('CONFIG_TEST_ENV')
self.assertEqual(self.config(), 'test-value') 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): def test_with_children(self):
self.config.section1.value1.from_env('CONFIG_TEST_ENV') self.config.section1.value1.from_env('CONFIG_TEST_ENV')
self.assertEqual(self.config(), {'section1': {'value1': 'test-value'}}) self.assertEqual(self.config(), {'section1': {'value1': 'test-value'}})
self.assertEqual(self.config.section1(), {'value1': 'test-value'}) self.assertEqual(self.config.section1(), {'value1': 'test-value'})
self.assertEqual(self.config.section1.value1(), 'test-value') self.assertEqual(self.config.section1.value1(), 'test-value')
def test_default(self):
self.config.from_env('UNDEFINED_ENV', 'default-value')
self.assertEqual(self.config(), 'default-value')
def test_default_none(self):
self.config.from_env('UNDEFINED_ENV')
self.assertIsNone(self.config())
def test_option_default_none(self):
self.config.option.from_env('UNDEFINED_ENV')
self.assertIsNone(self.config.option())
def test_undefined_in_strict_mode(self):
self.config = providers.Configuration(strict=True)
with self.assertRaises(ValueError):
self.config.from_env('UNDEFINED_ENV')
def test_option_undefined_in_strict_mode(self):
self.config = providers.Configuration(strict=True)
with self.assertRaises(ValueError):
self.config.option.from_env('UNDEFINED_ENV')
def test_undefined_in_strict_mode_with_default(self):
self.config = providers.Configuration(strict=True)
self.config.from_env('UNDEFINED_ENV', 'default-value')
self.assertEqual(self.config(), 'default-value')
def test_option_undefined_in_strict_mode_with_default(self):
self.config = providers.Configuration(strict=True)
self.config.option.from_env('UNDEFINED_ENV', 'default-value')
self.assertEqual(self.config.option(), 'default-value')
def test_required_undefined(self):
with self.assertRaises(ValueError):
self.config.from_env('UNDEFINED_ENV', required=True)
def test_required_undefined_with_default(self):
self.config.from_env('UNDEFINED_ENV', default='default-value', required=True)
self.assertEqual(self.config(), 'default-value')
def test_option_required_undefined(self):
with self.assertRaises(ValueError):
self.config.option.from_env('UNDEFINED_ENV', required=True)
def test_option_required_undefined_with_default(self):
self.config.option.from_env('UNDEFINED_ENV', default='default-value', required=True)
self.assertEqual(self.config.option(), 'default-value')
def test_not_required_undefined_in_strict_mode(self):
self.config = providers.Configuration(strict=True)
self.config.from_env('UNDEFINED_ENV', required=False)
self.assertIsNone(self.config())
def test_option_not_required_undefined_in_strict_mode(self):
self.config = providers.Configuration(strict=True)
self.config.option.from_env('UNDEFINED_ENV', required=False)
self.assertIsNone(self.config.option())
def test_not_required_undefined_with_default_in_strict_mode(self):
self.config = providers.Configuration(strict=True)
self.config.from_env('UNDEFINED_ENV', default='default-value', required=False)
self.assertEqual(self.config(), 'default-value')
def test_option_not_required_undefined_with_default_in_strict_mode(self):
self.config = providers.Configuration(strict=True)
self.config.option.from_env('UNDEFINED_ENV', default='default-value', required=False)
self.assertEqual(self.config.option(), 'default-value')

12
tox.ini
View File

@ -1,6 +1,6 @@
[tox] [tox]
envlist= envlist=
coveralls, pylint, flake8, pydocstyle, py27, py34, py35, py36, py37, py38, pypy, pypy3 coveralls, pylint, flake8, pydocstyle, 2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, pypy2, pypy3
[testenv] [testenv]
deps= deps=
@ -17,7 +17,7 @@ commands=
unit2 discover -s tests/unit -p test_*_py3*.py unit2 discover -s tests/unit -p test_*_py3*.py
[testenv:coveralls] [testenv:coveralls]
passenv=TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH DEPENDENCY_INJECTOR_DEBUG_MODE passenv = GITHUB_* COVERALLS_*
basepython=python3.9 basepython=python3.9
usedevelop=True usedevelop=True
deps= deps=
@ -31,7 +31,7 @@ commands=
coverage report --rcfile=./.coveragerc coverage report --rcfile=./.coveragerc
coveralls coveralls
[testenv:py27] [testenv:2.7]
deps= deps=
unittest2 unittest2
extras= extras=
@ -40,7 +40,7 @@ extras=
commands= commands=
unit2 discover -s tests/unit -p test_*_py2_py3.py unit2 discover -s tests/unit -p test_*_py2_py3.py
[testenv:py34] [testenv:3.4]
deps= deps=
unittest2 unittest2
extras= extras=
@ -48,7 +48,7 @@ extras=
commands= commands=
unit2 discover -s tests/unit -p test_*_py3.py unit2 discover -s tests/unit -p test_*_py3.py
[testenv:py35] [testenv:3.5]
deps= deps=
unittest2 unittest2
extras= extras=
@ -57,7 +57,7 @@ extras=
commands= commands=
unit2 discover -s tests/unit -p test_*_py3.py unit2 discover -s tests/unit -p test_*_py3.py
[testenv:pypy] [testenv:pypy2]
deps= deps=
unittest2 unittest2
extras= extras=