Skip to content

Commit 6639db6

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

File tree

5 files changed

+81
-25
lines changed

5 files changed

+81
-25
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
### Dataclasses
22+
23+
```py
24+
from dataclasses import dataclass
25+
from copy import replace
26+
27+
@dataclass
28+
class Point:
29+
x: int
30+
y: int
31+
32+
a = Point(1, 2)
33+
34+
# It accepts keyword arguments
35+
reveal_type(a.__replace__) # revealed: (*, x: int = int, y: int = int) -> Point
36+
b = a.__replace__(x=3, y=4)
37+
reveal_type(b) # revealed: Point
38+
b = replace(a, x=3, y=4)
39+
reveal_type(b) # revealed: Point
40+
41+
# It does not require all keyword arguments
42+
c = a.__replace__(x=3)
43+
reveal_type(c) # revealed: Point
44+
d = replace(a, x=3)
45+
reveal_type(d) # revealed: Point
46+
47+
e = a.__replace__(x="wrong") # error: [invalid-argument-type]
48+
e = replace(a, x="wrong") # TODO: error: [invalid-argument-type]
49+
```

crates/ty_python_semantic/src/module_resolver/module.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ pub enum KnownModule {
264264
Typing,
265265
Sys,
266266
Abc,
267+
Copy,
267268
Dataclasses,
268269
Collections,
269270
Inspect,
@@ -292,6 +293,7 @@ impl KnownModule {
292293
Self::Sys => "sys",
293294
Self::Abc => "abc",
294295
Self::Dataclasses => "dataclasses",
296+
Self::Copy => "copy",
295297
Self::Collections => "collections",
296298
Self::Inspect => "inspect",
297299
Self::TypeCheckerInternals => "_typeshed._type_checker_internals",

crates/ty_python_semantic/src/types/call/bind.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use crate::types::{
3333
WrapperDescriptorKind, enums, ide_support, todo_type,
3434
};
3535
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
36-
use ruff_python_ast as ast;
36+
use ruff_python_ast::{self as ast};
3737

3838
/// Binding information for a possible union of callables. At a call site, the arguments must be
3939
/// compatible with _all_ of the types in the union for the call to be valid.

crates/ty_python_semantic/src/types/class.rs

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,7 +1596,10 @@ 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 instance_ty =
1600+
Type::instance(db, self.apply_optional_specialization(db, specialization));
1601+
1602+
let signature_from_fields = |mut parameters: Vec<_>, return_ty: Option<Type<'db>>| {
16001603
let mut kw_only_field_seen = false;
16011604
for (
16021605
field_name,
@@ -1669,21 +1672,27 @@ impl<'db> ClassLiteral<'db> {
16691672
}
16701673
}
16711674

1672-
let mut parameter = if kw_only_field_seen {
1675+
// For the `__replace__` signature, force to kw only
1676+
let mut parameter = if kw_only_field_seen || name == "__replace__" {
16731677
Parameter::keyword_only(field_name)
16741678
} else {
16751679
Parameter::positional_or_keyword(field_name)
16761680
}
16771681
.with_annotated_type(field_ty);
16781682

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

16831692
parameters.push(parameter);
16841693
}
16851694

1686-
let mut signature = Signature::new(Parameters::new(parameters), Some(Type::none(db)));
1695+
let mut signature = Signature::new(Parameters::new(parameters), return_ty);
16871696
signature.inherited_generic_context = self.generic_context(db);
16881697
Some(CallableType::function_like(db, signature))
16891698
};
@@ -1701,16 +1710,13 @@ impl<'db> ClassLiteral<'db> {
17011710

17021711
let self_parameter = Parameter::positional_or_keyword(Name::new_static("self"))
17031712
// TODO: could be `Self`.
1704-
.with_annotated_type(Type::instance(
1705-
db,
1706-
self.apply_optional_specialization(db, specialization),
1707-
));
1708-
signature_from_fields(vec![self_parameter])
1713+
.with_annotated_type(instance_ty);
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) {
@@ -1721,16 +1727,10 @@ impl<'db> ClassLiteral<'db> {
17211727
Parameters::new([
17221728
Parameter::positional_or_keyword(Name::new_static("self"))
17231729
// TODO: could be `Self`.
1724-
.with_annotated_type(Type::instance(
1725-
db,
1726-
self.apply_optional_specialization(db, specialization),
1727-
)),
1730+
.with_annotated_type(instance_ty),
17281731
Parameter::positional_or_keyword(Name::new_static("other"))
17291732
// TODO: could be `Self`.
1730-
.with_annotated_type(Type::instance(
1731-
db,
1732-
self.apply_optional_specialization(db, specialization),
1733-
)),
1733+
.with_annotated_type(instance_ty),
17341734
]),
17351735
Some(KnownClass::Bool.to_instance(db)),
17361736
);
@@ -1745,15 +1745,20 @@ impl<'db> ClassLiteral<'db> {
17451745
.place
17461746
.ignore_possibly_unbound()
17471747
}
1748+
(CodeGeneratorKind::DataclassLike, "__replace__")
1749+
if Program::get(db).python_version(db) >= PythonVersion::PY313 =>
1750+
{
1751+
let self_parameter = Parameter::positional_or_keyword(Name::new_static("self"))
1752+
.with_annotated_type(instance_ty);
1753+
1754+
signature_from_fields(vec![self_parameter], Some(instance_ty))
1755+
}
17481756
(CodeGeneratorKind::DataclassLike, "__setattr__") => {
17491757
if has_dataclass_param(DataclassParams::FROZEN) {
17501758
let signature = Signature::new(
17511759
Parameters::new([
17521760
Parameter::positional_or_keyword(Name::new_static("self"))
1753-
.with_annotated_type(Type::instance(
1754-
db,
1755-
self.apply_optional_specialization(db, specialization),
1756-
)),
1761+
.with_annotated_type(instance_ty),
17571762
Parameter::positional_or_keyword(Name::new_static("name")),
17581763
Parameter::positional_or_keyword(Name::new_static("value")),
17591764
]),

crates/ty_python_semantic/src/types/function.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ use bitflags::bitflags;
5555
use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span};
5656
use ruff_db::files::{File, FileRange};
5757
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
58-
use ruff_python_ast as ast;
58+
use ruff_python_ast::{self as ast};
5959
use ruff_text_size::Ranged;
6060

6161
use crate::module_resolver::{KnownModule, file_to_module};
@@ -1391,7 +1391,7 @@ pub(crate) mod tests {
13911391
use strum::IntoEnumIterator;
13921392

13931393
use super::*;
1394-
use crate::db::tests::setup_db;
1394+
use crate::db::tests::{TestDbBuilder, setup_db};
13951395
use crate::place::known_module_symbol;
13961396

13971397
#[test]

0 commit comments

Comments
 (0)