diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..dc51aa9 --- /dev/null +++ b/.flake8 @@ -0,0 +1,17 @@ +[flake8] +max-line-length = 88 +ignore = E501, E203, W503 +per-file-ignores = __init__.py:F401 +exclude = + .git + __pycache__ + setup.py + build + dist + releases + .venv + .tox + .mypy_cache + .pytest_cache + .vscode + .github diff --git a/README.md b/README.md index 023ab30..261b97b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,54 @@ # config-injector -A simple json configuration dependency injector framework. +Config-injector is a very simple framework which aims to do only two things: (1) define configurable functions and (2) inject configuration data into those functions at runtime. + +## Installation +Install with pip. +```bash +pip install config-injector +``` + +## Getting Started +Annotate any callable as a configurable function using `@config`. Note that the `@config` decorator requires that you provide callable functions for each argument. These callable functions should return the expected type. The object is to break all arguments down to fundamental types: string, integer, float or dictionary. + +```python +from collections import namedtuple +from typing import Text, Dict, SupportsInt +from pathlib import Path + +from config_injector import config, Injector + + +MockThing0 = namedtuple("MockThing0", ["arg_1", "arg_2", "arg_3", "arg_4"]) + +@config(arg_1=str, arg_2=str, arg_3=str, arg_4=str) +def mock_thing_0(arg_1: Text, arg_2: Text, arg_3: Text, arg_4: Text): + return MockThing0(arg_1, arg_2, arg_3, arg_4) + + +@config(arg_5=int, arg_6=int, arg_7=int, arg_8=int) +def mock_thing_1(arg_5, arg_6, arg_7, arg_8): + return {"key_a": arg_5, "key_b": arg_6, "key_c": arg_7, "key_d": arg_8} + +@config(t0=mock_thing_0, t1=mock_thing_1, arg_9=str) +def mock_things(t0: MockThing0, t1: Dict[SupportsInt], arg_9: Text): + return (t0, t1, arg_9) + +def get_things(config_file=Path("config.json")): + injector = Injector() + injector.load_file(config_file) + return injector["things"].instantiate(mock_things) +``` + +Now that the configurable functions are annotated, we can write a configuration for them. + +```json +{ + "things": { + "t0": {"arg_1": "a", "arg_2": "b", "arg_3": "c", "arg_4": "d"}, + "t1": {"arg_5": 1, "arg_6": 2, "arg_7": 3, "arg_8": 4}, + "arg_9": "e" + } +} +``` + +This configuration file can be loaded in the runtime portion of our implementation using `get_things()` to instantiate the configured objects created by our functions. \ No newline at end of file diff --git a/config_injector/__init__.py b/config_injector/__init__.py index e69de29..ed0613d 100644 --- a/config_injector/__init__.py +++ b/config_injector/__init__.py @@ -0,0 +1,10 @@ +try: + from importlib.metadata import version as get_version +except ImportError: + from importlib_metadata import version as get_version + +from config_injector.config import config +from config_injector.injector import Injector + + +__version__ = get_version(__package__) diff --git a/config_injector/config/config.py b/config_injector/config.py similarity index 77% rename from config_injector/config/config.py rename to config_injector/config.py index c94317d..8383a81 100644 --- a/config_injector/config/config.py +++ b/config_injector/config.py @@ -1,24 +1,33 @@ from abc import ABC -from typing import (Any, Callable, Dict, List, SupportsFloat, SupportsInt, - Text, Tuple, Union) +from typing import Any +from typing import Callable +from typing import Dict +from typing import List +from typing import SupportsFloat +from typing import SupportsInt +from typing import Text +from typing import Tuple +from typing import Union + +from config_injector.exc import DoesNotSupportBuild +from config_injector.exc import InvalidConfigValue +from config_injector.exc import KeyNotInConfig +from config_injector.exc import TypeNotDefined +from config_injector.utils import get_type -from config_injector.config.exc import (DoesNotSupportBuild, - InvalidConfigValue, KeyNotInConfig, - TypeNotDefined) -from config_injector.config.utils import get_type JsonTypes = Union[Dict, List, bool, SupportsInt, SupportsFloat, Text] ComponentCallable = Union[Any, Tuple[Any]] -class SupportsBuild(ABC): - def __build__(self, **kwargs: Dict): +class SupportsFill(ABC): + def __fill__(self, **kwargs: Dict): ... -def build(f: SupportsBuild, config: Dict) -> Any: +def fill(f: SupportsFill, context: Dict) -> Any: try: - return f.__build__(**config) + return f.__fill__(**context) except AttributeError as e: if not hasattr(f, "__build__"): raise DoesNotSupportBuild(f"{f} does not support build.", e) @@ -26,7 +35,7 @@ def build(f: SupportsBuild, config: Dict) -> Any: raise e -class Config(SupportsBuild): +class Config(SupportsFill): def __init__(self, callback: Callable, **arg_callables: ComponentCallable): """ A configurable component containing hints for json parsing. @@ -49,7 +58,10 @@ def __name__(self): raise AttributeError(f"{self.callback} has no attribute {__name__}") def get_arg_type(self, arg_name, arg): - _arg_tp = self.arg_callables[arg_name] + try: + _arg_tp = self.arg_callables[arg_name] + except KeyError as e: + raise e try: type_map = {get_type(x): x for x in _arg_tp} except TypeError: @@ -74,7 +86,7 @@ def get_arg_type(self, arg_name, arg): arg_tp = _arg_tp return arg_tp - def __build__(self, **kwargs: JsonTypes) -> Any: + def __fill__(self, **kwargs: JsonTypes) -> Any: """ Cast data from parsed json prior to calling the callback. @@ -85,8 +97,8 @@ def __build__(self, **kwargs: JsonTypes) -> Any: for arg_name, arg in kwargs.items(): arg_tp = self.get_arg_type(arg_name, arg) if arg_name in self.arg_callables: - if hasattr(arg, "items") and hasattr(arg_tp, "__build__"): - arg_cast = build(arg_tp, arg) + if hasattr(arg, "items") and hasattr(arg_tp, "__fill__"): + arg_cast = fill(arg_tp, arg) else: arg_cast = arg_tp(arg) else: @@ -101,11 +113,10 @@ def config(**arg_callables: ComponentCallable) -> Callable[[], Config]: :param key: The key to use for the configuration. :param kwargs: The type for each argument in the function f. - :return: + :return: Wrapper """ def wrapper(f) -> Config: - _key = get_type(f) component = Config(f, **arg_callables) return component diff --git a/config_injector/config/__init__.py b/config_injector/config/__init__.py deleted file mode 100644 index 04c1e97..0000000 --- a/config_injector/config/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -import config_injector.config.exc -from config_injector.config.config import Config, config diff --git a/config_injector/config/exc.py b/config_injector/exc.py similarity index 87% rename from config_injector/config/exc.py rename to config_injector/exc.py index e58d6c7..6b3803e 100644 --- a/config_injector/config/exc.py +++ b/config_injector/exc.py @@ -28,3 +28,7 @@ class InvalidConfigValue(ConfigError): class DoesNotSupportBuild(ConfigError): ... + + +class FileTypeNotRecognized(ConfigError): + ... diff --git a/config_injector/injector.py b/config_injector/injector.py new file mode 100644 index 0000000..867b5cc --- /dev/null +++ b/config_injector/injector.py @@ -0,0 +1,70 @@ +import json + +from collections.abc import MutableMapping +from pathlib import Path +from typing import Dict +from typing import Text + +import toml +import yaml + +from config_injector.config import SupportsFill +from config_injector.config import fill +from config_injector.exc import FileTypeNotRecognized + + +class Injector(MutableMapping): + def __init__(self, context: Dict = None): + self.context = {} if context is None else context + + def __getitem__(self, k: Text): + v = self.context[k] + if hasattr(v, "items"): + return self.__class__(v) + else: + return v + + def __iter__(self): + return iter(self.context) + + def __setitem__(self, k, v): + self.context[k] = v + + def __delitem__(self, k): + self.context.pop(k) + + def __len__(self): + return len(self.context) + + def clear(self): + self.context = {} + + def load(self, context: Dict): + self.context.update(context) + + def load_file(self, file: Path): + if file.name.lower().endswith(".json"): + self._load_json_file(file) + elif file.name.lower().endswith(".toml"): + self._load_toml_file(file) + elif file.name.lower().endswith(".yaml") or file.name.lower().endswith(".yml"): + self._load_yaml_file(file) + else: + raise FileTypeNotRecognized( + f"Unable to determine file type for {file.name}" + ) + + def _load_json_file(self, file: Path): + with file.open() as f: + self.load(json.load(f)) + + def _load_toml_file(self, file: Path): + with file.open() as f: + self.load(toml.load(f)) + + def _load_yaml_file(self, file: Path): + with file.open() as f: + self.load(yaml.load(f, Loader=yaml.SafeLoader)) + + def instantiate(self, config: SupportsFill): + return fill(config, self.context) diff --git a/config_injector/config/utils.py b/config_injector/utils.py similarity index 100% rename from config_injector/config/utils.py rename to config_injector/utils.py diff --git a/poetry.lock b/poetry.lock index fa9ef41..490a6b4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -39,6 +39,7 @@ python-versions = ">=3.6" [package.dependencies] appdirs = "*" click = ">=7.1.2" +dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} mypy-extensions = ">=0.4.3" pathspec = ">=0.6,<1" regex = ">=2020.1.8" @@ -67,20 +68,12 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] -name = "distlib" -version = "0.3.1" -description = "Distribution utilities" +name = "dataclasses" +version = "0.8" +description = "A backport of the dataclasses module for Python 3.6" category = "dev" optional = false -python-versions = "*" - -[[package]] -name = "filelock" -version = "3.0.12" -description = "A platform independent file lock." -category = "dev" -optional = false -python-versions = "*" +python-versions = ">=3.6, <3.7" [[package]] name = "flake8" @@ -98,11 +91,11 @@ pyflakes = ">=2.2.0,<2.3.0" [[package]] name = "importlib-metadata" -version = "2.1.0" +version = "3.1.0" description = "Read metadata from Python packages" -category = "dev" +category = "main" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] zipp = ">=0.5" @@ -190,6 +183,17 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pyaml" +version = "20.4.0" +description = "PyYAML-based module to produce pretty and readable YAML-serialized data" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +PyYAML = "*" + [[package]] name = "pycodestyle" version = "2.6.0" @@ -234,9 +238,17 @@ py = ">=1.8.2" toml = "*" [package.extras] -checkqa_mypy = ["mypy (0.780)"] +checkqa_mypy = ["mypy (==0.780)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +[[package]] +name = "pyyaml" +version = "5.3.1" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "regex" version = "2020.11.13" @@ -257,33 +269,10 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -[[package]] -name = "tox" -version = "3.20.1" -description = "tox is a generic virtualenv management and test command line tool" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[package.dependencies] -colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} -filelock = ">=3.0.0" -importlib-metadata = {version = ">=0.12,<3", markers = "python_version < \"3.8\""} -packaging = ">=14" -pluggy = ">=0.12.0" -py = ">=1.4.17" -six = ">=1.14.0" -toml = ">=0.9.4" -virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" - -[package.extras] -docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] -testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)"] - [[package]] name = "typed-ast" version = "1.4.1" @@ -300,41 +289,22 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "virtualenv" -version = "20.2.1" -description = "Virtual Python Environment builder" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" - -[package.dependencies] -appdirs = ">=1.4.3,<2" -distlib = ">=0.3.1,<1" -filelock = ">=3.0.0,<4" -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -six = ">=1.9.0,<2" - -[package.extras] -docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] - [[package]] name = "zipp" version = "3.4.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" -python-versions = "^3.7.3" -content-hash = "f1a4c3073b98c4af1cb55a4cdbe4b2b4d0d43713fa0d6bc103c69b1c9fb05cff" +python-versions = "^3.6" +content-hash = "194d61ae6d488a286dcd2c9c54cdfbea399339a2db710edd0242ae68370ab000" [metadata.files] appdirs = [ @@ -360,21 +330,17 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] -distlib = [ - {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, - {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, -] -filelock = [ - {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, - {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +dataclasses = [ + {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, + {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, ] flake8 = [ {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, ] importlib-metadata = [ - {file = "importlib_metadata-2.1.0-py2.py3-none-any.whl", hash = "sha256:030f3b1bdb823ecbe4a9659e14cc861ce5af403fe99863bae173ec5fe00ab132"}, - {file = "importlib_metadata-2.1.0.tar.gz", hash = "sha256:caeee3603f5dcf567864d1be9b839b0bcfdf1383e3e7be33ce2dead8144ff19c"}, + {file = "importlib_metadata-3.1.0-py2.py3-none-any.whl", hash = "sha256:590690d61efdd716ff82c39ca9a9d4209252adfe288a4b5721181050acbd4175"}, + {file = "importlib_metadata-3.1.0.tar.gz", hash = "sha256:d9b8a46a0885337627a6430db287176970fff18ad421becec1d64cfc763c2099"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -408,6 +374,10 @@ py = [ {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, ] +pyaml = [ + {file = "pyaml-20.4.0-py2.py3-none-any.whl", hash = "sha256:67081749a82b72c45e5f7f812ee3a14a03b3f5c25ff36ec3b290514f8c4c4b99"}, + {file = "pyaml-20.4.0.tar.gz", hash = "sha256:29a5c2a68660a799103d6949167bd6c7953d031449d08802386372de1db6ad71"}, +] pycodestyle = [ {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, @@ -424,6 +394,19 @@ pytest = [ {file = "pytest-6.1.2-py3-none-any.whl", hash = "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe"}, {file = "pytest-6.1.2.tar.gz", hash = "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"}, ] +pyyaml = [ + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, +] regex = [ {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"}, @@ -475,10 +458,6 @@ toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] -tox = [ - {file = "tox-3.20.1-py2.py3-none-any.whl", hash = "sha256:42ce19ce5dc2f6d6b1fdc5666c476e1f1e2897359b47e0aa3a5b774f335d57c2"}, - {file = "tox-3.20.1.tar.gz", hash = "sha256:4321052bfe28f9d85082341ca8e233e3ea901fdd14dab8a5d3fbd810269fbaf6"}, -] typed-ast = [ {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, @@ -516,10 +495,6 @@ typing-extensions = [ {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, ] -virtualenv = [ - {file = "virtualenv-20.2.1-py2.py3-none-any.whl", hash = "sha256:07cff122e9d343140366055f31be4dcd61fd598c69d11cd33a9d9c8df4546dd7"}, - {file = "virtualenv-20.2.1.tar.gz", hash = "sha256:e0aac7525e880a429764cefd3aaaff54afb5d9f25c82627563603f5d7de5a6e5"}, -] zipp = [ {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, diff --git a/pyproject.toml b/pyproject.toml index 6fb53ee..231c854 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "config-injector" -version = "0.1.1" +version = "0.2.0" description = "Simple dependency injection framework for python for easy and logical app configuration." authors = ["DustinMoriarty "] readme = "README.md" @@ -9,17 +9,51 @@ repository = "https://github.com/DustinMoriarty/config-injector" documentation = "https://github.com/DustinMoriarty/config-injector" license = "MIT" - [tool.poetry.dependencies] -python = "^3.7.3" +python = "^3.6" +toml = "^0.10.2" +pyaml = "^20.4.0" +PyYAML = "^5.3.1" +importlib-metadata = {version = "^3.1.0", python = "<3.8"} [tool.poetry.dev-dependencies] -black = "^20.8b1" isort = "^5.6.4" flake8 = "^3.8.4" pytest = "^6.1.2" -tox = "^3.20.1" +black = "^20.8b1" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.isort] +profile = "black" +force_single_line = true +atomic = true +include_trailing_comma = true +lines_after_imports = 2 +lines_between_types = 1 +use_parentheses = true +src_paths = ["poetry", "tests"] +skip_glob = ["*/setup.py"] +filter_files = true +known_first_party = "poetry" + +[tool.black] +line-length = 88 +include = '\.pyi?$' +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | tests/.*/setup.py +)/ +''' diff --git a/tests/config/test_config.py b/tests/test_config.py similarity index 94% rename from tests/config/test_config.py rename to tests/test_config.py index 241fa6f..b880e4a 100644 --- a/tests/config/test_config.py +++ b/tests/test_config.py @@ -2,7 +2,8 @@ import pytest -from config_injector.config.config import build, config +from config_injector.config import config +from config_injector.config import fill class DictEq: @@ -86,4 +87,4 @@ def __init__(self, pets): ], ) def test_foo(f, config, expected): - assert expected == build(f, config) + assert expected == fill(f, config) diff --git a/tests/test_injector.py b/tests/test_injector.py new file mode 100644 index 0000000..a5fe64d --- /dev/null +++ b/tests/test_injector.py @@ -0,0 +1,57 @@ +from collections import namedtuple +from typing import Text + +import pytest + +from config_injector import Injector +from config_injector import config + + +MockThing0 = namedtuple("MockThing0", ["arg_1", "arg_2", "arg_3", "arg_4"]) +MockThing1 = namedtuple("MockThing1", ["arg_5", "arg_6", "arg_7", "arg_8"]) + + +@config(arg_1=str, arg_2=str, arg_3=str, arg_4=str) +def mock_thing_0(arg_1: Text, arg_2: Text, arg_3: Text, arg_4: Text): + return MockThing0(arg_1, arg_2, arg_3, arg_4) + + +@config(arg_5=int, arg_6=int, arg_7=int, arg_8=int) +def mock_thing_1(arg_5, arg_6, arg_7, arg_8): + return MockThing1(arg_5, arg_6, arg_7, arg_8) + + +@config(t0=mock_thing_0, t1=mock_thing_1, arg_9=str) +def mock_things(t0: MockThing0, t1: MockThing1, arg_9: Text): + return (t0, t1, arg_9) + + +@pytest.fixture() +def context(): + return { + "things": { + "t0": {"arg_1": "a", "arg_2": "b", "arg_3": "c", "arg_4": "d"}, + "t1": {"arg_5": 1, "arg_6": 2, "arg_7": 3, "arg_8": 4}, + "arg_9": "e", + } + } + + +@pytest.fixture() +def injector(context): + return Injector(context) + + +def test_injector_inject(injector): + thing_1: MockThing1 = injector["things"]["t1"].instantiate(mock_thing_1) + assert injector["things"]["t1"]["arg_5"] == thing_1.arg_5 + assert injector["things"]["t1"]["arg_6"] == thing_1.arg_6 + + +def test_injector_inject_nested(injector): + things = injector["things"].instantiate(mock_things) + assert isinstance(things[0], MockThing0) + assert isinstance(things[1], MockThing1) + assert injector["things"]["t0"]["arg_1"] == things[0].arg_1 + assert injector["things"]["t1"]["arg_5"] == things[1].arg_5 + assert injector["things"]["arg_9"] == things[2] diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..82a3f22 --- /dev/null +++ b/tox.ini @@ -0,0 +1,17 @@ +[tox] +isolated_build = True +envlist = py36, py37, py38 + +[testenv] +deps = + pytest + black + isort + flake8 +commands = + isort -c . + black --check . + flake8 + pytest tests + +