Skip to content

Commit

Permalink
Teach red-knot that type(x) is the same as x.__class__ (#16301)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexWaygood authored Feb 21, 2025
1 parent 5347abc commit 224a36f
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 9 deletions.
10 changes: 10 additions & 0 deletions crates/red_knot_python_semantic/resources/mdtest/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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,
Expand Down
23 changes: 23 additions & 0 deletions crates/red_knot_python_semantic/resources/mdtest/call/builtins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
},
Expand Down
8 changes: 8 additions & 0 deletions crates/red_knot_python_semantic/src/types/call/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 224a36f

Please sign in to comment.