-
Notifications
You must be signed in to change notification settings - Fork 0
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
Add Async Support #49
Changes from 20 commits
a3c36ed
2e52bc9
97eab3c
772dc1f
08b3348
50ac88c
16336bc
6108222
2cf7794
4ce0c74
41c9a4e
838d369
173156c
a47911c
08400a9
6f8bd63
2e24f28
0d2245c
82a1332
d2d655a
b53d559
22c73fb
f00b433
5f57733
9ab7706
77af2de
234b9c9
da92ce9
2736a50
db4b93d
991f9f5
16c1db4
48056b8
acba95d
5e2d462
5f655c5
1720159
cfe4dac
dd19c1f
cc4c5e9
ee30e3c
8e7d796
f18dd14
cd13e39
9e64c57
dabb7da
ae6c0d3
537b1a2
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 |
---|---|---|
|
@@ -2,22 +2,36 @@ | |
|
||
import itertools | ||
import os | ||
from typing import Any, Callable, Dict, Optional, Sequence, Type, TypeVar, Union, cast, overload | ||
from typing import ( | ||
Any, | ||
Callable, | ||
Dict, | ||
Optional, | ||
Sequence, | ||
Type, | ||
TypeVar, | ||
Union, | ||
cast, | ||
overload, | ||
) | ||
|
||
from typing_extensions import TypeGuard | ||
|
||
from .metadata import RegistryMetadata, _gen_meta, _get_meta | ||
from minject.types import _AsyncContext | ||
|
||
from .metadata import _INJECT_METADATA_ATTR, RegistryMetadata, _gen_meta, _get_meta | ||
from .model import ( | ||
Deferred, | ||
DeferredAny, | ||
RegistryKey, # pylint: disable=unused-import | ||
Resolver, | ||
resolve_value, | ||
) | ||
from .types import _MinimalMappingProtocol | ||
from .types import _AsyncContext, _MinimalMappingProtocol | ||
|
||
T = TypeVar("T") | ||
T_co = TypeVar("T_co", covariant=True) | ||
T_async_context = TypeVar("T_async_context", bound=_AsyncContext) | ||
R = TypeVar("R") | ||
|
||
|
||
|
@@ -70,6 +84,19 @@ def wrap(cls: Type[T]) -> Type[T]: | |
return wrap | ||
|
||
|
||
def async_context(cls: Type[T_async_context]) -> Type[T_async_context]: | ||
""" | ||
Decorator to declare that a class is as an async context manager | ||
that can be initialized by the registry through aget(). This | ||
is to distinguish the class from an async context manager that | ||
should not be initialized by the registry (an example of | ||
this being asyncio.Lock()). | ||
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. (nit) Drop the parens after |
||
""" | ||
meta = _gen_meta(cls) | ||
meta.update_async_context(True) | ||
return cls | ||
|
||
|
||
def define( | ||
base_class: Type[T], | ||
_close: Optional[Callable[[T], None]] = None, | ||
|
@@ -91,6 +118,24 @@ def _is_type(key: "RegistryKey[T]") -> TypeGuard[Type[T]]: | |
return isinstance(key, type) | ||
|
||
|
||
def _is_key_async(key: RegistryKey) -> bool: | ||
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. (not sure) It looks like 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. Good eye! I am not sure either. I ran a little test and the following code snippet passes from typing import Generic, TypeVar
T = TypeVar('T')
class GenericType(Generic[T]):
def __init__(self, value: T) -> None:
self.value = value
def get_value(self) -> T:
return self.value
def test_typing(g : GenericType) -> bool:
return bool(g.get_value()) Adding a param to My guess is that by default Generic types default to specifying an I'll update the code to explicitly specify Update: Python 3.7 does not support subscripted generics in isinstance checks. Leaving TypeVar in however as I don't think it hurts anything. |
||
""" | ||
Check whether a registry key is an async context manager | ||
marked for initialization within the registry (@async_context). | ||
""" | ||
if isinstance(key, str): | ||
return False | ||
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. A comment for the Actually, does this need to be a separate function at all? In
So it seems like we could inline this into 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've removed the extra The larger edit to remove I've made an issue documenting this suggestion, I think it could make sense to group this with other Refactor suggestions and go through them together when we have some free time (most likely next grease-week/hackathon). |
||
elif isinstance(key, RegistryMetadata): | ||
return key.is_async_context() | ||
elif isinstance(key, type): | ||
try: # 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. Comments explaining the reasons for the (Could we avoid the need for the |
||
inject_metadata = object.__getattribute__(key, _INJECT_METADATA_ATTR) | ||
return inject_metadata.is_async_context() # type: ignore | ||
except AttributeError: | ||
return False | ||
assert False, f"Unexpected key type: {key}" | ||
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. (nit) Maybe raise a Or, better still, change the last 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. Much cleaner! 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. Using assert for control flow in the library can lead to issues since assertions can be disabled with the -O flag, which may result in this check being overlooked. 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. Fixed in #49 (comment) |
||
|
||
|
||
class _RegistryReference(Deferred[T_co]): | ||
"""Reference to an object in the registry to be loaded later. | ||
(you should not instantiate this class directly, instead use the | ||
|
@@ -103,6 +148,13 @@ def __init__(self, key: "RegistryKey[T_co]") -> None: | |
def resolve(self, registry_impl: Resolver) -> T_co: | ||
return registry_impl.resolve(self._key) | ||
|
||
async def aresolve(self, registry_impl: Resolver) -> T_co: | ||
if _is_key_async(self._key): | ||
result = await registry_impl.aresolve(self._key) | ||
await registry_impl.push_async_context(result) | ||
return result | ||
return registry_impl.resolve(self._key) | ||
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. This assumes that the all Should we call this using 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. Interesting problem, I'm leaning toward "don't run in Here is my thinking:
I'm not sure what I think anymore. Gonna sit on this for a bit 🤔. 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. @xiyan128 also commented on this here: #49 (comment) 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. So, I think calling The alternative, for now, would be to have 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've moved all synchronous I'm slightly nervous that this could encourage people to run blocking code within their async classes ( 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. question: when the key is not async, we still resolve it by calling the blocking function. Not sure if we discussed it somewhere else, but should we
*realized we do throw RegistryAPIError for aget, and so this probably for implementation purpose. 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. Let's merge this discussion with #49 (comment) . I think these are largely getting at the same thing. Let me know if you disagree though! |
||
|
||
@property | ||
def type_of_object_referenced_in_key(self) -> "Type[T_co]": | ||
if type(self.key) == RegistryMetadata: | ||
|
@@ -188,6 +240,9 @@ def resolve(self, registry_impl: Resolver) -> T_co: | |
kwargs[key] = resolve_value(registry_impl, arg) | ||
return self.func()(*args, **kwargs) | ||
|
||
async def aresolve(self, registry_impl: Resolver) -> T_co: | ||
raise NotImplementedError("Have not implemented async registry function") | ||
|
||
def func(self) -> Callable[..., T_co]: | ||
return self._func | ||
|
||
|
@@ -273,6 +328,9 @@ def resolve(self, registry_impl: Resolver) -> T_co: | |
else: | ||
return cast(T_co, self._default) | ||
|
||
async def aresolve(self, registry_impl: Resolver) -> T_co: | ||
return self.resolve(registry_impl) | ||
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. (not sure) Can 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. Merging this discussion with similar discussion: #49 (comment) |
||
|
||
@property | ||
def key(self) -> Optional[str]: | ||
return self._key | ||
|
@@ -316,6 +374,9 @@ def resolve(self, registry_impl: Resolver) -> T_co: | |
return self._default | ||
return cast(T_co, sub) | ||
|
||
async def aresolve(self, registry_impl: Resolver) -> T_co: | ||
return self.resolve(registry_impl) | ||
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. (not sure) Can the evaluation of 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'm not sure either. I don't think I have the bandwidth/necessary system call knowledge to investigate this in detail, so I have wrapped all async -> sync handoff points in |
||
|
||
|
||
def nested_config( | ||
keys: Union[Sequence[str], str], default: Union[T, _RaiseKeyError] = RAISE_KEY_ERROR | ||
|
@@ -375,5 +436,8 @@ class _RegistrySelf(Deferred[Resolver]): | |
def resolve(self, registry_impl: Resolver) -> Resolver: | ||
return registry_impl | ||
|
||
async def aresolve(self, registry_impl: Resolver) -> Resolver: | ||
return self.resolve(registry_impl) | ||
|
||
|
||
self_tag = _RegistrySelf() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -112,6 +112,7 @@ def __init__( | |
|
||
self._close = close | ||
self._interfaces = [cls for cls in inspect.getmro(cls) if cls is not object] | ||
self._is_async_context = False | ||
|
||
@property | ||
def interfaces(self) -> Sequence[Type]: | ||
|
@@ -142,6 +143,18 @@ def update_bindings(self, **bindings: DeferredAny) -> None: | |
# TODO: 'lock' the bindings once added to the registry to make above note unnecessary | ||
self._bindings.update(bindings) | ||
|
||
def update_async_context(self, is_async_context: bool) -> None: | ||
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. nit: this and the next function just update |
||
""" | ||
Set the async context flag for this metadata. | ||
""" | ||
self._is_async_context = is_async_context | ||
|
||
def is_async_context(self) -> bool: | ||
""" | ||
Returns the value of the async context flag for this metadata. | ||
""" | ||
return self._is_async_context | ||
|
||
def _new_object(self) -> T_co: | ||
return self._cls.__new__(self._cls) | ||
|
||
|
@@ -159,6 +172,17 @@ def _init_object(self, obj: T_co, registry_impl: "Registry") -> None: # type: i | |
|
||
self._cls.__init__(obj, **init_kwargs) | ||
|
||
async def _ainit_object(self, obj: T_co, registry_impl: "Registry") -> None: # type: ignore[misc] | ||
""" | ||
asynchronous version of _init_object. Calls _aresolve instead | ||
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.
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've made an issue to improve documentation for the 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 agree that in general we can defer fixes for future maintainability, but In this case I'm requesting the doc comment in order to understand the code as a reviewer right now. I'm having a hard time following the invariants around when the async objects are in what state, and having a more complete picture of what it means for an object to be “initialized” would help me better understand the rest of the code without having to spend as much time reverse-engineering it. After spending some time digging into it, it appears that |
||
of _resolve. | ||
""" | ||
init_kwargs = {} | ||
for name_, value in self._bindings.items(): | ||
# the specific deferred value checks if they are async or | ||
init_kwargs[name_] = await registry_impl._aresolve(value) | ||
self._cls.__init__(obj, **init_kwargs) | ||
|
||
def _close_object(self, obj: T_co) -> None: # type: ignore[misc] | ||
if self._close: | ||
self._close(obj) | ||
|
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: "declare that a class is an async context manager"