Skip to content

Commit f07fce3

Browse files
committed
Merge branch 'main' into dcreager/legacy-class
* main: [red-knot] Preliminary `NamedTuple` support (#17738) [red-knot] Add tests for classes that have incompatible `__new__` and `__init__` methods (#17747) Update dependency vite to v6.2.7 (#17746) [red-knot] Update call binding to return all matching overloads (#17618) [`airflow`] apply Replacement::AutoImport to `AIR312` (#17570) [`ruff`] Add fix safety section (`RUF028`) (#17722) [syntax-errors] Detect single starred expression assignment `x = *y` (#17624) py-fuzzer: fix minimization logic when `--only-new-bugs` is passed (#17739) Fix example syntax for pydocstyle ignore_var_parameters option (#17740) [red-knot] Update salsa to prevent panic in custom panic-handler (#17742) [red-knot] Ban direct instantiation of generic protocols as well as non-generic ones (#17741) [red-knot] Lookup of `__new__` (#17733) [red-knot] Check decorator consistency on overloads (#17684) [`flake8-use-pathlib`] Avoid suggesting `Path.iterdir()` for `os.listdir` with file descriptor (`PTH208`) (#17715) [red-knot] Check overloads without an implementation (#17681) Expand Semantic Syntax Coverage (#17725) [red-knot] Check for invalid overload usages (#17609)
2 parents 4bc56bb + 03d8679 commit f07fce3

File tree

50 files changed

+3359
-1429
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+3359
-1429
lines changed

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ rayon = { version = "1.10.0" }
124124
regex = { version = "1.10.2" }
125125
rustc-hash = { version = "2.0.0" }
126126
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
127-
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "79afd59ed5a5edb4dac63cf5b6cf4a6aa9514bdf" }
127+
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7" }
128128
schemars = { version = "0.8.16" }
129129
seahash = { version = "4.1.0" }
130130
serde = { version = "1.0.197", features = ["derive"] }

crates/red_knot_python_semantic/resources/mdtest/call/constructor.md

Lines changed: 114 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
# Constructor
22

3-
When classes are instantiated, Python calls the meta-class `__call__` method, which can either be
4-
customized by the user or `type.__call__` is used.
3+
When classes are instantiated, Python calls the metaclass's `__call__` method. The metaclass of most
4+
Python classes is the class `builtins.type`.
55

6-
The latter calls the `__new__` method of the class, which is responsible for creating the instance
7-
and then calls the `__init__` method on the resulting instance to initialize it with the same
8-
arguments.
6+
`type.__call__` calls the `__new__` method of the class, which is responsible for creating the
7+
instance. `__init__` is then called on the constructed instance with the same arguments that were
8+
passed to `__new__`.
99

10-
Both `__new__` and `__init__` are looked up using full descriptor protocol, but `__new__` is then
11-
called as an implicit static, rather than bound method with `cls` passed as the first argument.
12-
`__init__` has no special handling, it is fetched as bound method and is called just like any other
13-
dunder method.
10+
Both `__new__` and `__init__` are looked up using the descriptor protocol, i.e., `__get__` is called
11+
if these attributes are descriptors. `__new__` is always treated as a static method, i.e., `cls` is
12+
passed as the first argument. `__init__` has no special handling; it is fetched as a bound method
13+
and called just like any other dunder method.
1414

1515
`type.__call__` does other things too, but this is not yet handled by us.
1616

1717
Since every class has `object` in it's MRO, the default implementations are `object.__new__` and
1818
`object.__init__`. They have some special behavior, namely:
1919

20-
- If neither `__new__` nor `__init__` are defined anywhere in the MRO of class (except for `object`)
21-
\- no arguments are accepted and `TypeError` is raised if any are passed.
22-
- If `__new__` is defined, but `__init__` is not - `object.__init__` will allow arbitrary arguments!
20+
- If neither `__new__` nor `__init__` are defined anywhere in the MRO of class (except for
21+
`object`), no arguments are accepted and `TypeError` is raised if any are passed.
22+
- If `__new__` is defined but `__init__` is not, `object.__init__` will allow arbitrary arguments!
2323

2424
As of today there are a number of behaviors that we do not support:
2525

@@ -146,6 +146,25 @@ reveal_type(Foo()) # revealed: Foo
146146

147147
### Possibly Unbound
148148

