python / python/mypy

Case of generic decorators not working on generic callables (ParamSpec)

Aperta
#17,621 2 commenti 1 reazione 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

bug topic-paramspec
Lingua principale
Python
Stelle
20.6k
Fork
3.3k
Metriche di merge delle PR
Metriche PR in attesa

Descrizione

This describes an issue where a generic decorator that returns a generic sub-type of a "callable" (using __call__ and ParamSpec) cannot be applied to a generic function. In the example below, Callable[_P, _R] -> Traceable[_P, _R] works, but Callable[_P, _R] -> Decorated[_P, _R] does not. It seems to work if either the decorated function is not generic (E.G. radius() instead of apply() in the example), or if the return type of the decorator is a super-type of its argument (E.G. Traceable instead of Decorated).

Relevant closed issues

  • #15837
  • #15287
  • #1317

To Reproduce

from collections.abc import Callable
from typing_extensions import (
  TypeVar,
  ParamSpec,
  Generic,
  Protocol,
  reveal_type)

_P = ParamSpec('_P')
_R = TypeVar('_R', covariant=True)

_P2 = ParamSpec('_P2')
_R2 = TypeVar('_R2', covariant=True)

class Traceable(Protocol[_P, _R]):
  def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...

class Decorated(Traceable[_P, _R]):
  target: Traceable[_P, _R]

  def __init__(self, target: Traceable[_P, _R]):
    self.target = target

  def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R:
    return self.target(*args, **kwargs)

def decorator1(target: Callable[_P, _R]) -> Traceable[_P, _R]:
  return Decorated(target)

def decorator2(target: Traceable[_P, _R]) -> Decorated[_P, _R]:
  return Decorated(target)

def apply(
    func: Callable[_P2, _R2],
    *args: _P2.args,
    **kwargs: _P2.kwargs) -> _R2:

  return func(*args, **kwargs)

@decorator1
def apply_decorated1(
    func: Callable[_P2, _R2],
    *args: _P2.args,
    **kwargs: _P2.kwargs) -> _R2:

  return func(*args, **kwargs)

@decorator2 # error: Argument 1 to "decorator2" has incompatible type "Callable[[Callable[_P2, _R2], **_P2], _R2]"; expected "Traceable[Never, Never]"  [arg-type]
def apply_decorated2(
    func: Callable[_P2, _R2],
    *args: _P2.args,
    **kwargs: _P2.kwargs) -> _R2:

  return func(*args, **kwargs)

@decorator2
def radius(x: float, y: float) -> float:
  return (x**2 + y**2)**0.5

reveal_type(apply) # Revealed type is "def [_P2, _R2] (func: def (*_P2.args, **_P2.kwargs) -> _R2`-2, *_P2.args, **_P2.kwargs) -> _R2`-2"
reveal_type(apply_decorated1) # Revealed type is "def [_P2, _R] (func: def (*_P2.args, **_P2.kwargs) -> _R`6, *_P2.args, **_P2.kwargs) -> _R`6"
reveal_type(apply_decorated2) # Revealed type is "typehint_decorator.Decorated[Never, Never]"
reveal_type(radius) # Revealed type is "typehint_decorator.Decorated[[x: builtins.float, y: builtins.float], builtins.float]"

r00 = radius(1.0, 0.0)
reveal_type(r00) # Revealed type is "builtins.float"

r01 = apply(radius, 1.0, 0.0)
reveal_type(r01) # Revealed type is "builtins.float"

r1 = apply_decorated1(radius, 1.0, 0.0)
reveal_type(r1) # Revealed type is "builtins.float"

r2 = apply_decorated2(radius, 1.0, 0.0) # error: Argument 1 to "__call__" of "Decorated" has incompatible type "Decorated[[float, float], float]"; expected "Never"  [arg-type]
reveal_type(r2) # Revealed type is "Any"

f2: Decorated[[float, float], float] = radius
f1: Traceable[[float, float], float] = f2
f0: Callable[[float, float], float] = f1

