Replies: 2 comments
-
|
This can be explained in two step.
def double(x: int):
return 2 * x
@transformed(double)
#^^^^^^^^^^^^^^^^^^^
#Argument of type "() -> Generator[int, None, None]" cannot be assigned to parameter "f" of type "(**P@decorator) -> int" in function "decorator"
# Type "() -> Generator[int, None, None]" is not assignable to type "(**P@decorator) -> int"
# Function return type "Generator[int, None, None]" is incompatible with type "int"
# "Generator[int, None, None]" is not assignable to "int" (reportArgumentType)
# Argument type is "GeneratorType[Unknown, Unknown, Unknown]*" (reportUnknownArgumentType)
def despite_what_you_think_this_wont_type_check() -> Generator[int]:
for i in range(10):
yield iAlso, the decorator function returns a different type of function depending on the output type, so we also need from collections.abc import Callable, Generator
from typing import TypeVar, ParamSpec, overload
import types
from functools import wraps
T = TypeVar("T")
P = ParamSpec("P")
def imap(transformer: Callable[[T], T], generator: Generator[T]) -> Generator[T]:
for x in generator:
yield transformer(x)
def transformed(transformer: Callable[[T], T]):
@overload
def decorator(f: Callable[P, Generator[T]]) -> Callable[P, Generator[T]]: ...
@overload
def decorator(f: Callable[P, T]) -> Callable[P, T]: ...
def decorator(f: Callable[P, T | Generator[T]]):
@wraps(f)
def decorated(*args: P.args, **kwargs: P.kwargs) -> T | Generator[T]:
result = f(*args, **kwargs)
if isinstance(result, types.GeneratorType):
# E Argument type is partially unknown: # Argument type is "GeneratorType[Unknown, Unknown, Unknown]*" Pyright (reportUnknownArgumentType) [21, 42]
return imap(transformer, result)
# ^^^^^^
#Argument type is partially unknown
# Argument corresponds to parameter "generator" in function "imap"
# Argument type is "GeneratorType[Unknown, Unknown, Unknown]* | GeneratorType[T@transformed, None, None]" (reportUnknownArgumentType)
else:
return transformer(result)
return decorated
return decorator
Unfortunately, this increases the number of errors to 4. I included the most readable one above. What is it saying? Pyright thinks that Two remaining errors result from If you understand that And here are some tangents to end it
|
Beta Was this translation helpful? Give feedback.
-
|
I believe the core issue here is union ambiguity; when # If R = Generator[int], then R | Generator[R] becomes:
result: Generator[int] | Generator[Generator[int]]
# After isinstance(result, Generator), both cases match:
if isinstance(result, Generator): # True for both cases!
# Is this Generator[int] or Generator[Generator[int]]?This explains the Type System Constraints That Block Clean Solutions
Workaround Solution: Separate FunctionsThe type-safe approach requires splitting the decorator: def transformed_value[R, **P](transformer: Callable[[R], R]) -> Callable[
[Callable[P, R]],
Callable[P, R]
]: ...
def transformed_generator[R, **P](transformer: Callable[[R], R]) -> Callable[
[Callable[P, Generator[R]]],
Callable[P, Generator[R]]
]: ...This eliminates all casts and runtime type ambiguity, at the cost of requiring Sufficient Typing ExtensionsThe pattern would be naturally expressible with either:
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
This is a boiled-down toy example from a larger set of requirements in my real project.
This is close as I got on my own:
Desiderata:
If it's not possible to have all these, I'd like to understand why, at least.
Beta Was this translation helpful? Give feedback.
All reactions