Skip to content

Commit 9a743a2

Browse files
committed
[ty] synthesize __replace__ for >=3.13
1 parent c8c80e0 commit 9a743a2

File tree

2 files changed

+92
-6
lines changed

2 files changed

+92
-6
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
## `replace()` function
7+
8+
It is present in the `copy` module.
9+
10+
```toml
11+
[environment]
12+
python-version = "3.13"
13+
```
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+
53+
## Version support check
54+
55+
It is not present in Python < 3.13
56+
57+
```toml
58+
[environment]
59+
python-version = "3.12"
60+
```
61+
62+
```py
63+
from copy import replace # error: [unresolved-import]
64+
```

crates/ty_python_semantic/src/types/class.rs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,7 +1592,9 @@ impl<'db> ClassLiteral<'db> {
15921592

15931593
let field_policy = CodeGeneratorKind::from_class(db, self)?;
15941594

1595-
let signature_from_fields = |mut parameters: Vec<_>| {
1595+
let signature_from_fields = |mut parameters: Vec<_>,
1596+
return_ty: Option<Type<'db>>,
1597+
for_replace: bool| {
15961598
let mut kw_only_field_seen = false;
15971599
for (
15981600
field_name,
@@ -1659,21 +1661,27 @@ impl<'db> ClassLiteral<'db> {
16591661
}
16601662
}
16611663

1662-
let mut parameter = if kw_only_field_seen {
1664+
// For the `__replace__` signature, force to kw only
1665+
let mut parameter = if kw_only_field_seen || for_replace {
16631666
Parameter::keyword_only(field_name)
16641667
} else {
16651668
Parameter::positional_or_keyword(field_name)
16661669
}
16671670
.with_annotated_type(field_ty);
16681671

1669-
if let Some(default_ty) = default_ty {
1672+
// When replacing, we know there is a default value for the field
1673+
// (the value that is currently assigned to the field)
1674+
// assume this to be the declared type of the field
1675+
if for_replace {
1676+
parameter = parameter.with_default_type(field_ty);
1677+
} else if let Some(default_ty) = default_ty {
16701678
parameter = parameter.with_default_type(default_ty);
16711679
}
16721680

16731681
parameters.push(parameter);
16741682
}
16751683

1676-
let mut signature = Signature::new(Parameters::new(parameters), Some(Type::none(db)));
1684+
let mut signature = Signature::new(Parameters::new(parameters), return_ty);
16771685
signature.inherited_generic_context = self.generic_context(db);
16781686
Some(CallableType::function_like(db, signature))
16791687
};
@@ -1695,12 +1703,12 @@ impl<'db> ClassLiteral<'db> {
16951703
db,
16961704
self.apply_optional_specialization(db, specialization),
16971705
));
1698-
signature_from_fields(vec![self_parameter])
1706+
signature_from_fields(vec![self_parameter], Some(Type::none(db)), false)
16991707
}
17001708
(CodeGeneratorKind::NamedTuple, "__new__") => {
17011709
let cls_parameter = Parameter::positional_or_keyword(Name::new_static("cls"))
17021710
.with_annotated_type(KnownClass::Type.to_instance(db));
1703-
signature_from_fields(vec![cls_parameter])
1711+
signature_from_fields(vec![cls_parameter], Some(Type::none(db)), false)
17041712
}
17051713
(CodeGeneratorKind::DataclassLike, "__lt__" | "__le__" | "__gt__" | "__ge__") => {
17061714
if !has_dataclass_param(DataclassParams::ORDER) {
@@ -1735,6 +1743,20 @@ impl<'db> ClassLiteral<'db> {
17351743
.place
17361744
.ignore_possibly_unbound()
17371745
}
1746+
(CodeGeneratorKind::DataclassLike, "__replace__") => {
1747+
if Program::get(db).python_version(db) < PythonVersion::PY313 {
1748+
return None;
1749+
}
1750+
1751+
let self_parameter =
1752+
Parameter::positional_or_keyword(Name::new_static("self")).with_annotated_type(
1753+
Type::instance(db, self.apply_optional_specialization(db, specialization)),
1754+
);
1755+
let instance_ty =
1756+
Type::instance(db, self.apply_optional_specialization(db, specialization));
1757+
1758+
signature_from_fields(vec![self_parameter], Some(instance_ty), true)
1759+
}
17381760
(CodeGeneratorKind::DataclassLike, "__setattr__") => {
17391761
if has_dataclass_param(DataclassParams::FROZEN) {
17401762
let signature = Signature::new(

0 commit comments

Comments
 (0)