mirror of
https://github.com/ets-labs/python-dependency-injector.git
synced 2024-11-21 17:16:46 +03:00
Add config.from_env(as_=...) (#541)
* Add implementation and typing stub * Add unit tests * Update demo example * Add typing tests * Update changelog * Update docs * Add tests for an empty environment variable * Improve wording in di_in_python.rst * Update wording in changelog and docs * Update doc blocks
This commit is contained in:
parent
cc17052acc
commit
cfadd8c3fa
|
@ -90,7 +90,7 @@ Key features of the ``Dependency Injector``:
|
|||
api_client = providers.Singleton(
|
||||
ApiClient,
|
||||
api_key=config.api_key,
|
||||
timeout=config.timeout.as_int(),
|
||||
timeout=config.timeout,
|
||||
)
|
||||
|
||||
service = providers.Factory(
|
||||
|
@ -106,8 +106,8 @@ Key features of the ``Dependency Injector``:
|
|||
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.config.api_key.from_env("API_KEY")
|
||||
container.config.timeout.from_env("TIMEOUT")
|
||||
container.config.api_key.from_env("API_KEY", required=True)
|
||||
container.config.timeout.from_env("TIMEOUT", as_=int, default=5)
|
||||
container.wire(modules=[__name__])
|
||||
|
||||
main() # <-- dependency is injected automatically
|
||||
|
|
|
@ -96,7 +96,7 @@ Key features of the ``Dependency Injector``:
|
|||
api_client = providers.Singleton(
|
||||
ApiClient,
|
||||
api_key=config.api_key,
|
||||
timeout=config.timeout.as_int(),
|
||||
timeout=config.timeout,
|
||||
)
|
||||
|
||||
service = providers.Factory(
|
||||
|
@ -112,8 +112,8 @@ Key features of the ``Dependency Injector``:
|
|||
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.config.api_key.from_env("API_KEY")
|
||||
container.config.timeout.from_env("TIMEOUT")
|
||||
container.config.api_key.from_env("API_KEY", required=True)
|
||||
container.config.timeout.from_env("TIMEOUT", as_=int, default=5)
|
||||
container.wire(modules=[__name__])
|
||||
|
||||
main() # <-- dependency is injected automatically
|
||||
|
|
|
@ -150,7 +150,7 @@ What does the Dependency Injector do?
|
|||
-------------------------------------
|
||||
|
||||
With the dependency injection pattern objects loose the responsibility of assembling
|
||||
the dependencies. The ``Dependency Injector`` absorbs that responsibilities.
|
||||
the dependencies. The ``Dependency Injector`` absorbs that responsibility.
|
||||
|
||||
``Dependency Injector`` helps to assemble and inject the dependencies.
|
||||
|
||||
|
@ -172,7 +172,7 @@ the dependency.
|
|||
api_client = providers.Singleton(
|
||||
ApiClient,
|
||||
api_key=config.api_key,
|
||||
timeout=config.timeout.as_int(),
|
||||
timeout=config.timeout,
|
||||
)
|
||||
|
||||
service = providers.Factory(
|
||||
|
@ -188,8 +188,8 @@ the dependency.
|
|||
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.config.api_key.from_env("API_KEY")
|
||||
container.config.timeout.from_env("TIMEOUT")
|
||||
container.config.api_key.from_env("API_KEY", required=True)
|
||||
container.config.timeout.from_env("TIMEOUT", as_=int, default=5)
|
||||
container.wire(modules=[__name__])
|
||||
|
||||
main() # <-- dependency is injected automatically
|
||||
|
|
|
@ -9,6 +9,8 @@ follows `Semantic versioning`_
|
|||
|
||||
Development version
|
||||
-------------------
|
||||
- Add argument ``as_`` to the ``config.from_env()`` method for the explicit type casting
|
||||
of an environment variable value, e.g.: ``config.timeout.from_env("TIMEOUT", as_=int)``.
|
||||
- Add ``.providers`` attribute to the ``FactoryAggregate`` provider. It is an alias for
|
||||
``FactoryAggregate.factories`` attribute.
|
||||
- Add ``.set_providers()`` method to the ``FactoryAggregate`` provider. It is an alias for
|
||||
|
|
|
@ -205,6 +205,24 @@ Loading from an environment variable
|
|||
:lines: 3-
|
||||
:emphasize-lines: 18-20
|
||||
|
||||
You can use ``as_`` argument for the type casting of an environment variable value:
|
||||
|
||||
.. code-block:: python
|
||||
:emphasize-lines: 2,6,10
|
||||
|
||||
# API_KEY=secret
|
||||
container.config.api_key.from_env("API_KEY", as_=str, required=True)
|
||||
assert container.config.api_key() == "secret"
|
||||
|
||||
# SAMPLING_RATIO=0.5
|
||||
container.config.sampling.from_env("SAMPLING_RATIO", as_=float, required=True)
|
||||
assert container.config.sampling() == 0.5
|
||||
|
||||
# TIMEOUT undefined, default is used
|
||||
container.config.timeout.from_env("TIMEOUT", as_=int, default=5)
|
||||
assert container.config.timeout() == 5
|
||||
|
||||
|
||||
Loading a value
|
||||
---------------
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ class Container(containers.DeclarativeContainer):
|
|||
api_client = providers.Singleton(
|
||||
ApiClient,
|
||||
api_key=config.api_key,
|
||||
timeout=config.timeout.as_int(),
|
||||
timeout=config.timeout,
|
||||
)
|
||||
|
||||
service = providers.Factory(
|
||||
|
@ -29,8 +29,8 @@ def main(service: Service = Provide[Container.service]):
|
|||
|
||||
if __name__ == "__main__":
|
||||
container = Container()
|
||||
container.config.api_key.from_env("API_KEY")
|
||||
container.config.timeout.from_env("TIMEOUT")
|
||||
container.config.api_key.from_env("API_KEY", required=True)
|
||||
container.config.timeout.from_env("TIMEOUT", as_=int, default=5)
|
||||
container.wire(modules=[__name__])
|
||||
|
||||
main() # <-- dependency is injected automatically
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -205,7 +205,7 @@ class ConfigurationOption(Provider[Any]):
|
|||
def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any] = None, envs_required: bool = False) -> None: ...
|
||||
def from_pydantic(self, settings: PydanticSettings, required: bool = False, **kwargs: Any) -> None: ...
|
||||
def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ...
|
||||
def from_env(self, name: str, default: Optional[Any] = None, required: bool = False) -> None: ...
|
||||
def from_env(self, name: str, default: Optional[Any] = None, required: bool = False, as_: Optional[_Callable[..., Any]] = None) -> None: ...
|
||||
def from_value(self, value: Any) -> None: ...
|
||||
|
||||
|
||||
|
@ -262,7 +262,7 @@ class Configuration(Object[Any]):
|
|||
def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any] = None, envs_required: bool = False) -> None: ...
|
||||
def from_pydantic(self, settings: PydanticSettings, required: bool = False, **kwargs: Any) -> None: ...
|
||||
def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ...
|
||||
def from_env(self, name: str, default: Optional[Any] = None, required: bool = False) -> None: ...
|
||||
def from_env(self, name: str, default: Optional[Any] = None, required: bool = False, as_: Optional[_Callable[..., Any]] = None) -> None: ...
|
||||
def from_value(self, value: Any) -> None: ...
|
||||
|
||||
|
||||
|
|
|
@ -1689,7 +1689,7 @@ cdef class ConfigurationOption(Provider):
|
|||
|
||||
self.override(merge_dicts(current_config, options))
|
||||
|
||||
def from_env(self, name, default=UNDEFINED, required=UNDEFINED):
|
||||
def from_env(self, name, default=UNDEFINED, required=UNDEFINED, as_=UNDEFINED):
|
||||
"""Load configuration value from the environment variable.
|
||||
|
||||
:param name: Name of the environment variable.
|
||||
|
@ -1701,6 +1701,9 @@ cdef class ConfigurationOption(Provider):
|
|||
:param required: When required is True, raise an exception if environment variable is undefined.
|
||||
:type required: bool
|
||||
|
||||
:param as_: Callable used for type casting (int, float, etc).
|
||||
:type as_: object
|
||||
|
||||
:rtype: None
|
||||
"""
|
||||
value = os.environ.get(name, default)
|
||||
|
@ -1711,6 +1714,9 @@ cdef class ConfigurationOption(Provider):
|
|||
raise ValueError('Environment variable "{0}" is undefined'.format(name))
|
||||
value = None
|
||||
|
||||
if as_ is not UNDEFINED:
|
||||
value = as_(value)
|
||||
|
||||
self.override(value)
|
||||
|
||||
def from_value(self, value):
|
||||
|
@ -2191,7 +2197,7 @@ cdef class Configuration(Object):
|
|||
current_config = {}
|
||||
self.override(merge_dicts(current_config, options))
|
||||
|
||||
def from_env(self, name, default=UNDEFINED, required=UNDEFINED):
|
||||
def from_env(self, name, default=UNDEFINED, required=UNDEFINED, as_=UNDEFINED):
|
||||
"""Load configuration value from the environment variable.
|
||||
|
||||
:param name: Name of the environment variable.
|
||||
|
@ -2203,6 +2209,9 @@ cdef class Configuration(Object):
|
|||
:param required: When required is True, raise an exception if environment variable is undefined.
|
||||
:type required: bool
|
||||
|
||||
:param as_: Callable used for type casting (int, float, etc).
|
||||
:type as_: object
|
||||
|
||||
:rtype: None
|
||||
"""
|
||||
value = os.environ.get(name, default)
|
||||
|
@ -2213,6 +2222,9 @@ cdef class Configuration(Object):
|
|||
raise ValueError('Environment variable "{0}" is undefined'.format(name))
|
||||
value = None
|
||||
|
||||
if as_ is not UNDEFINED:
|
||||
value = as_(value)
|
||||
|
||||
self.override(value)
|
||||
|
||||
def from_value(self, value):
|
||||
|
|
|
@ -14,7 +14,11 @@ config2.from_ini(Path("config.ini"))
|
|||
|
||||
config2.from_yaml("config.yml")
|
||||
config2.from_yaml(Path("config.yml"))
|
||||
|
||||
config2.from_env("ENV", "default")
|
||||
config2.from_env("ENV", as_=int, default=123)
|
||||
config2.from_env("ENV", as_=float, required=True)
|
||||
config2.from_env("ENV", as_=lambda env: str(env))
|
||||
|
||||
# Test 3: to check as_*() methods
|
||||
config3 = providers.Configuration()
|
||||
|
|
|
@ -106,7 +106,11 @@ def environment_variables():
|
|||
os.environ["CONFIG_TEST_ENV"] = "test-value"
|
||||
os.environ["CONFIG_TEST_PATH"] = "test-path"
|
||||
os.environ["DEFINED"] = "defined"
|
||||
os.environ["EMPTY"] = ""
|
||||
os.environ["CONFIG_INT"] = "42"
|
||||
yield
|
||||
os.environ.pop("CONFIG_TEST_ENV", None)
|
||||
os.environ.pop("CONFIG_TEST_PATH", None)
|
||||
os.environ.pop("DEFINED", None)
|
||||
os.environ.pop("EMPTY", None)
|
||||
os.environ.pop("CONFIG_INT", None)
|
||||
|
|
|
@ -31,6 +31,54 @@ def test_option_default_none(config):
|
|||
assert config.option() is None
|
||||
|
||||
|
||||
def test_as_(config):
|
||||
config.from_env("CONFIG_INT", as_=int)
|
||||
assert config() == 42
|
||||
assert isinstance(config(), int)
|
||||
|
||||
|
||||
def test_as__default(config):
|
||||
config.from_env("UNDEFINED", as_=int, default="33")
|
||||
assert config() == 33
|
||||
assert isinstance(config(), int)
|
||||
|
||||
|
||||
def test_as__undefined_required(config):
|
||||
with raises(ValueError):
|
||||
config.from_env("UNDEFINED", as_=int, required=True)
|
||||
assert config() == {}
|
||||
|
||||
|
||||
def test_as__defined_empty(config):
|
||||
with raises(ValueError):
|
||||
config.from_env("EMPTY", as_=int)
|
||||
assert config() == {}
|
||||
|
||||
|
||||
def test_option_as_(config):
|
||||
config.option.from_env("CONFIG_INT", as_=int)
|
||||
assert config.option() == 42
|
||||
assert isinstance(config.option(), int)
|
||||
|
||||
|
||||
def test_option_as__default(config):
|
||||
config.option.from_env("UNDEFINED", as_=int, default="33")
|
||||
assert config.option() == 33
|
||||
assert isinstance(config.option(), int)
|
||||
|
||||
|
||||
def test_option_as__undefined_required(config):
|
||||
with raises(ValueError):
|
||||
config.option.from_env("UNDEFINED", as_=int, required=True)
|
||||
assert config.option() is None
|
||||
|
||||
|
||||
def test_option_as__defined_empty(config):
|
||||
with raises(ValueError):
|
||||
config.option.from_env("EMPTY", as_=int)
|
||||
assert config.option() is None
|
||||
|
||||
|
||||
@mark.parametrize("config_type", ["strict"])
|
||||
def test_undefined_in_strict_mode(config):
|
||||
with raises(ValueError):
|
||||
|
|
Loading…
Reference in New Issue
Block a user