diff --git a/README.md b/README.md index 4e1a194..304a12f 100644 --- a/README.md +++ b/README.md @@ -66,10 +66,9 @@ Then configuration is built from: - `PYTHON_ALPHACONF` environment variable may contain a path to load - configuration files from configuration directories (using application name) - environment variables based on key prefixes, - except "BASE" and "PYTHON"; + except "BASE" and "PYTHON"; \ if you have a configuration key "abc", all environment variables starting - with "ABC_" will be loaded where keys are converted to lower case and "_" - to ".": "ABC_HELLO=a" would set "abc.hello=a" + with "ABC_" will be loaded, for example "ABC_HELLO=a" would set "abc.hello=a" - key-values from the program arguments Finally, the configuration is fully resolved and logging is configured. @@ -104,10 +103,11 @@ class MyConf(pydantic.BaseModel): def build(self): # use as a factory pattern to create more complex objects + # for example, a connection to the database return self.value * 2 # setup the configuration -alphaconf.setup_configuration(MyConf, path='a') +alphaconf.setup_configuration(MyConf, prefix='a') # read the value alphaconf.get('a', MyConf) v = alphaconf.get(MyConf) # because it's registered as a type @@ -136,9 +136,8 @@ alphaconf.invoke.run(__name__, ns) ``` ## Way to 1.0 -- Run function `@alphaconf.inject` -- Run a specific function `alphaconf.cli.run_module()`: - find functions and parse their args +- Run a specific function `alphaconf my.module.main`: + find functions and inject args - Install completions for bash `alphaconf --install-autocompletion` [OmegaConf]: https://omegaconf.readthedocs.io/ diff --git a/alphaconf/inject.py b/alphaconf/inject.py new file mode 100644 index 0000000..3093d67 --- /dev/null +++ b/alphaconf/inject.py @@ -0,0 +1,60 @@ +import inspect +from dataclasses import dataclass +from typing import Callable, Dict, TypeVar + +import alphaconf + +R = TypeVar('R') + + +@dataclass +class InjectArgument: + name: str + verify: bool = False + # rtype: type = None # TODO add type transformer + + def get_value(self, type_spec, required): + get_args: dict = {'key': self.name} + if type_spec: + get_args['type'] = type_spec + if not required: + get_args['default'] = None + value = alphaconf.get(**get_args) + return value + + +class Injector: + args: Dict[str, InjectArgument] + prefix: str + + def __init__(self, prefix: str = ""): + if prefix and not prefix.endswith("."): + prefix += "." + self.prefix = prefix + self.args = {} + + def inject(self, name: str, optional, type, resolver): + pass + + def decorate(self, func: Callable[..., R]) -> Callable[[], R]: + signature = inspect.signature(func) + + def call(): + args = {} # TODO {**self.values} + for name, iarg in self.args.items(): + param = signature.parameters.get(name, None) + if not param: + if iarg.verify: + raise TypeError("Missing argument", name) + continue + arg_type = None + if param.annotation is not param.empty and isinstance(param.annotation, type): + arg_type = param.annotation + required = param.default is param.empty + value = iarg.get_value(arg_type, required) + if value is None and not required: + continue + args[name] = value + return func(**args) + + return call diff --git a/alphaconf/internal/configuration.py b/alphaconf/internal/configuration.py index 8da1d1e..ff50048 100644 --- a/alphaconf/internal/configuration.py +++ b/alphaconf/internal/configuration.py @@ -130,7 +130,7 @@ def setup_configuration( conf: Union[DictConfig, dict, Any], helpers: Dict[str, str] = {}, *, - path: str = "", + prefix: str = "", ): """Add a default configuration @@ -146,10 +146,10 @@ def setup_configuration( conf_type = None if conf_type: # if already registered, set path to None - self.__type_path[conf_type] = None if conf_type in self.__type_path else path + self.__type_path[conf_type] = None if conf_type in self.__type_path else prefix self.__type_value.pop(conf_type, None) - if path and not path.endswith('.'): - path += "." + if prefix and not prefix.endswith('.'): + prefix += "." if isinstance(conf, str): warnings.warn("provide a dict directly", DeprecationWarning) created_config = OmegaConf.create(conf) @@ -157,16 +157,16 @@ def setup_configuration( raise ValueError("The config is not a dict") conf = created_config if isinstance(conf, DictConfig): - config = self.__prepare_dictconfig(conf, path=path) + config = self.__prepare_dictconfig(conf, path=prefix) else: - created_config = self.__prepare_config(conf, path=path) + created_config = self.__prepare_config(conf, path=prefix) if not isinstance(created_config, DictConfig): raise ValueError("Failed to convert to a DictConfig") config = created_config - # add path and merge - if path: - config = self.__add_path(config, path.rstrip(".")) - helpers = {path + k: v for k, v in helpers.items()} + # add prefix and merge + if prefix: + config = self.__add_prefix(config, prefix.rstrip(".")) + helpers = {prefix + k: v for k, v in helpers.items()} self._merge([config]) # helpers self.helpers.update(**helpers) @@ -226,7 +226,7 @@ def __prepare_dictconfig( v = self.__prepare_config(v, path + k + ".") if '.' in k: obj.pop(k) - sub_configs.append(self.__add_path(v, k)) + sub_configs.append(self.__add_prefix(v, k)) if sub_configs: obj = cast(DictConfig, OmegaConf.unsafe_merge(obj, *sub_configs)) return obj @@ -285,7 +285,7 @@ def __prepare_pydantic(self, obj, path): return None @staticmethod - def __add_path(config: Any, path: str) -> DictConfig: - for part in reversed(path.split(".")): + def __add_prefix(config: Any, prefix: str) -> DictConfig: + for part in reversed(prefix.split(".")): config = OmegaConf.create({part: config}) return config diff --git a/alphaconf/internal/type_resolvers.py b/alphaconf/internal/type_resolvers.py index be49a47..1da4f5f 100644 --- a/alphaconf/internal/type_resolvers.py +++ b/alphaconf/internal/type_resolvers.py @@ -16,11 +16,7 @@ """ -def read_text(value): - return Path(value).expanduser().read_text() - - -def parse_bool(value) -> bool: +def _parse_bool(value) -> bool: if isinstance(value, str): value = value.strip().lower() if value in ('no', 'false', 'n', 'f', 'off', 'none', 'null', 'undefined', '0'): @@ -29,14 +25,14 @@ def parse_bool(value) -> bool: TYPE_CONVERTER = { - bool: parse_bool, + bool: _parse_bool, datetime.datetime: datetime.datetime.fromisoformat, datetime.date: lambda s: datetime.datetime.strptime(s, '%Y-%m-%d').date(), datetime.time: datetime.time.fromisoformat, - Path: lambda s: Path(s).expanduser(), + Path: lambda s: Path(str(s)).expanduser(), str: lambda v: str(v), - 'read_text': read_text, - 'read_strip': lambda s: read_text(s).strip(), + 'read_text': lambda s: Path(s).expanduser().read_text(), + 'read_strip': lambda s: Path(s).expanduser().read_text().strip(), 'read_bytes': lambda s: Path(s).expanduser().read_bytes(), } diff --git a/example-typed.py b/example-typed.py index ece0717..fc2b1bd 100755 --- a/example-typed.py +++ b/example-typed.py @@ -20,7 +20,7 @@ class MyConfiguration(BaseModel): connection: Optional[Conn] = None -alphaconf.setup_configuration(MyConfiguration, path="c") +alphaconf.setup_configuration(MyConfiguration, prefix="c") def main(): diff --git a/pyproject.toml b/pyproject.toml index af22d06..b9a7e56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,13 +23,13 @@ classifiers = [ ] dependencies = [ "omegaconf>=2", - "pydantic>=2", ] [project.optional-dependencies] color = ["colorama"] dotenv = ["python-dotenv"] invoke = ["invoke"] +pydantic = ["pydantic>=2"] toml = ["toml"] [project.urls] diff --git a/tests/test_configuration.py b/tests/test_configuration.py index b9386f0..bca02fb 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -132,5 +132,5 @@ def test_config_setup_dots(config): def test_config_setup_path(config): - config.setup_configuration({'test': 954}, path='a.b') + config.setup_configuration({'test': 954}, prefix='a.b') assert config.get('a.b.test') == 954 diff --git a/tests/test_configuration_typed.py b/tests/test_configuration_typed.py index 3f1808d..5b55097 100644 --- a/tests/test_configuration_typed.py +++ b/tests/test_configuration_typed.py @@ -95,6 +95,6 @@ def test_set_person(config_typed): def test_set_person_type(config_typed): - config_typed.setup_configuration(Person(first_name='A', last_name='T'), path='x_person') + config_typed.setup_configuration(Person(first_name='A', last_name='T'), prefix='x_person') person = config_typed.get(Person) assert person.full_name == 'A T' diff --git a/tests/test_inject.py b/tests/test_inject.py new file mode 100644 index 0000000..17f0f2f --- /dev/null +++ b/tests/test_inject.py @@ -0,0 +1,9 @@ +import alphaconf +from alphaconf.inject import Injector + + +def test_inject(): + alphaconf.setup_configuration({'a': 5}) + alphaconf.set_application(alphaconf.Application()) + v = Injector().inject('a').decorate(lambda a: a + 1) + assert v() == 6