-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Making a type alias of a Callable that describes a decorator discards all type information #18842
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
Comments
I'm not sure whether this is a bug or intended behavior. Let's look at the decorator: @deco()
def fn() -> None: ...
# is the same as
deco_callable = deco() # We don't know the type here - neither P nor R appeared so far!
def fn() -> None: ...
fn = deco_callable(fn) # So we fail here - P and R should've already been bound, but there is not enough info beforehand Your Protocol approach is almost there. If you move the type vars to the appropriate scope (only from typing import *
type Fn[**P, R] = Callable[P, R]
class Decorator(Protocol):
def __call__[**P, R](self, fn: Fn[P, R], /) -> Fn[P, R]: ...
def do_the_thing() -> Decorator:
def decorator[**P, R](fn: Fn[P, R]) -> Fn[P, R]:
return fn
return decorator
@do_the_thing()
def blahblah(x: str) -> None:
pass
reveal_type(blahblah) # N: Revealed type is "def (x: builtins.str)" It isn't correct to say that a I'm not sure how to express that without protocol, unfortunately - let's wait for someone else to comment on that? There doesn't seem to be any way to propagate generics like you want, you need
so that it isn't generic itself, but binds typevars internally. I doubt it can be expressed without an explicit Protocol. |
Looking at this from the perspective of the typing spec, I think the code sample in the original repro should type check without error. As the OP notes, it type checks fine if you manually expand the type alias. Using a type alias shouldn't change the behavior. For comparison, it type checks fine in pyright. |
Do I understand correctly that the spec requires that either both versions pass or they are both rejected, only mandating consistency? I can't find anything about such typevar propagation in In other words, according to the spec, does And how about |
Yeah, typevar binding for The typing spec is less ambiguous (although admittedly not as clear as it could be) regarding type aliases. A type alias substitution shouldn't change the behavior. I think that means both versions should either pass or fail. |
Yes, I agree that aliases should be treated in the same way, this part is indeed a bug - snippets with and without alias should be equivalent. I learned only recently that the version without an alias works at all - IMO it's just wrong or at least ill-defined, while the explicit Protocol is an unambiguous solution. Are there any cases where a long-ish |
I can't think of any off the top of my head. However, there's unfortunately quite a bit of code that relies on the special-case behavior that was initially introduced by mypy (presumably before |
Protocols in their current form require explicitly importing typing and have runtime evaluation costs, type alias statements are deferred and can be written in a way that the expression is never evaluated unless introspected. Outside of that, they're also a lot more verbose without a reasonable reason, especially in the case of paramspec based callable declarations: # Use below doesn't accept non-task Futures, so can't accept general awaitables
type CoroFunc[**P, R] = Callable[P, Coroutine[t.Any, t.Any, R]]
type TaskFunc[**P, R] = Callable[P, asyncio.Task[R]]
type TaskCoroFunc[**P, R] = CoroFunc[P, R] | TaskFunc[P, R]
type Deco[**P, R] = Callable[[TaskCoroFunc[P, R]], TaskFunc[P, R]]
def taskcache[**P, R](
ttl: float | None = None,
*,
cache_transform: CacheTransformer | None = None,
) -> Deco[P, R]: The equivalent written with protocols and without type aliases doesn't have a meaningful difference: class CoroFunc[**P, R](Protocol):
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Coroutine[t.Any, t.Any, R]:
...
class TaskFunc[**P, R](Protocol):
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> asyncio.Task[R]:
...
class Deco[**P, R](Protocol):
def __call__(self, f: CoroFunc[P, R] | TaskFunc[P, R], /) -> TaskFunc[P, R]:
...
def taskcache[**P, R](
ttl: float | None = None,
*,
cache_transform: CacheTransformer | None = None,
) -> Deco[P, R]: (I've copy pasted this from an existing codebase, CacheTransformer's declaration is left out, but irrelevant here other than that it would show even more verbosity) I don't see any reason the examples here should fail, or why we should force this to require protocols |
@mikeshardmind does that mean this is likely definitely a bug in MyPy as opposed to an issue with how I am approaching this? |
@ascopes I consider this a bug in mypy. As Eric pointed out above:
If you can think of a reason why the two of my examples should not be seen as equivalent to a type checker, or why the latter should fail, then there's a potential argument that there's specification clarity at play, however I don't believe Paramspecs are unclear about their use in Callables or in Protocols currently. |
We failed to expand the alias somewhere - yes, it is a bug, aliases (at least non-recursive) and their expansions should be completely equivalent. But also we aren't required to support pushing unsolvable type variables to the |
Btw, #18877 answers my question about "something doable with Callable but not with Protocol". |
Bug Report
Attempting to make a type alias for a decorator results in MyPy losing type information.
To Reproduce
Seems this is not an artifact of how variance is being calculated with the new type alias syntax as using the old style syntax has the same issue when declaring the type hints...
...and the same issue if I just discard the type variables entirely in the function definition:
Using a protocol doesn't work either:
...so it seems the type information is just totally discarded.
Strangely, if I don't type-alias the decorator, then it works fine (which is a workaround, but very annoying and verbose for more complicated logic).
The problem is that I want to be able to alias
Callable[[Fn[P, R]], Fn[P, R]]
to a clean single identifier that I can reuse in the current type context, but I cannot find a nice way of doing that at the moment.Expected Behavior
This should be valid.
Actual Behavior
Related Issues:
I came across GH-16512, but that was closed as fixed. Not sure if it is the same problem or not!
Your Environment
mypy.ini
(and other config files): noneThe text was updated successfully, but these errors were encountered: