mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-05 08:00:27 +00:00

## Summary https://github.com/astral-sh/ruff/pull/20165 added a lot of false positives around calls to `builtins.open()`, because our missing support for PEP-613 type aliases means that we don't understand typeshed's overloads for `builtins.open()` at all yet, and therefore always select the first overload. This didn't use to matter very much, but now that we have a much stricter implementation of protocol assignability/subtyping it matters a lot, because most of the stdlib functions dealing with I/O (`pickle`, `marshal`, `io`, `json`, etc.) are annotated in typeshed as taking in protocols of some kind. In lieu of full PEP-613 support, which is blocked on various things and might not land in time for our next alpha release, this PR adds some temporary special-casing for `builtins.open()` to avoid the false positives. We just infer `Todo` for anything that isn't meant to match typeshed's first `open()` overload. This should be easy to rip out again once we have proper support for PEP-613 type aliases, which hopefully should be pretty soon! ## Test Plan Added an mdtest
5.2 KiB
5.2 KiB
Calling builtins
bool
with incorrect arguments
class NotBool:
__bool__ = None
# error: [too-many-positional-arguments] "Too many positional arguments to class `bool`: expected 1, got 2"
bool(1, 2)
# TODO: We should emit an `unsupported-bool-conversion` error here because the argument doesn't implement `__bool__` correctly.
bool(NotBool())
Calls to type()
A single-argument call to type()
returns an object that has the argument's meta-type. (This is
tested more extensively in crates/ty_python_semantic/resources/mdtest/attributes.md
, alongside the
tests for the __class__
attribute.)
reveal_type(type(1)) # revealed: <class 'int'>
But a three-argument call to type creates a dynamic instance of the type
class:
class Base: ...
reveal_type(type("Foo", (), {})) # revealed: type
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: type
Other numbers of arguments are invalid
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type("Foo", ())
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type("Foo", (), {}, weird_other_arg=42)
The following calls are also invalid, due to incorrect argument types:
class Base: ...
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `Literal[b"Foo"]`"
type(b"Foo", (), {})
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `<class 'Base'>`"
type("Foo", Base, {})
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`"
type("Foo", (1, 2), {})
# TODO: this should be an error
type("Foo", (Base,), {b"attr": 1})
Calls to str()
Valid calls
str()
str("")
str(b"")
str(1)
str(object=1)
str(b"M\xc3\xbcsli", "utf-8")
str(b"M\xc3\xbcsli", "utf-8", "replace")
str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16")
str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16", errors="ignore")
str(bytearray.fromhex("4d c3 bc 73 6c 69"), "utf-8")
str(bytearray(), "utf-8")
str(encoding="utf-8", object=b"M\xc3\xbcsli")
str(b"", errors="replace")
str(encoding="utf-8")
str(errors="replace")
Invalid calls
# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `bytes | bytearray`, found `Literal[1]`"
# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `str`, found `Literal[2]`"
str(1, 2)
# error: [no-matching-overload]
str(o=1)
# First argument is not a bytes-like object:
# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `bytes | bytearray`, found `Literal["Müsli"]`"
str("Müsli", "utf-8")
# Second argument is not a valid encoding:
# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `str`, found `Literal[b"utf-8"]`"
str(b"M\xc3\xbcsli", b"utf-8")
Calls to isinstance
We infer Literal[True]
for a limited set of cases where we can be sure that the answer is correct,
but fall back to bool
otherwise.
from enum import Enum
from types import FunctionType
class Answer(Enum):
NO = 0
YES = 1
reveal_type(isinstance(True, bool)) # revealed: Literal[True]
reveal_type(isinstance(True, int)) # revealed: Literal[True]
reveal_type(isinstance(True, object)) # revealed: Literal[True]
reveal_type(isinstance("", str)) # revealed: Literal[True]
reveal_type(isinstance(1, int)) # revealed: Literal[True]
reveal_type(isinstance(b"", bytes)) # revealed: Literal[True]
reveal_type(isinstance(Answer.NO, Answer)) # revealed: Literal[True]
reveal_type(isinstance((1, 2), tuple)) # revealed: Literal[True]
def f(): ...
reveal_type(isinstance(f, FunctionType)) # revealed: Literal[True]
reveal_type(isinstance("", int)) # revealed: bool
class A: ...
class SubclassOfA(A): ...
class B: ...
reveal_type(isinstance(A, type)) # revealed: Literal[True]
a = A()
reveal_type(isinstance(a, A)) # revealed: Literal[True]
reveal_type(isinstance(a, object)) # revealed: Literal[True]
reveal_type(isinstance(a, SubclassOfA)) # revealed: bool
reveal_type(isinstance(a, B)) # revealed: bool
s = SubclassOfA()
reveal_type(isinstance(s, SubclassOfA)) # revealed: Literal[True]
reveal_type(isinstance(s, A)) # revealed: Literal[True]
def _(x: A | B, y: list[int]):
reveal_type(isinstance(y, list)) # revealed: Literal[True]
reveal_type(isinstance(x, A)) # revealed: bool
if isinstance(x, A):
pass
else:
reveal_type(x) # revealed: B & ~A
reveal_type(isinstance(x, B)) # revealed: Literal[True]
Calls to open()
We do not fully understand typeshed's overloads for open()
yet, due to missing support for PEP-613
type aliases. However, we also do not emit false-positive diagnostics on common calls to open()
:
import pickle
reveal_type(open("")) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(open("", "r")) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(open("", "rb")) # revealed: @Todo(`builtins.open` return type)
with open("foo.pickle", "rb") as f:
x = pickle.load(f) # fine
def _(mode: str):
reveal_type(open("", mode)) # revealed: @Todo(`builtins.open` return type)