python / python/mypy

`ParamSpec` inferred overly wide from protocol

Offen
#21,384 2 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

bug topic-inference topic-paramspec
Vorherrschende Sprache
Python
Sterne
20.6k
Forks
3.3k
PR-Merge-Kennzahlen
PR-Kennzahlen ausstehend

Beschreibung

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)

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne damit, den Reproducer aus dem Issue auszuführen und die Playgrounds von mypy 1.12.0 und 1.20.2 zu vergleichen. Verwende die erwarteten Fehler für Impl1 und den funktionierenden Workaround als Abschlusskriterien und füge Regressionstests in den relevanten Tests zur Typinferenz hinzu, sobald der Implementierungsbereich identifiziert ist.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
python
Bereich
compilers
Issue-Typ
Bug
Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Aktivitätsstatus
Ruhig
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
45/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.