-
Notifications
You must be signed in to change notification settings - Fork 153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generic destination / sink decorator #1065
Changes from 47 commits
8b6028b
6a924d9
83c1af6
b972f23
9dabeff
af6defd
4d730e8
3b577b0
5527689
0657034
b5db5b8
189d24b
57ed090
4f53bc4
c6c06ba
2f6a15a
374b267
daae33e
644e6f3
9930ad6
678d187
376832d
79fce9e
8eb3e1a
105569a
27b8b2c
e60f2f1
3229745
dbbbe7c
db9b488
3c39f41
3dfcf39
bc44618
db8d0ed
d8719c1
d7eb19d
ef35502
2db3430
9582b9f
f488210
4d2563b
12b5cbd
3e5f808
53dc2f9
d1e1e38
943ac3e
1f47a05
b66c653
31b661b
97ed409
246df72
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,15 @@ | ||
import inspect | ||
|
||
from functools import wraps | ||
from typing import Callable, Dict, Type, Any, Optional, Tuple, TypeVar, overload | ||
from typing import Callable, Dict, Type, Any, Optional, Tuple, TypeVar, overload, cast | ||
from inspect import Signature, Parameter | ||
from contextlib import nullcontext | ||
|
||
from dlt.common.typing import DictStrAny, StrAny, TFun, AnyFun | ||
from dlt.common.configuration.resolve import resolve_configuration, inject_section | ||
from dlt.common.configuration.specs.base_configuration import BaseConfiguration | ||
from dlt.common.configuration.specs.config_section_context import ConfigSectionContext | ||
|
||
from dlt.common.reflection.spec import spec_from_signature | ||
|
||
|
||
|
@@ -32,6 +35,8 @@ def with_config( | |
auto_pipeline_section: bool = False, | ||
include_defaults: bool = True, | ||
accept_partial: bool = False, | ||
initial_config: BaseConfiguration = None, | ||
base: Type[BaseConfiguration] = BaseConfiguration, | ||
) -> TFun: ... | ||
|
||
|
||
|
@@ -45,6 +50,8 @@ def with_config( | |
auto_pipeline_section: bool = False, | ||
include_defaults: bool = True, | ||
accept_partial: bool = False, | ||
initial_config: Optional[BaseConfiguration] = None, | ||
base: Type[BaseConfiguration] = BaseConfiguration, | ||
) -> Callable[[TFun], TFun]: ... | ||
|
||
|
||
|
@@ -58,6 +65,7 @@ def with_config( | |
include_defaults: bool = True, | ||
accept_partial: bool = False, | ||
initial_config: Optional[BaseConfiguration] = None, | ||
base: Type[BaseConfiguration] = BaseConfiguration, | ||
) -> Callable[[TFun], TFun]: | ||
"""Injects values into decorated function arguments following the specification in `spec` or by deriving one from function's signature. | ||
|
||
|
@@ -71,10 +79,11 @@ def with_config( | |
prefer_existing_sections: (bool, optional): When joining existing section context, the existing context will be preferred to the one in `sections`. Default: False | ||
auto_pipeline_section (bool, optional): If True, a top level pipeline section will be added if `pipeline_name` argument is present . Defaults to False. | ||
include_defaults (bool, optional): If True then arguments with default values will be included in synthesized spec. If False only the required arguments marked with `dlt.secrets.value` and `dlt.config.value` are included | ||
|
||
base (Type[BaseConfiguration], optional): A base class for synthesized spec. Defaults to BaseConfiguration. | ||
Returns: | ||
Callable[[TFun], TFun]: A decorated function | ||
""" | ||
|
||
section_f: Callable[[StrAny], str] = None | ||
# section may be a function from function arguments to section | ||
if callable(sections): | ||
|
@@ -88,9 +97,8 @@ def decorator(f: TFun) -> TFun: | |
) | ||
spec_arg: Parameter = None | ||
pipeline_name_arg: Parameter = None | ||
|
||
if spec is None: | ||
SPEC = spec_from_signature(f, sig, include_defaults) | ||
SPEC = spec_from_signature(f, sig, include_defaults, base=base) | ||
else: | ||
SPEC = spec | ||
|
||
|
@@ -109,49 +117,52 @@ def decorator(f: TFun) -> TFun: | |
pipeline_name_arg = p | ||
pipeline_name_arg_default = None if p.default == Parameter.empty else p.default | ||
|
||
@wraps(f) | ||
def _wrap(*args: Any, **kwargs: Any) -> Any: | ||
def resolve_config(bound_args: inspect.BoundArguments) -> BaseConfiguration: | ||
"""Resolve arguments using the provided spec""" | ||
# bind parameters to signature | ||
bound_args = sig.bind(*args, **kwargs) | ||
# for calls containing resolved spec in the kwargs, we do not need to resolve again | ||
config: BaseConfiguration = None | ||
if _LAST_DLT_CONFIG in kwargs: | ||
config = last_config(**kwargs) | ||
|
||
# if section derivation function was provided then call it | ||
if section_f: | ||
curr_sections: Tuple[str, ...] = (section_f(bound_args.arguments),) | ||
# sections may be a string | ||
elif isinstance(sections, str): | ||
curr_sections = (sections,) | ||
else: | ||
# if section derivation function was provided then call it | ||
if section_f: | ||
curr_sections: Tuple[str, ...] = (section_f(bound_args.arguments),) | ||
# sections may be a string | ||
elif isinstance(sections, str): | ||
curr_sections = (sections,) | ||
else: | ||
curr_sections = sections | ||
|
||
# if one of arguments is spec the use it as initial value | ||
if initial_config: | ||
config = initial_config | ||
elif spec_arg: | ||
config = bound_args.arguments.get(spec_arg.name, None) | ||
# resolve SPEC, also provide section_context with pipeline_name | ||
if pipeline_name_arg: | ||
curr_pipeline_name = bound_args.arguments.get( | ||
pipeline_name_arg.name, pipeline_name_arg_default | ||
) | ||
else: | ||
curr_pipeline_name = None | ||
section_context = ConfigSectionContext( | ||
pipeline_name=curr_pipeline_name, | ||
sections=curr_sections, | ||
merge_style=sections_merge_style, | ||
curr_sections = sections | ||
|
||
# if one of arguments is spec the use it as initial value | ||
if initial_config: | ||
config = initial_config | ||
elif spec_arg: | ||
config = bound_args.arguments.get(spec_arg.name, None) | ||
# resolve SPEC, also provide section_context with pipeline_name | ||
if pipeline_name_arg: | ||
curr_pipeline_name = bound_args.arguments.get( | ||
pipeline_name_arg.name, pipeline_name_arg_default | ||
) | ||
# this may be called from many threads so section_context is thread affine | ||
with inject_section(section_context): | ||
# print(f"RESOLVE CONF in inject: {f.__name__}: {section_context.sections} vs {sections}") | ||
config = resolve_configuration( | ||
config or SPEC(), | ||
explicit_value=bound_args.arguments, | ||
accept_partial=accept_partial, | ||
) | ||
else: | ||
curr_pipeline_name = None | ||
section_context = ConfigSectionContext( | ||
pipeline_name=curr_pipeline_name, | ||
sections=curr_sections, | ||
merge_style=sections_merge_style, | ||
) | ||
|
||
# this may be called from many threads so section_context is thread affine | ||
with inject_section(section_context, lock_context=True): | ||
# print(f"RESOLVE CONF in inject: {f.__name__}: {section_context.sections} vs {sections}") | ||
return resolve_configuration( | ||
config or SPEC(), | ||
explicit_value=bound_args.arguments, | ||
accept_partial=accept_partial, | ||
) | ||
|
||
def update_bound_args( | ||
bound_args: inspect.BoundArguments, config: BaseConfiguration, args: Any, kwargs: Any | ||
) -> None: | ||
# overwrite or add resolved params | ||
resolved_params = dict(config) | ||
# overwrite or add resolved params | ||
for p in sig.parameters.values(): | ||
|
@@ -167,12 +178,56 @@ def _wrap(*args: Any, **kwargs: Any) -> Any: | |
bound_args.arguments[kwargs_arg.name].update(resolved_params) | ||
bound_args.arguments[kwargs_arg.name][_LAST_DLT_CONFIG] = config | ||
bound_args.arguments[kwargs_arg.name][_ORIGINAL_ARGS] = (args, kwargs) | ||
|
||
def with_partially_resolved_config(config: Optional[BaseConfiguration] = None) -> Any: | ||
# creates a pre-resolved partial of the decorated function | ||
empty_bound_args = sig.bind_partial() | ||
if not config: | ||
config = resolve_config(empty_bound_args) | ||
|
||
def wrapped(*args: Any, **kwargs: Any) -> Any: | ||
nonlocal config | ||
|
||
# Do we need an exception here? | ||
if spec_arg and spec_arg.name in kwargs: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not think we care at all? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, but in the case of partially resolving, the resolve_config is called before even the func args are available, do you know what i mean? It's a bit complicated but also quite clear. We can also remove it if you like, it's just a warning that made sense to put in given the mechanics of the code. |
||
from dlt.common import logger | ||
|
||
logger.warning( | ||
"Spec argument is provided in kwargs, ignoring it for resolved partial" | ||
" function." | ||
) | ||
|
||
# we can still overwrite the config | ||
if _LAST_DLT_CONFIG in kwargs: | ||
config = last_config(**kwargs) | ||
|
||
# call the function with the pre-resolved config | ||
bound_args = sig.bind(*args, **kwargs) | ||
update_bound_args(bound_args, config, args, kwargs) | ||
return f(*bound_args.args, **bound_args.kwargs) | ||
|
||
return wrapped | ||
|
||
@wraps(f) | ||
def _wrap(*args: Any, **kwargs: Any) -> Any: | ||
rudolfix marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# Resolve config | ||
config: BaseConfiguration = None | ||
bound_args = sig.bind(*args, **kwargs) | ||
if _LAST_DLT_CONFIG in kwargs: | ||
config = last_config(**kwargs) | ||
else: | ||
config = resolve_config(bound_args) | ||
|
||
# call the function with resolved config | ||
update_bound_args(bound_args, config, args, kwargs) | ||
return f(*bound_args.args, **bound_args.kwargs) | ||
|
||
# register the spec for a wrapped function | ||
_FUNC_SPECS[id(_wrap)] = SPEC | ||
|
||
# add a method to create a pre-resolved partial | ||
setattr(_wrap, "__RESOLVED_PARTIAL_FUNC__", with_partially_resolved_config) # noqa: B010 | ||
|
||
return _wrap # type: ignore | ||
|
||
# See if we're being called as @with_config or @with_config(). | ||
|
@@ -197,3 +252,10 @@ def last_config(**kwargs: Any) -> Any: | |
|
||
def get_orig_args(**kwargs: Any) -> Tuple[Tuple[Any], DictStrAny]: | ||
return kwargs[_ORIGINAL_ARGS] # type: ignore | ||
|
||
|
||
def create_resolved_partial(f: AnyFun, config: Optional[BaseConfiguration] = None) -> AnyFun: | ||
"""Create a pre-resolved partial of the with_config decorated function""" | ||
if partial_func := getattr(f, "__RESOLVED_PARTIAL_FUNC__", None): | ||
return cast(AnyFun, partial_func(config)) | ||
return f |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we have a "thread_safe" parameter so we only lock on decorated functions where we know that there might be issues?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we will always lock when resolving configuration so not optional