python / python/mypy

[match-case]/[isinstance] narrowing logic against the special `AnyCallable` Protocol differs from `callable()` check

Offen
#19,470 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

bug topic-match-statement topic-type-narrowing
Vorherrschende Sprache
Python
Sterne
20.6k
Forks
3.3k
PR-Merge-Kennzahlen
PR-Kennzahlen ausstehend

Beschreibung

According to the typing spec (https://typing.python.org/en/latest/spec/callables.html#meaning-of-in-callable),

@runtime_checkable
class AnyCallable(Protocol):
    def __call__(self, *args: Any, **kwargs: Any) -> Any: ...

is an implementation of Callable[..., Any]. Therefore, its isinstance-narrowing and match-case class pattern narrowing should behave the same as a simple if callable() narrowing. However, this is not the case: both isinstance and match-case produce different results:

from typing import Any, Callable
from typing_extensions import Protocol, runtime_checkable, assert_type

@runtime_checkable
class AnyCallable(Protocol):
    def __call__(self, *args: Any, **kwargs: Any) -> Any: ...

def check_proto(p: AnyCallable) -> None:
    assert_type(p, Callable[..., Any])

class FnImpl:
    def __call__(self, x: object, /) -> int: ...

# Test Baseline
def test_iscallable_object(x: object) -> None:
    if callable(x):
        reveal_type(x)  # N: Revealed type is "__main__.<callable subtype of object>"

def test_iscallable_impl(x: FnImpl) -> None:
    if callable(x):
        reveal_type(x)  # N: Revealed type is "__main__.FnImpl"

def test_iscallable_callable(x: Callable[[object], int]) -> None:
    if callable(x):
        reveal_type(x)  # N: Revealed type is "def (builtins.object) -> builtins.int"


# Test isinstance
def test_isinstance_object(x: object) -> None:
    if isinstance(x, AnyCallable):
        reveal_type(x)  # N: Revealed type is "__main__.AnyCallable"

def test_isinstance_impl(x: FnImpl) -> None:
    if isinstance(x, AnyCallable):
        reveal_type(x)  # N: Revealed type is "__main__.FnImpl"

def test_isinstance_callable(x: Callable[[object], int]) -> None:
    if isinstance(x, AnyCallable):
        reveal_type(x)  # N: Revealed type is "__main__.AnyCallable"


# test match-case
def test_match_object(x: object) -> None:
    match x:
        case AnyCallable() as fn:
            reveal_type(fn)  # N: Revealed type is "__main__.AnyCallable"

def test_match_impl(x: FnImpl) -> None:
    match x:
        case AnyCallable() as fn:
            reveal_type(fn)  # N: Revealed type is "__main__.AnyCallable"

def test_match_callable(x: Callable[[object], int]) -> None:
    match x:
        case AnyCallable() as fn:
            reveal_type(fn)  # N: Revealed type is "__main__.AnyCallable"

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

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, die Reproduktion im verlinkten mypy-play-Beispiel auszuführen und callable(), isinstance(x, AnyCallable) sowie die Eingrenzung bei match-case zu vergleichen. Verfolge das Eingrenzungsverhalten für Eingaben vom Typ object, FnImpl und Callable; abgeschlossen ist die Arbeit, wenn die protokollbasierten Prüfungen dieselbe effektive Eingrenzung wie callable() erzeugen, sofern die Typing-Spezifikation dies verlangt.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

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

Neue Issues direkt in Ihr Postfach

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