Skip to content

Commit 6ed4a25

Browse files
committed
[ty] synthesize __replace__ for >=3.13
1 parent 8c0743d commit 6ed4a25

File tree

2 files changed

+78
-6
lines changed

2 files changed

+78
-6
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Replace
2+
3+
The replace function and protocol added in Python 3.13:
4+
<https://docs.python.org/3/whatsnew/3.13.html#copy>
5+
6+
```toml
7+
[environment]
8+
python-version = "3.13"
9+
```
10+
11+
## `replace()` function
12+
13+
It is present in the `copy` module.
14+
15+
```py
16+
from copy import replace
17+
```
18+
19+
## `__replace__` protocol
20+
21+
```toml
22+
[environment]
23+
python-version = "3.13"
24+
```
25+
26+
### Dataclasses
27+
28+
```py
29+
from dataclasses import dataclass
30+
from copy import replace
31+
32+
@dataclass
33+
class Point:
34+
x: int
35+
y: int
36+
37+
a = Point(1, 2)
38+
39+
# It accepts keyword arguments
40+
reveal_type(a.__replace__) # revealed: (*, x: int = int, y: int = int) -> Point
41+
b = a.__replace__(x=3, y=4)
42+
reveal_type(b) # revealed: Point
43+
b = replace(a, x=3, y=4)
44+
reveal_type(b) # revealed: Point
45+
46+
# It does not require all keyword arguments
47+
c = a.__replace__(x=3)
48+
reveal_type(c) # revealed: Point
49+
d = replace(a, x=3)
50+
reveal_type(d) # revealed: Point
51+
52+
e = a.__replace__(x="wrong") # error: [invalid-argument-type]
53+
e = replace(a, x="wrong") # TODO: error: [invalid-argument-type]
54+
```

crates/ty_python_semantic/src/types/class.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,7 +1596,7 @@ impl<'db> ClassLiteral<'db> {
15961596

15971597
let field_policy = CodeGeneratorKind::from_class(db, self)?;
15981598

1599-
let signature_from_fields = |mut parameters: Vec<_>| {
1599+
let signature_from_fields = |mut parameters: Vec<_>, return_ty: Option<Type<'db>>| {
16001600
let mut kw_only_field_seen = false;
16011601
for (
16021602
field_name,
@@ -1669,21 +1669,27 @@ impl<'db> ClassLiteral<'db> {
16691669
}
16701670
}
16711671

1672-
let mut parameter = if kw_only_field_seen {
1672+
// For the `__replace__` signature, force to kw only
1673+
let mut parameter = if kw_only_field_seen || name == "__replace__" {
16731674
Parameter::keyword_only(field_name)
16741675
} else {
16751676
Parameter::positional_or_keyword(field_name)
16761677
}
16771678
.with_annotated_type(field_ty);
16781679

1679-
if let Some(default_ty) = default_ty {
1680+
if name == "__replace__" {
1681+
// When replacing, we know there is a default value for the field
1682+
// (the value that is currently assigned to the field)
1683+
// assume this to be the declared type of the field
1684+
parameter = parameter.with_default_type(field_ty);
1685+
} else if let Some(default_ty) = default_ty {
16801686
parameter = parameter.with_default_type(default_ty);
16811687
}
16821688

16831689
parameters.push(parameter);
16841690
}
16851691

1686-
let mut signature = Signature::new(Parameters::new(parameters), Some(Type::none(db)));
1692+
let mut signature = Signature::new(Parameters::new(parameters), return_ty);
16871693
signature.inherited_generic_context = self.generic_context(db);
16881694
Some(CallableType::function_like(db, signature))
16891695
};
@@ -1705,12 +1711,12 @@ impl<'db> ClassLiteral<'db> {
17051711
db,
17061712
self.apply_optional_specialization(db, specialization),
17071713
));
1708-
signature_from_fields(vec![self_parameter])
1714+
signature_from_fields(vec![self_parameter], Some(Type::none(db)))
17091715
}
17101716
(CodeGeneratorKind::NamedTuple, "__new__") => {
17111717
let cls_parameter = Parameter::positional_or_keyword(Name::new_static("cls"))
17121718
.with_annotated_type(KnownClass::Type.to_instance(db));
1713-
signature_from_fields(vec![cls_parameter])
1719+
signature_from_fields(vec![cls_parameter], Some(Type::none(db)))
17141720
}
17151721
(CodeGeneratorKind::DataclassLike, "__lt__" | "__le__" | "__gt__" | "__ge__") => {
17161722
if !has_dataclass_param(DataclassParams::ORDER) {
@@ -1745,6 +1751,18 @@ impl<'db> ClassLiteral<'db> {
17451751
.place
17461752
.ignore_possibly_unbound()
17471753
}
1754+
(CodeGeneratorKind::DataclassLike, "__replace__")
1755+
if Program::get(db).python_version(db) >= PythonVersion::PY313 =>
1756+
{
1757+
let self_parameter =
1758+
Parameter::positional_or_keyword(Name::new_static("self")).with_annotated_type(
1759+
Type::instance(db, self.apply_optional_specialization(db, specialization)),
1760+
);
1761+
let instance_ty =
1762+
Type::instance(db, self.apply_optional_specialization(db, specialization));
1763+
1764+
signature_from_fields(vec![self_parameter], Some(instance_ty))
1765+
}
17481766
(CodeGeneratorKind::DataclassLike, "__setattr__") => {
17491767
if has_dataclass_param(DataclassParams::FROZEN) {
17501768
let signature = Signature::new(

0 commit comments

Comments
 (0)