diff --git a/README.md b/README.md index 4fb9e2b..fef4d90 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ pip install aiodi name = "%env(str:APP_NAME, 'sample')%" version = "%env(int:APP_VERSION, '1')%" log_level = "%env(APP_LEVEL, 'INFO')%" +debug = "%env(bool:int:APP_DEBUG, '0')%" +text = "Hello World" [tool.aiodi.services."_defaults"] project_dir = "../../.." diff --git a/aiodi/__init__.py b/aiodi/__init__.py index 97965fb..bbc4ca7 100644 --- a/aiodi/__init__.py +++ b/aiodi/__init__.py @@ -3,7 +3,7 @@ from .builder import ContainerBuilder from .container import Container, ContainerKey -__version__ = '1.1.1' +__version__ = '1.1.2' __all__ = ( # di diff --git a/aiodi/resolver/__init__.py b/aiodi/resolver/__init__.py index bf0433c..a7af2c2 100644 --- a/aiodi/resolver/__init__.py +++ b/aiodi/resolver/__init__.py @@ -43,7 +43,7 @@ def times(self) -> int: class Resolver(ABC, Generic[Metadata, Value]): @abstractmethod - def extract_metadata(self, data: Dict[str, Any], extra: Dict[str, Any] = {}) -> Metadata: + def extract_metadata(self, data: Dict[str, Any], extra: Dict[str, Any]) -> Metadata: """ Extract metadata from data @@ -53,7 +53,7 @@ def extract_metadata(self, data: Dict[str, Any], extra: Dict[str, Any] = {}) -> """ @abstractmethod - def parse_value(self, metadata: Metadata, retries: int = -1, extra: Dict[str, Any] = {}) -> Value: + def parse_value(self, metadata: Metadata, retries: int, extra: Dict[str, Any]) -> Value: """ Parse value from metadata diff --git a/aiodi/resolver/loader.py b/aiodi/resolver/loader.py index 39c22c3..5c1f586 100644 --- a/aiodi/resolver/loader.py +++ b/aiodi/resolver/loader.py @@ -62,9 +62,7 @@ def from_metadata(cls, metadata: LoaderMetadata, data: OutputData) -> 'LoadData' class LoaderResolver(Resolver[LoaderMetadata, LoadData]): - def extract_metadata( - self, data: Dict[str, Any], extra: Dict[str, Any] = {} # pylint: disable=W0613 - ) -> LoaderMetadata: + def extract_metadata(self, data: Dict[str, Any], extra: Dict[str, Any]) -> LoaderMetadata: # pylint: disable=W0613 return LoaderMetadata( path_data=data['path_data'], decoders=data['decoders'], @@ -73,8 +71,8 @@ def extract_metadata( def parse_value( self, metadata: LoaderMetadata, - retries: int = -1, # pylint: disable=W0613 - extra: Dict[str, Any] = {}, # pylint: disable=W0613 + retries: int, # pylint: disable=W0613 + extra: Dict[str, Any], # pylint: disable=W0613 ) -> LoadData: return LoadData.from_metadata(metadata=metadata, data=metadata.decode()) @@ -83,5 +81,5 @@ def prepare_loader_to_parse( resolver: Resolver[Any, Any], items: Dict[str, Any], extra: Dict[str, Any] # pylint: disable=W0613 ) -> Dict[str, Tuple[LoaderMetadata, int]]: return { - 'value': (resolver.extract_metadata(data=items), 0), + 'value': (resolver.extract_metadata(data=items, extra=extra), 0), } diff --git a/aiodi/resolver/path.py b/aiodi/resolver/path.py index 092ee3f..c35014d 100644 --- a/aiodi/resolver/path.py +++ b/aiodi/resolver/path.py @@ -47,16 +47,14 @@ def from_metadata(cls, metadata: PathMetadata) -> 'PathData': class PathResolver(Resolver[PathMetadata, PathData]): - def extract_metadata( - self, data: Dict[str, Any], extra: Dict[str, Any] = {} # pylint: disable=W0613 - ) -> PathMetadata: + def extract_metadata(self, data: Dict[str, Any], extra: Dict[str, Any]) -> PathMetadata: # pylint: disable=W0613 return PathMetadata(cwd=data.get('cwd', None), filenames=data.get('filenames', [])) def parse_value( self, metadata: PathMetadata, - retries: int = -1, # pylint: disable=W0613 - extra: Dict[str, Any] = {}, # pylint: disable=W0613 + retries: int, # pylint: disable=W0613 + extra: Dict[str, Any], # pylint: disable=W0613 ) -> PathData: return PathData.from_metadata(metadata) @@ -65,5 +63,5 @@ def prepare_path_to_parse( resolver: Resolver[Any, Any], items: Dict[str, Any], extra: Dict[str, Any] # pylint: disable=W0613 ) -> Dict[str, Tuple[PathMetadata, int]]: return { - 'value': (resolver.extract_metadata(data=items), 0), + 'value': (resolver.extract_metadata(data=items, extra=extra), 0), } diff --git a/aiodi/resolver/service.py b/aiodi/resolver/service.py index 573660e..2814ffb 100644 --- a/aiodi/resolver/service.py +++ b/aiodi/resolver/service.py @@ -73,7 +73,7 @@ def compute_excludes(self) -> List[str]: return [left] if len(rights) == 0 else rights def compute_services( - self, resolver: Resolver[Any, Any], resources: List[str], excludes: List[str] + self, resolver: Resolver[Any, Any], resources: List[str], excludes: List[str], extra: Dict[str, Any] ) -> Dict[str, Tuple['ServiceMetadata', int]]: names: List[str] = [] for include in resources: @@ -100,7 +100,8 @@ def compute_services( autowire=self.autowire, autoconfigure=self.autoconfigure, ), - } + }, + extra=extra, ), 0, ) @@ -208,9 +209,7 @@ def _define_service_type(name: str, typ: str, cls: str) -> Tuple[Type[Any], Type return typ, cls # type: ignore - def extract_metadata( - self, data: Dict[str, Any], extra: Dict[str, Any] = {} # pylint: disable=W0613 - ) -> ServiceMetadata: + def extract_metadata(self, data: Dict[str, Any], extra: Dict[str, Any]) -> ServiceMetadata: # pylint: disable=W0613 key = cast(str, data.get('key')) val = data.get('val') defaults = cast(ServiceDefaults, data.get('defaults')) @@ -233,7 +232,7 @@ def extract_metadata( defaults=defaults, ) - def parse_value(self, metadata: ServiceMetadata, retries: int = -1, extra: Dict[str, Any] = {}) -> Any: + def parse_value(self, metadata: ServiceMetadata, retries: int, extra: Dict[str, Any]) -> Any: _variables = cast(Dict[str, Any], extra.get('variables')) _services = cast(Dict[str, Any], extra.get('services')) variable_resolver = cast(Resolver, extra.get('resolvers', {}).get('variable')) @@ -248,8 +247,10 @@ def parse_value(self, metadata: ServiceMetadata, retries: int = -1, extra: Dict[ data={ 'key': '@{0}:{1}'.format(metadata.name, param.name), 'val': metadata.arguments[param.name], - } + }, + extra=extra, ), + retries=-1, extra={'variables': _variables}, ) elif param.source_kind == 'svc': @@ -287,11 +288,14 @@ def prepare_services_to_parse( if defaults.has_resources(): services.update( defaults.compute_services( - resolver=resolver, resources=defaults.compute_resources(), excludes=defaults.compute_excludes() + resolver=resolver, + resources=defaults.compute_resources(), + excludes=defaults.compute_excludes(), + extra=extra, ) ) else: - metadata = resolver.extract_metadata(data={'key': key, 'val': val, 'defaults': defaults}) + metadata = resolver.extract_metadata(data={'key': key, 'val': val, 'defaults': defaults}, extra=extra) if is_abstract(metadata.type): raise TypeError('Can not instantiate abstract class <{0}>!'.format(metadata.name)) services[key] = (metadata, 0) diff --git a/aiodi/resolver/variable.py b/aiodi/resolver/variable.py index 8efe846..7f4cc5d 100644 --- a/aiodi/resolver/variable.py +++ b/aiodi/resolver/variable.py @@ -4,29 +4,36 @@ from ..helpers import raise_, re_finditer from . import Resolver, ValueNotFound, ValueResolutionPostponed -REGEX = r"%(static|env|var)\((str:|int:|float:|bool:)?([\w]+)(,\s{1}'.*?')?\)%" +REGEX = r"%(static|env|var)\(([str:int:float:bool:]*?)([\w]+)(,\s{1}'.*?')?\)%" STATIC_TEMPLATE: str = "%static({0}:{1}, '{2}')%" +_VAR_DEFAULTS = ... class VariableMetadata(NamedTuple): name: str value: Any - matches: List['VariableMetadata.MatchMetadata'] = [] # type: ignore + matches: List['VariableMetadata.MatchMetadata'] # type: ignore class MatchMetadata(NamedTuple): # type: ignore source_kind: str - type: Type[Any] + types: List[Type[Any]] source_name: str default: Any match: Match @classmethod def from_match(cls, match: Match) -> 'VariableMetadata.MatchMetadata': + raw_types = ( + ['str'] + if match.groups()[1] is None or len(str(match.groups()[1])) == 0 + else [s for s in str(match.groups()[1]).split(':') if s.strip() != ''] + ) + raw_default = _VAR_DEFAULTS if match.groups()[3] is None else str(match.groups()[3])[3:-1] return cls( source_kind=str(match.groups()[0]), - type=str if match.groups()[1] is None else globals()['__builtins__'][str(match.groups()[1])[:-1]], + types=[globals()['__builtins__'][raw_type] for raw_type in raw_types], source_name=str(match.groups()[2]), - default=None if match.groups()[3] is None else str(match.groups()[3])[3:-1], + default=None if isinstance(raw_default, str) and raw_default == 'None' else raw_default, match=match, ) @@ -36,6 +43,11 @@ def __init__(self, name: str) -> None: super().__init__(kind='Variable', name=name) +class EnvironmentVariableNotFound(ValueNotFound): + def __init__(self, name: str) -> None: + super().__init__(kind='EnvironmentVariable', name=name) + + class VariableResolutionPostponed(ValueResolutionPostponed[VariableMetadata]): pass @@ -49,7 +61,7 @@ def __call__(string: Any) -> List[Match]: return __call__(string=val) or __call__(string=STATIC_TEMPLATE.format(type(val).__name__, key, val)) def extract_metadata( - self, data: Dict[str, Any], extra: Dict[str, Any] = {} # pylint: disable=W0613 + self, data: Dict[str, Any], extra: Dict[str, Any] # pylint: disable=W0613 ) -> VariableMetadata: key: str = data.get('key') or raise_(KeyError('Missing key "key" to extract variable metadata')) # type: ignore val: Any = data.get('val') or raise_(KeyError('Missing key "val" to extract variable metadata')) @@ -64,8 +76,9 @@ def extract_metadata( ) def parse_value( - self, metadata: VariableMetadata, retries: int = -1, extra: Dict[str, Any] = {} # pylint: disable=W0613 + self, metadata: VariableMetadata, retries: int, extra: Dict[str, Any] # pylint: disable=W0613 ) -> Any: + extra = {} if extra is None or not isinstance(extra, dict) else extra _variables: Dict[str, Any] = extra.get('variables') # type: ignore if _variables is None: raise KeyError('Missing key "variables" to parse variable value') @@ -76,7 +89,12 @@ def parse_value( if metadata_.source_kind == 'static': typ_val = metadata_.default elif metadata_.source_kind == 'env': - typ_val = getenv(key=metadata_.source_name, default=metadata_.default or '') + typ_val = getenv(key=metadata_.source_name, default=metadata_.default) + if typ_val is None: + # can only concatenate str to str + return typ_val + if metadata_.default is _VAR_DEFAULTS and typ_val == metadata_.default: + raise EnvironmentVariableNotFound(name=metadata_.source_name) elif metadata_.source_kind == 'var': if metadata_.source_name not in _variables: if retries != -1: @@ -93,11 +111,18 @@ def parse_value( values += metadata.value[metadata_.match.end() :] value: Any = ''.join(values) if len(metadata.matches) == 1: - value = metadata.matches[0].type(value) + # multiple casting + for type_ in reversed(metadata.matches[0].types): + value = type_(value) return value def prepare_variables_to_parse( resolver: Resolver[Any, Any], items: Dict[str, Any], extra: Dict[str, Any] # pylint: disable=W0613 ) -> Dict[str, Tuple[VariableMetadata, int]]: - return dict([(key, (resolver.extract_metadata(data={'key': key, 'val': val}), 0)) for key, val in items.items()]) + return dict( + [ + (key, (resolver.extract_metadata(data={'key': key, 'val': val}, extra=extra), 0)) + for key, val in items.items() + ] + ) diff --git a/pyproject.toml b/pyproject.toml index 558a688..743cef9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [metadata] name = "aiodi" -version = "1.1.1" +version = "1.1.2" description = "Container for the Dependency Injection in Python." license = "MIT" authors = ["ticdenis "] diff --git a/requirements-dev.txt b/requirements-dev.txt index fd6951e..5f9b36e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,11 +3,11 @@ autoflake==1.4 # fixed due to Python 3.6 support bandit==1.7.1 black==22.1.0 -commitizen==2.20.5 +commitizen==2.21.0 isort==5.10.1 liccheck==0.6.5 mkdocs==1.2.3 -mkdocs-material==8.1.10 +mkdocs-material==8.2.1 mypy==0.931 pre-commit==2.17.0 psutil==5.9.0 @@ -23,5 +23,5 @@ setuptools==59.6.0 # fixed due to Python 3.6 support shellcheck-py==0.8.0.3 twine==3.8.0 -types-toml==0.10.3 +types-toml==0.10.4 wheel==0.37.1 diff --git a/sample/pyproject.toml b/sample/pyproject.toml index f4ea8cd..c0aefd3 100644 --- a/sample/pyproject.toml +++ b/sample/pyproject.toml @@ -2,6 +2,8 @@ name = "%env(str:APP_NAME, 'sample')%" version = "%env(int:APP_VERSION, '1')%" log_level = "%env(APP_LEVEL, 'INFO')%" +debug = "%env(bool:int:APP_DEBUG, '0')%" +text = "Hello World" [tool.aiodi.services."_defaults"] project_dir = "../../.." diff --git a/tests/integration/aiodi/test_builder.py b/tests/integration/aiodi/test_builder.py index 845d0f3..1a97f5e 100644 --- a/tests/integration/aiodi/test_builder.py +++ b/tests/integration/aiodi/test_builder.py @@ -20,6 +20,8 @@ def test_container() -> None: assert 'env.log_level' in di and di.get('env.log_level', typ=str) == 'INFO' assert 'env.name' in di and di.get('env.name', typ=str) == 'sample' assert 'env.version' in di and di.get('env.version', typ=int) == 1 + assert 'env.debug' in di and di.get('env.debug', typ=bool) is False + assert 'env.text' in di and di.get('env.text', typ=str) == 'Hello World' assert 'UserLogger' in di and di.get('UserLogger', typ=InMemoryUserLogger) assert 'logging.Logger' in di and di.get(Logger)