Fix: make python 3.12 type X = ... syntax work with dependency injection #2951
Fix: make python 3.12 type X = ... syntax work with dependency injection #2951StefanBRas wants to merge 4 commits intohatchet-dev:mainfrom
type X = ... syntax work with dependency injection #2951Conversation
|
@StefanBRas is attempting to deploy a commit to the Hatchet Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Pull request overview
This PR updates the Python SDK’s dependency injection type-introspection to support Python 3.12 type X = ... aliases (runtime TypeAliasType) by unwrapping to the underlying __value__.
Changes:
- Unwrap
TypeAliasTypeannotations during dependency parsing so DI can readAnnotated[...]metadata. - Update the DI example to use a type alias for an injected dependency.
- Adjust async function detection to use
inspect.iscoroutinefunction.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| sdks/python/hatchet_sdk/runnables/types.py | Changes coroutine-function detection implementation. |
| sdks/python/hatchet_sdk/runnables/task.py | Adds handling for TypeAliasType by unwrapping to __value__ before DI parsing. |
| sdks/python/examples/dependency_injection/worker.py | Demonstrates DI with a type alias for an Annotated[..., Depends(...)] parameter. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| chained_async_dep: str | ||
|
|
||
|
|
||
| type AsyncDep = Annotated[str, Depends(async_dep)] |
There was a problem hiding this comment.
Using PEP 695 type AsyncDep = ... syntax makes this example file a SyntaxError on Python < 3.12. If the SDK/examples are intended to run on older supported Python versions, switch to a backwards-compatible alias (e.g., assignment-based alias, optionally with TypeAlias) instead of the type statement, or gate the example so it’s only executed on 3.12+.
| type AsyncDep = Annotated[str, Depends(async_dep)] | |
| AsyncDep: TypeAlias = Annotated[str, Depends(async_dep)] |
There was a problem hiding this comment.
@StefanBRas it'd be worth testing this on 3.10 and 3.11 to confirm, because we'll need to handle this case if this is correct
There was a problem hiding this comment.
We can handle TypeAlias on any version as well, but I wasn't sure how you want to organize the tests since the type X = ... is a syntax error on <3.12 so it is not even possible to import a module with the syntax.
I'm thinking to just add a module with the type that is then conditionally imported depending on the version.
Do you want a test that is just skipped with pytest skipped if version is <3.12 or just that the test only uses type syntax if >=3.12?
There was a problem hiding this comment.
we have this in utils/typing.py - something similar might help here too?
if sys.version_info >= (3, 12):
AwaitableLike: TypeAlias = Awaitable[_T_co]
CoroutineLike: TypeAlias = Coroutine[Any, Any, _T_co]
else:
AwaitableLike: TypeAlias = Generator[Any, None, _T_co] | Awaitable[_T_co]
CoroutineLike: TypeAlias = Generator[Any, None, _T_co] | Coroutine[Any, Any, _T_co]There was a problem hiding this comment.
Unfortunately that's not enough:
~/tmp/test_type$ cat test.py
import sys
from typing import Any, Awaitable, Coroutine, TypeAlias, Generator
if sys.version_info >= (3, 12):
type AwaitableLike = Awaitable[Any]
type CoroutineLike = Coroutine[Any, Any, Any]
else:
AwaitableLike: TypeAlias = Generator[Any, None, Any] | Awaitable[Any]
CoroutineLike: TypeAlias = Generator[Any, None, Any] | Coroutine[Any, Any, Any]
print("No errors")
~/tmp/test_type$ uv run --python 3.12 python test.py
No errors
~/tmp/test_type$ uv run --python 3.10 python test.py
File "/home/sras/tmp/test_type/test.py", line 5
type AwaitableLike = Awaitable[Any]
^^^^^^^^^^^^^
SyntaxError: invalid syntaxThe syntax can't be in the file at all, not even in a class scope.
Maybe it's possible to construct the TypeAliasType object itself without the syntax, but that feels more brittle.
My idea is to have something like this:
~/tmp/test_type$ cat test_split.py
import sys
from typing import Any, Awaitable, Coroutine
if sys.version_info >= (3, 12):
from test_312 import AwaitableLike, CoroutineLike
else:
from test_310 import AwaitableLike, CoroutineLike
print("No errors")
~/tmp/test_type$ cat test_310.py
from typing import Any, Awaitable, Coroutine, Generator, TypeAlias
AwaitableLike: TypeAlias = Generator[Any, None, Any] | Awaitable[Any]
CoroutineLike: TypeAlias = Generator[Any, None, Any] | Coroutine[Any, Any, Any]
~/tmp/test_type$ cat test_312.py
from typing import Any, Awaitable, Coroutine
type AwaitableLike = Awaitable[Any]
type CoroutineLike = Coroutine[Any, Any, Any]
~/tmp/test_type$ uv run --python 3.12 python test_split.py
No errors
~/tmp/test_type$ uv run --python 3.10 python test_split.py
No errors
~/tmp/test_type$| _i: EmptyModel, | ||
| ctx: Context, | ||
| async_dep: Annotated[str, Depends(async_dep)], | ||
| async_dep: AsyncDep, |
There was a problem hiding this comment.
Using PEP 695 type AsyncDep = ... syntax makes this example file a SyntaxError on Python < 3.12. If the SDK/examples are intended to run on older supported Python versions, switch to a backwards-compatible alias (e.g., assignment-based alias, optionally with TypeAlias) instead of the type statement, or gate the example so it’s only executed on 3.12+.
| get_origin, | ||
| get_type_hints, | ||
| ) | ||
| from typing_extensions import TypeAliasType |
There was a problem hiding this comment.
The runtime class for type X = ... in Python 3.12 is typing.TypeAliasType. Depending on how typing_extensions.TypeAliasType is implemented in the installed version, isinstance(annotation, TypeAliasType) can be brittle across environments (e.g., if it’s not the same class object). Prefer importing TypeAliasType from typing when available with a fallback to typing_extensions, or use a small compatibility check that handles both stdlib and typing_extensions implementations.
| from typing_extensions import TypeAliasType | |
| try: | |
| from typing import TypeAliasType # type: ignore[attr-defined] | |
| except ImportError: # pragma: no cover - fallback for older Python versions | |
| from typing_extensions import TypeAliasType |
| if isinstance(annotation, TypeAliasType): | ||
| annotation = annotation.__value__ |
There was a problem hiding this comment.
The runtime class for type X = ... in Python 3.12 is typing.TypeAliasType. Depending on how typing_extensions.TypeAliasType is implemented in the installed version, isinstance(annotation, TypeAliasType) can be brittle across environments (e.g., if it’s not the same class object). Prefer importing TypeAliasType from typing when available with a fallback to typing_extensions, or use a small compatibility check that handles both stdlib and typing_extensions implementations.
| fn: TaskFunc[TWorkflowInput, R], | ||
| ) -> TypeGuard[AsyncFunc[TWorkflowInput, R]]: | ||
| return asyncio.iscoroutinefunction(fn) | ||
| return inspect.iscoroutinefunction(fn) |
There was a problem hiding this comment.
asyncio.iscoroutinefunction() and inspect.iscoroutinefunction() are not equivalent: asyncio.iscoroutinefunction() also recognizes some asyncio-specific coroutine markers (e.g., generator-based coroutines / functions marked via _is_coroutine). Replacing it with inspect.iscoroutinefunction() can regress detection for those callables. Consider reverting to asyncio.iscoroutinefunction(fn) (or otherwise preserving asyncio’s extended detection) unless you explicitly want to drop support for those cases.
Description
For the python SDK, the dependency injection framework does some type introspection to figure out the annotation and get the dependency.
This fails for types created with
type X = ...which at runtime is aTypeAliasTypewhere the actual type is available ont.__value__.Sorry, I again just took the liberty of creating the PR without an issue but it's easier to explain with the code.
I'm not sure how to do the test. I think the type alias needs to be in two different files and then you import one depending on
sys.version_info. But didn't want to go too far that way until I got some commentsYou can verify the fix by removing the change and then see that the test now fails
Type of change
What's Changed
TypeAliasTypecreated with python 3.12type X = ...syntax in dependency injection framework.