149+
#### Possibly unbound `__new__` method
150+
151+
```py
152+
def _(flag: bool) -> None:
153+
class Foo:
154+
if flag:
155+
def __new__(cls):
156+
return object.__new__(cls)
157+
158+
# error: [call-possibly-unbound-method]
159+
reveal_type(Foo()) # revealed: Foo
160+
161+
# error: [call-possibly-unbound-method]
162+
# error: [too-many-positional-arguments]
163+
reveal_type(Foo(1)) # revealed: Foo
164+
```
165+
166+
#### Possibly unbound `__call__` on `__new__` callable
167+
149168
```py
150169
def _(flag: bool) -> None:
151170
class Callable:
@@ -323,3 +342,86 @@ reveal_type(Foo(1)) # revealed: Foo
323342
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
324343
reveal_type(Foo(1, 2)) # revealed: Foo
325344
```
345+
346+
### Incompatible signatures
347+
348+
```py
349+
import abc
350+
351+
class Foo:
352+
def __new__(cls) -> "Foo":
353+
return object.__new__(cls)
354+
355+
def __init__(self, x):
356+
self.x = 42
357+
358+
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
359+
reveal_type(Foo()) # revealed: Foo
360+
361+
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 0, got 1"
362+
reveal_type(Foo(42)) # revealed: Foo
363+
364+
class Foo2:
365+
def __new__(cls, x) -> "Foo2":
366+
return object.__new__(cls)
367+
368+
def __init__(self):
369+
pass
370+
371+
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
372+
reveal_type(Foo2()) # revealed: Foo2
373+
374+
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 0, got 1"
375+
reveal_type(Foo2(42)) # revealed: Foo2
376+
377+
class Foo3(metaclass=abc.ABCMeta):
378+
def __new__(cls) -> "Foo3":
379+
return object.__new__(cls)
380+
381+
def __init__(self, x):
382+
self.x = 42
383+
384+
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
385+
reveal_type(Foo3()) # revealed: Foo3
386+
387+
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 0, got 1"
388+
reveal_type(Foo3(42)) # revealed: Foo3
389+
390+
class Foo4(metaclass=abc.ABCMeta):
391+
def __new__(cls, x) -> "Foo4":
392+
return object.__new__(cls)
393+
394+
def __init__(self):
395+
pass
396+
397+
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
398+
reveal_type(Foo4()) # revealed: Foo4
399+
400+
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 0, got 1"
401+
reveal_type(Foo4(42)) # revealed: Foo4
402+
```
403+
404+
### Lookup of `__new__`
405+
406+
The `__new__` method is always invoked on the class itself, never on the metaclass. This is
407+
different from how other dunder methods like `__lt__` are implicitly called (always on the
408+
meta-type, never on the type itself).
409+
410+
```py
411+
from typing_extensions import Literal
412+
413+
class Meta(type):
414+
def __new__(mcls, name, bases, namespace, /, **kwargs):
415+
return super().__new__(mcls, name, bases, namespace)
416+
417+
def __lt__(cls, other) -> Literal[True]:
418+
return True
419+
420+
class C(metaclass=Meta): ...
421+
422+
# No error is raised here, since we don't implicitly call `Meta.__new__`
423+
reveal_type(C()) # revealed: C
424+
425+
# Meta.__lt__ is implicitly called here:
426+
reveal_type(C < C) # revealed: Literal[True]
427+
```

crates/red_knot_python_semantic/resources/mdtest/call/methods.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or
205205

206206
The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed:
207207

208-
```py
208+
```pyi
209209
from types import FunctionType, MethodType
210210
from typing import overload
211211

crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,62 @@ async def g():
130130
(x async for x in g())
131131
```
132132

133+
## Rebound comprehension variable
134+
135+
Walrus operators cannot rebind variables already in use as iterators:
136+
137+
```py
138+
# error: [invalid-syntax] "assignment expression cannot rebind comprehension variable"
139+
[x := 2 for x in range(10)]
140+
141+
# error: [invalid-syntax] "assignment expression cannot rebind comprehension variable"
142+
{y := 5 for y in range(10)}
143+
```
144+
145+
## Multiple case assignments
146+
147+
Variable names in pattern matching must be unique within a single pattern:
148+
149+
```toml
150+
[environment]
151+
python-version = "3.10"
152+
```
153+
154+
```py
155+
x = [1, 2]
156+
match x:
157+
# error: [invalid-syntax] "multiple assignments to name `a` in pattern"
158+
case [a, a]:
159+
pass
160+
case _:
161+
pass
162+
163+
d = {"key": "value"}
164+
match d:
165+
# error: [invalid-syntax] "multiple assignments to name `b` in pattern"
166+
case {"key": b, "other": b}:
167+
pass
168+
```
169+
170+
## Duplicate type parameter
171+
172+
Type parameter names must be unique in a generic class or function definition:
173+
174+
```toml
175+
[environment]
176+
python-version = "3.12"
177+
```
178+
179+
```py
180+
# error: [invalid-syntax] "duplicate type parameter"
181+
class C[T, T]:
182+
pass
183+
184+
# error: [invalid-syntax] "duplicate type parameter"
185+
def f[X, Y, X]():
186+
pass
187+
```
188+
133189
## `await` outside async function
134190

135191
This error includes `await`, `async for`, `async with`, and `async` comprehensions.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# `NamedTuple`
2+
3+
`NamedTuple` is a type-safe way to define named tuples — a tuple where each field can be accessed by
4+
name, and not just by its numeric position within the tuple:
5+
6+
## `typing.NamedTuple`
7+
8+
### Basics
9+
10+
```py
11+
from typing import NamedTuple
12+
13+
class Person(NamedTuple):
14+
id: int
15+
name: str
16+
age: int | None = None
17+
18+
alice = Person(1, "Alice", 42)
19+
alice = Person(id=1, name="Alice", age=42)
20+
bob = Person(2, "Bob")
21+
bob = Person(id=2, name="Bob")
22+
23+
reveal_type(alice.id) # revealed: int
24+
reveal_type(alice.name) # revealed: str
25+
reveal_type(alice.age) # revealed: int | None
26+
27+
# TODO: These should reveal the types of the fields
28+
reveal_type(alice[0]) # revealed: Unknown
29+
reveal_type(alice[1]) # revealed: Unknown
30+
reveal_type(alice[2]) # revealed: Unknown
31+
32+
# error: [missing-argument]
33+
Person(3)
34+
35+
# error: [too-many-positional-arguments]
36+
Person(3, "Eve", 99, "extra")
37+
38+
# error: [invalid-argument-type]
39+
Person(id="3", name="Eve")
40+
```
41+
42+
Alternative functional syntax:
43+
44+
```py
45+
Person2 = NamedTuple("Person", [("id", int), ("name", str)])
46+
alice2 = Person2(1, "Alice")
47+
48+
# TODO: should be an error
49+
Person2(1)
50+
51+
reveal_type(alice2.id) # revealed: @Todo(GenericAlias instance)
52+
reveal_type(alice2.name) # revealed: @Todo(GenericAlias instance)
53+
```
54+
55+
### Multiple Inheritance
56+
57+
Multiple inheritance is not supported for `NamedTuple` classes:
58+
59+
```py
60+
from typing import NamedTuple
61+
62+
# This should ideally emit a diagnostic
63+
class C(NamedTuple, object):
64+
id: int
65+
name: str
66+
```
67+
68+
### Inheriting from a `NamedTuple`
69+
70+
Inheriting from a `NamedTuple` is supported, but new fields on the subclass will not be part of the
71+
synthesized `__new__` signature:
72+
73+
```py
74+
from typing import NamedTuple
75+
76+
class User(NamedTuple):
77+
id: int
78+
name: str
79+
80+
class SuperUser(User):
81+
level: int
82+
83+
# This is fine:
84+
alice = SuperUser(1, "Alice")
85+
reveal_type(alice.level) # revealed: int
86+
87+
# This is an error because `level` is not part of the signature:
88+
# error: [too-many-positional-arguments]
89+
alice = SuperUser(1, "Alice", 3)
90+
```
91+
92+
### Generic named tuples
93+
94+
```toml
95+
[environment]
96+
python-version = "3.12"
97+
```
98+
99+
```py
100+
from typing import NamedTuple
101+
102+
class Property[T](NamedTuple):
103+
name: str
104+
value: T
105+
106+
# TODO: this should be supported (no error, revealed type of `Property[float]`)
107+
# error: [invalid-argument-type]
108+
reveal_type(Property("height", 3.4)) # revealed: Property[Unknown]
109+
```
110+
111+
## `collections.namedtuple`
112+
113+
```py
114+
from collections import namedtuple
115+
116+
Person = namedtuple("Person", ["id", "name", "age"], defaults=[None])
117+
118+
alice = Person(1, "Alice", 42)
119+
bob = Person(2, "Bob")
120+
```

0 commit comments

Comments
 (0)