g1: Traceable[[Callable[[float, float], float], float, float], float] = apply_decorated1
g0: Callable[[Callable[[float, float], float], float, float], float] = g1

h2: Decorated[[Callable[[float, float], float], float, float], float] = apply_decorated2
h1: Traceable[[Callable[[float, float], float], float, float], float] = h2
h0: Callable[[Callable[[float, float], float], float, float], float] = h1

reveal_type(h2) # Revealed type is "typehint_decorator.Decorated[[def (builtins.float, builtins.float) -> builtins.float, builtins.float, builtins.float], builtins.float]"

https://mypy-play.net/?mypy=latest&python=3.12&gist=a8f681e6c14ec013bf3ae56c81fe94b2

Expected Behavior

Expected variables transferred from input generic callable to returned generic callable, even if the return is not a super-type of the input.

Actual Behavior

ParamSpec variables are not used to parameterize the returned generic if it is not a super-type of the input.

LOG:  Mypy Version:           1.12.0+dev.a0dbbd5b462136914bb7a378221ae094b6844710
LOG:  Config File:            Default
...
typehint_decorator.py:48: error: Argument 1 to "decorator2" has incompatible type "Callable[[Callable[_P2, _R2], **_P2], _R2]"; expected "Traceable[Never, Never]"  [arg-type]
typehint_decorator.py:60: note: Revealed type is "def [_P2, _R2] (func: def (*_P2.args, **_P2.kwargs) -> _R2`-2, *_P2.args, **_P2.kwargs) -> _R2`-2"
typehint_decorator.py:61: note: Revealed type is "def [_P2, _R] (func: def (*_P2.args, **_P2.kwargs) -> _R`6, *_P2.args, **_P2.kwargs) -> _R`6"
typehint_decorator.py:62: note: Revealed type is "typehint_decorator.Decorated[Never, Never]"
typehint_decorator.py:63: note: Revealed type is "typehint_decorator.Decorated[[x: builtins.float, y: builtins.float], builtins.float]"
typehint_decorator.py:66: note: Revealed type is "builtins.float"
typehint_decorator.py:69: note: Revealed type is "builtins.float"
typehint_decorator.py:72: note: Revealed type is "builtins.float"
typehint_decorator.py:74: error: Need type annotation for "r2"  [var-annotated]
typehint_decorator.py:74: error: Argument 1 to "__call__" of "Decorated" has incompatible type "Decorated[[float, float], float]"; expected "Never"  [arg-type]
typehint_decorator.py:74: error: Argument 2 to "__call__" of "Decorated" has incompatible type "float"; expected "Never"  [arg-type]
typehint_decorator.py:74: error: Argument 3 to "__call__" of "Decorated" has incompatible type "float"; expected "Never"  [arg-type]
typehint_decorator.py:75: note: Revealed type is "Any"
typehint_decorator.py:88: note: Revealed type is "typehint_decorator.Decorated[[def (builtins.float, builtins.float) -> builtins.float, builtins.float, builtins.float], builtins.float]"
LOG:  Deleting typehint_decorator typehint_decorator.py typehint_decorator.meta.json typehint_decorator.data.json
LOG:  No fresh SCCs left in queue
LOG:  Build finished in 0.419 seconds with 50 modules, and 14 errors
Found 5 errors in 1 file (checked 1 source file)

Your Environment

  • Mypy version used: 1.12.0+dev*
  • Mypy command-line flags: python -m mypy -v typehint_decorator.py
  • Python version used: 3.12.4

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Inizia eseguendo la riproduzione riportata nell’issue, python -m mypy -v typehint_decorator.py, e confronta i tipi rivelati e gli errori per decorator2 con quelli per decorator1. Analizza come vengono gestiti i callable generici, ParamSpec e i tipi Decorated restituiti; il lavoro è completato quando le variabili vengono trasferite al callable generico restituito senza gli errori Never riportati.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
python
Ambito
devtools
Tipo di issue
Bug
Difficoltà
4/5
Tempo stimato
3-5 giorni
Stato di attività
Ferma
Chiarezza
Abbastanza chiara
Idoneità per principianti
35/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.