-
Notifications
You must be signed in to change notification settings - Fork 3
Introduce ActivityDefinition as a wrapper for Activity Fns #29
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,14 @@ | ||
import inspect | ||
from abc import ABC, abstractmethod | ||
from contextlib import contextmanager | ||
from contextvars import ContextVar | ||
from dataclasses import dataclass | ||
from datetime import timedelta, datetime | ||
from typing import Iterator | ||
from enum import Enum | ||
from functools import update_wrapper | ||
from inspect import signature, Parameter | ||
from typing import Iterator, TypedDict, Unpack, Callable, Type, ParamSpec, TypeVar, Generic, get_type_hints, \ | ||
Any, overload | ||
|
||
from cadence import Client | ||
|
||
|
@@ -59,3 +64,99 @@ def is_set() -> bool: | |
@staticmethod | ||
def get() -> 'ActivityContext': | ||
return ActivityContext._var.get() | ||
|
||
|
||
@dataclass(frozen=True) | ||
class ActivityParameter: | ||
name: str | ||
type_hint: Type | None | ||
default_value: Any | None | ||
|
||
class ExecutionStrategy(Enum): | ||
ASYNC = "async" | ||
THREAD_POOL = "thread_pool" | ||
|
||
class ActivityDefinitionOptions(TypedDict, total=False): | ||
name: str | ||
|
||
P = ParamSpec('P') | ||
T = TypeVar('T') | ||
|
||
class ActivityDefinition(Generic[P, T]): | ||
def __init__(self, wrapped: Callable[P, T], name: str, strategy: ExecutionStrategy, params: list[ActivityParameter]): | ||
self._wrapped = wrapped | ||
self._name = name | ||
self._strategy = strategy | ||
self._params = params | ||
update_wrapper(self, wrapped) | ||
|
||
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: | ||
return self._wrapped(*args, **kwargs) | ||
|
||
@property | ||
def name(self) -> str: | ||
return self._name | ||
|
||
@property | ||
def strategy(self) -> ExecutionStrategy: | ||
return self._strategy | ||
|
||
@property | ||
def params(self) -> list[ActivityParameter]: | ||
return self._params | ||
|
||
@staticmethod | ||
def wrap(fn: Callable[P, T], opts: ActivityDefinitionOptions) -> 'ActivityDefinition[P, T]': | ||
name = fn.__qualname__ | ||
if "name" in opts and opts["name"]: | ||
name = opts["name"] | ||
|
||
strategy = ExecutionStrategy.THREAD_POOL | ||
if inspect.iscoroutinefunction(fn) or inspect.iscoroutinefunction(fn.__call__): # type: ignore | ||
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. if fn is enforced as a callable, what's the difference between those two if conditions? 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. It covers some really weird edge cases. This PR to CPython actually has good examples: https://github.com/python/cpython/pull/99247/files
I don't really know why it works this way, but it does. The only likely scenario for someone to run into an issue with this is if they have other decorators on the function. |
||
strategy = ExecutionStrategy.ASYNC | ||
|
||
params = _get_params(fn) | ||
return ActivityDefinition(fn, name, strategy, params) | ||
|
||
|
||
ActivityDecorator = Callable[[Callable[P, T]], ActivityDefinition[P, T]] | ||
|
||
@overload | ||
def defn(fn: Callable[P, T]) -> ActivityDefinition[P, T]: | ||
... | ||
|
||
@overload | ||
def defn(**kwargs: Unpack[ActivityDefinitionOptions]) -> ActivityDecorator: | ||
... | ||
|
||
def defn(fn: Callable[P, T] | None = None, **kwargs: Unpack[ActivityDefinitionOptions]) -> ActivityDecorator | ActivityDefinition[P, T]: | ||
options = ActivityDefinitionOptions(**kwargs) | ||
def decorator(inner_fn: Callable[P, T]) -> ActivityDefinition[P, T]: | ||
return ActivityDefinition.wrap(inner_fn, options) | ||
|
||
if fn is not None: | ||
return decorator(fn) | ||
|
||
return decorator | ||
|
||
|
||
def _get_params(fn: Callable) -> list[ActivityParameter]: | ||
args = signature(fn).parameters | ||
hints = get_type_hints(fn) | ||
result = [] | ||
for name, param in args.items(): | ||
# "unbound functions" aren't a thing in the Python spec. Filter out the self parameter and hope they followed | ||
# the convention. | ||
if param.name == "self": | ||
continue | ||
default = None | ||
if param.default != Parameter.empty: | ||
default = param.default | ||
if param.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD): | ||
type_hint = hints.get(name, None) | ||
result.append(ActivityParameter(name, type_hint, default)) | ||
|
||
else: | ||
raise ValueError(f"Parameters must be positional. {name} is {param.kind}, and not valid") | ||
|
||
return result |
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.
nit: naming, are we going to have more than 2 types of enum here? if not, perhaps a naming of isAsync might be more descriptive here.
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 there's a chance we might add multiprocessing support in the future.