Skip to content

Commit

Permalink
Injection draft
Browse files Browse the repository at this point in the history
  • Loading branch information
kmagusiak committed Dec 3, 2023
1 parent af440bd commit 1807d2a
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 33 deletions.
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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/
Expand Down
60 changes: 60 additions & 0 deletions alphaconf/inject.py
Original file line number Diff line number Diff line change
@@ -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
26 changes: 13 additions & 13 deletions alphaconf/internal/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def setup_configuration(
conf: Union[DictConfig, dict, Any],
helpers: Dict[str, str] = {},
*,
path: str = "",
prefix: str = "",
):
"""Add a default configuration
Expand All @@ -146,27 +146,27 @@ 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)
if not isinstance(created_config, DictConfig):
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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
14 changes: 5 additions & 9 deletions alphaconf/internal/type_resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'):
Expand All @@ -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(),
}

Expand Down
2 changes: 1 addition & 1 deletion example-typed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion tests/test_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion tests/test_configuration_typed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
9 changes: 9 additions & 0 deletions tests/test_inject.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 1807d2a

Please sign in to comment.