python / python/mypy

`ParamSpec` inferred overly wide from protocol

Aperta
#21,384 2 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

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

Descrizione

Bug Report

When implementing a protocol generic wrt a ParamSpec type inference ends up inferring Any for the param spec in certain cases. Writing code around the way the type checker works we can force it to unify against a properly inferred param spec, but this limits our ability to write nice APIs.

To Reproduce

from typing import Protocol, reveal_type

class Context: pass

class Namer[**P](Protocol):
    def name_for(self, *args: P.args, **kwargs: P.kwargs) -> str: ...

    def execute_on(
        self, ctx: Context, *args: P.args, **kwargs: P.kwargs
    ) -> None: ...

class Impl0:
    @staticmethod
    def name_for(x: int, y: str) -> str:
        return 'Test'
    @staticmethod
    def execute_on(ctx: Context, x: int, y: str):
        pass
    
class Impl1:
    @staticmethod
    def name_for(y: str) -> str:
        return 'Test'
    @staticmethod
    def execute_on(ctx: Context, x: int, y: str):
        pass

class UseImplFirst[**P, T]:
    def __init__(self, impl: T, *args: P.args, **kwargs: P.kwargs) -> None:
        self.impl = impl
        self.args = args
        self.kwargs = kwargs
    def __call__(self: UseImplFirst[P, Namer[P]]) -> None:
        pass

def useImplSecond[**P](impl: Namer[P], *args: P.args, **kwargs: P.kwargs):
    pass

def useImplThird[**P](wrapper: UseImplFirst[P, Namer[P]]) -> None:
    pass

__testImpl0: Namer[[int, str]] = Impl0
UseImplFirst(Impl0, 0, '0')()
useImplSecond(Impl0, 0, '0')
useImplThird(UseImplFirst(Impl0, 0, '0'))

__testImpl1: Namer[[int, str]] = Impl1    # Should fail (Fails)
UseImplFirst(Impl1, 1, '1')()             # Should fail (Succeeds)
useImplSecond(Impl1, 1, '1')              # Should fail (Succeeds)
useImplThird(UseImplFirst(Impl1, 1, '1')) # Should fail (Succeeds)

Expected Behavior / Actual Behavior

See the snippet above.

Woraround
The following forces mypy to infer the ParamSpec first and unify against the protocol implementation later:

class UseImpl[**P]:
    def __init__(self, *args: P.args, **kwargs: P.kwargs):
        self.args = args
        self.kwargs = kwargs
    def __call__(self, impl: Namer[P]) -> None:
        pass


__testImpl0: Namer[[int, str]] = Impl0
UseImpl(0, '0')(Impl0)

__testImpl1: Namer[[int, str]] = Impl1 # Should fail (Fails)
UseImpl(1, '1')(Impl1)                 # Should fail (fails)

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 il reproducer dell’issue e confrontando i playground di mypy 1.12.0 e 1.20.2. Usa i fallimenti previsti per Impl1 e il workaround funzionante come criteri di completamento, quindi aggiungi la copertura di regressione nei test pertinenti di inferenza dei tipi una volta identificata l’area di implementazione.

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

Valutazione

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

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.