[ty] Preserve TypedDicts for compatible PEP 584 updates#23806
[ty] Preserve TypedDicts for compatible PEP 584 updates#23806charliermarsh merged 5 commits intomainfrom
TypedDicts for compatible PEP 584 updates#23806Conversation
Typing conformance resultsNo changes detected ✅Current numbersThe percentage of diagnostics emitted that were expected errors held steady at 85.05%. The percentage of expected errors that received a diagnostic held steady at 78.05%. The number of fully passing files held steady at 63/132. |
Memory usage reportMemory usage unchanged ✅ |
|
e297e20 to
8ac2cf6
Compare
AlexWaygood
left a comment
There was a problem hiding this comment.
Hmm, what motivated the change in approach here from #23408? I liked the overall approach in that PR of synthesizing __(r)or__ in types/class.rs, I just wasn't convinced that the signatures of the synthesized methods you had were correct
|
Oh sorry, my impression was the prior approach was way off. Let me revisit. |
|
Okay, I looked at #23408 again (and updated the relevant branch). The problem is, if we just add the synthesized method, we still see this behavior: from typing import TypedDict
class MyDict(TypedDict):
foo: int
bar: str
val = MyDict(foo=1, bar="hello")
result = val | {"foo": 2}
reveal_type(result) # dict[str, object], but should be MyDictBecause The resulting diff ends up being similar, except we have a synthesized method instead of: (Type::TypedDict(left_typed_dict), rhs, ast::Operator::BitOr)
if rhs.is_assignable_to(db, Type::TypedDict(left_typed_dict)) =>
{
Some(Type::TypedDict(left_typed_dict))
}
(lhs, Type::TypedDict(right_typed_dict), ast::Operator::BitOr)
if lhs.is_assignable_to(db, Type::TypedDict(right_typed_dict)) =>
{
Some(Type::TypedDict(right_typed_dict))
}To me, this approach seems a little better given the above. |
|
The advantage of the synthesized-method approach is that these tests, which fail on your branch, would (I think) naturally pass: from typing import TypedDict, Protocol, Self
class Foo(TypedDict):
x: str
y: int
class Bar(TypedDict, closed=True):
x: str
def _(f: Foo, g: Bar):
f |= g # errors on this branch, but should pass
class Proto(Protocol):
def __or__(self, other: Bar) -> Self: ...
def takes_proto(x: Proto): ...
def test(x: Foo):
takes_proto(x) # errors on this branch, but should passAnd I suspect we will have to add bidirectional inference for |
| /// Returns a synthesized `TypedDict` with the same keys and value types, but with all keys | ||
| /// marked `NotRequired`. The result is always a `Synthesized` variant (the original class | ||
| /// identity, if any, is not preserved). | ||
| pub(crate) fn to_partial(self, db: &'db dyn Db) -> Self { | ||
| let items: TypedDictSchema<'db> = self | ||
| .items(db) | ||
| .iter() | ||
| .map(|(name, field)| (name.clone(), field.clone().with_required(false))) | ||
| .collect(); | ||
|
|
||
| Self::Synthesized(SynthesizedTypedDictType::new(db, items)) | ||
| } |
There was a problem hiding this comment.
@sharkdp and I had a conversation over DMs after your original PR and concluded that the synthesized TypedDict should probably also be closed=True in order for the approach to be sound. We don't support PEP 728 yet, so we can't implement that right now, but we should probably add a TODO comment.
|
Okay, let me update this branch to use the synthesized approach and add those tests. |
8ac2cf6 to
9a6db7f
Compare
|
BTW, in the updated implementation, I actually needed three overloads... Where If you only have the first and third overload, then partial dicts on the RHS aren't accepted. If you only have the second and third, then if |
|
Ah, hmm... can those first two overloads be combined? def __or__(self: Self, value: Self | Partial[Self], /) -> Self
def __or__(self: Self, value: dict[str, Any], /) -> dict[str, object] |
238283e to
a4f6eca
Compare
|
Yeah this one is getting quite challenging to get right everywhere. |
a4f6eca to
032b423
Compare
032b423 to
3d01ece
Compare
dcreager
left a comment
There was a problem hiding this comment.
Couple of naming/comment nits, and a merge conflict to fix, otherwise looks good!
| CallableTypeKind::FunctionLike, | ||
| ))) | ||
| } | ||
| (CodeGeneratorKind::TypedDict, name @ ("__or__" | "__ror__" | "__ior__")) => { |
There was a problem hiding this comment.
Can you add a comment here showing the Python syntax of the signatures you're creating?
| /// Returns a synthesized `TypedDict` used to model PEP 584 update operands. This accepts | ||
| /// dictionary literals that update any subset of known keys, and also accepts other | ||
| /// `TypedDict`s as long as any overlapping keys are compatible. | ||
| pub(crate) fn to_update_operand(self, db: &'db dyn Db) -> Self { |
There was a problem hiding this comment.
nit: I think I prefer the "partial" naming you used in the PR comments. This name talks about (and makes assumptions about) how the result will be used, but something like make_partial talks about the structure of the result more generally.
ibraheemdev
left a comment
There was a problem hiding this comment.
This looks good to me. I think a lot of this special casing can be removed with more general bidirectional inference for binary operator dunder calls, but it seems fine for now.
3d01ece to
8b5edd4
Compare
Summary
Preserve
TypedDicttypes for compatible PEP 584 updates instead of widening todict:Closes astral-sh/ty#2798.