diff --git a/crates/ty_python_semantic/resources/mdtest/call/replace.md b/crates/ty_python_semantic/resources/mdtest/call/replace.md new file mode 100644 index 0000000000..b0112c1129 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/call/replace.md @@ -0,0 +1,70 @@ +# `replace` + +The `replace` function and the `replace` protocol were added in Python 3.13: + + +```toml +[environment] +python-version = "3.13" +``` + +## Basic + +```py +from copy import replace +from datetime import time + +t = time(12, 0, 0) +t = replace(t, minute=30) + +reveal_type(t) # revealed: time +``` + +## The `__replace__` protocol + +### Dataclasses + +Dataclasses support the `__replace__` protocol: + +```py +from dataclasses import dataclass +from copy import replace + +@dataclass +class Point: + x: int + y: int + +reveal_type(Point.__replace__) # revealed: (self: Point, *, x: int = int, y: int = int) -> Point +``` + +The `__replace__` method can either be called directly or through the `replace` function: + +```py +a = Point(1, 2) + +b = a.__replace__(x=3, y=4) +reveal_type(b) # revealed: Point + +b = replace(a, x=3, y=4) +reveal_type(b) # revealed: Point +``` + +A call to `replace` does not require all keyword arguments: + +```py +c = a.__replace__(y=4) +reveal_type(c) # revealed: Point + +d = replace(a, y=4) +reveal_type(d) # revealed: Point +``` + +Invalid calls to `__replace__` or `replace` will raise an error: + +```py +e = a.__replace__(x="wrong") # error: [invalid-argument-type] + +# TODO: this should ideally also be emit an error +e = replace(a, x="wrong") +``` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index b7157dbc08..8ca5516bb1 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1596,7 +1596,10 @@ impl<'db> ClassLiteral<'db> { let field_policy = CodeGeneratorKind::from_class(db, self)?; - let signature_from_fields = |mut parameters: Vec<_>| { + let instance_ty = + Type::instance(db, self.apply_optional_specialization(db, specialization)); + + let signature_from_fields = |mut parameters: Vec<_>, return_ty: Option>| { let mut kw_only_field_seen = false; for ( field_name, @@ -1669,21 +1672,26 @@ impl<'db> ClassLiteral<'db> { } } - let mut parameter = if kw_only_field_seen { + let mut parameter = if kw_only_field_seen || name == "__replace__" { Parameter::keyword_only(field_name) } else { Parameter::positional_or_keyword(field_name) } .with_annotated_type(field_ty); - if let Some(default_ty) = default_ty { + if name == "__replace__" { + // When replacing, we know there is a default value for the field + // (the value that is currently assigned to the field) + // assume this to be the declared type of the field + parameter = parameter.with_default_type(field_ty); + } else if let Some(default_ty) = default_ty { parameter = parameter.with_default_type(default_ty); } parameters.push(parameter); } - let mut signature = Signature::new(Parameters::new(parameters), Some(Type::none(db))); + let mut signature = Signature::new(Parameters::new(parameters), return_ty); signature.inherited_generic_context = self.generic_context(db); Some(CallableType::function_like(db, signature)) }; @@ -1701,16 +1709,13 @@ impl<'db> ClassLiteral<'db> { let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) // TODO: could be `Self`. - .with_annotated_type(Type::instance( - db, - self.apply_optional_specialization(db, specialization), - )); - signature_from_fields(vec![self_parameter]) + .with_annotated_type(instance_ty); + signature_from_fields(vec![self_parameter], Some(Type::none(db))) } (CodeGeneratorKind::NamedTuple, "__new__") => { let cls_parameter = Parameter::positional_or_keyword(Name::new_static("cls")) .with_annotated_type(KnownClass::Type.to_instance(db)); - signature_from_fields(vec![cls_parameter]) + signature_from_fields(vec![cls_parameter], Some(Type::none(db))) } (CodeGeneratorKind::DataclassLike, "__lt__" | "__le__" | "__gt__" | "__ge__") => { if !has_dataclass_param(DataclassParams::ORDER) { @@ -1721,16 +1726,10 @@ impl<'db> ClassLiteral<'db> { Parameters::new([ Parameter::positional_or_keyword(Name::new_static("self")) // TODO: could be `Self`. - .with_annotated_type(Type::instance( - db, - self.apply_optional_specialization(db, specialization), - )), + .with_annotated_type(instance_ty), Parameter::positional_or_keyword(Name::new_static("other")) // TODO: could be `Self`. - .with_annotated_type(Type::instance( - db, - self.apply_optional_specialization(db, specialization), - )), + .with_annotated_type(instance_ty), ]), Some(KnownClass::Bool.to_instance(db)), ); @@ -1745,15 +1744,20 @@ impl<'db> ClassLiteral<'db> { .place .ignore_possibly_unbound() } + (CodeGeneratorKind::DataclassLike, "__replace__") + if Program::get(db).python_version(db) >= PythonVersion::PY313 => + { + let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty); + + signature_from_fields(vec![self_parameter], Some(instance_ty)) + } (CodeGeneratorKind::DataclassLike, "__setattr__") => { if has_dataclass_param(DataclassParams::FROZEN) { let signature = Signature::new( Parameters::new([ Parameter::positional_or_keyword(Name::new_static("self")) - .with_annotated_type(Type::instance( - db, - self.apply_optional_specialization(db, specialization), - )), + .with_annotated_type(instance_ty), Parameter::positional_or_keyword(Name::new_static("name")), Parameter::positional_or_keyword(Name::new_static("value")), ]),