Teach red-knot that type(x) is the same as x.__class__ (#16301)

This commit is contained in:
Alex Waygood 2025-02-21 21:05:48 +00:00 committed by GitHub
parent 5347abc766
commit 224a36f5f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 49 additions and 9 deletions

View file

@ -884,13 +884,18 @@ def _(flag: bool):
## Objects of all types have a `__class__` method
The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is always the same value as
`type(x)`.
```py
import typing_extensions
reveal_type(typing_extensions.__class__) # revealed: Literal[ModuleType]
reveal_type(type(typing_extensions)) # revealed: Literal[ModuleType]
a = 42
reveal_type(a.__class__) # revealed: Literal[int]
reveal_type(type(a)) # revealed: Literal[int]
b = "42"
reveal_type(b.__class__) # revealed: Literal[str]
@ -906,8 +911,13 @@ reveal_type(e.__class__) # revealed: Literal[tuple]
def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
reveal_type(a.__class__) # revealed: type[int]
reveal_type(type(a)) # revealed: type[int]
reveal_type(b.__class__) # revealed: Literal[str]
reveal_type(type(b)) # revealed: Literal[str]
reveal_type(c.__class__) # revealed: type[int] | type[str]
reveal_type(type(c)) # revealed: type[int] | type[str]
# `type[type]`, a.k.a., either the class `type` or some subclass of `type`.
# It would be incorrect to infer `Literal[type]` here,

View file

@ -12,3 +12,26 @@ 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/red_knot_python_semantic/resources/mdtest/attributes.md`,
alongside the tests for the `__class__` attribute.)
```py
reveal_type(type(1)) # revealed: Literal[int]
```
But a three-argument call to type creates a dynamic instance of the `type` class:
```py
reveal_type(type("Foo", (), {})) # revealed: type
```
Other numbers of arguments are invalid (TODO -- these should emit a diagnostic)
```py
type("Foo", ())
type("Foo", (), {}, weird_other_arg=42)
```

View file

@ -97,12 +97,7 @@ else:
## No narrowing for instances of `builtins.type`
```py
def _(flag: bool):
t = type("t", (), {})
# This isn't testing what we want it to test if we infer anything more precise here:
reveal_type(t) # revealed: type
def _(flag: bool, t: type):
x = 1 if flag else "foo"
if isinstance(x, t):

View file

@ -112,8 +112,7 @@ def _(flag: bool):
reveal_type(t) # revealed: Literal[NoneType]
if issubclass(t, type(None)):
# TODO: this should be just `Literal[NoneType]`
reveal_type(t) # revealed: Literal[int, NoneType]
reveal_type(t) # revealed: Literal[NoneType]
```
## `classinfo` contains multiple types

View file

@ -2181,7 +2181,12 @@ impl<'db> Type<'db> {
Some(KnownClass::Str) => arguments
.first_argument()
.map(|arg| arg.str(db))
.unwrap_or(Type::string_literal(db, "")),
.unwrap_or_else(|| Type::string_literal(db, "")),
Some(KnownClass::Type) => arguments
.exactly_one_argument()
.map(|arg| arg.to_meta_type(db))
.unwrap_or_else(|| KnownClass::Type.to_instance(db)),
_ => Type::Instance(InstanceType { class }),
},

View file

@ -35,6 +35,14 @@ impl<'a, 'db> CallArguments<'a, 'db> {
self.0.first().map(Argument::ty)
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn exactly_one_argument(&self) -> Option<Type<'db>> {
match &*self.0 {
[arg] => Some(arg.ty()),
_ => None,
}
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn second_argument(&self) -> Option<Type<'db>> {
self.0.get(1).map(Argument::ty)