Skip to content

Commit e804d01

Browse files
rchen152facebook-github-bot
authored andcommitted
Fix a small bug in assignability checking
Summary: Fixes a bug where we didn't take readonly-ness into account. Also fixes a comment that described the assignability relationship backwards. Confusingly, the spec and the conformance tests flip the names they use for the "got" and "want" TypedDicts when describing the relationship (i.e., the spec says B is assignable to A if <blah>, whereas the conformance tests say A is assignable to B if <blah>), which may be why we got this wrong. Reviewed By: samwgoldman Differential Revision: D78776806 fbshipit-source-id: 12345d101adbbc57979acb4aa455a35973697471
1 parent 511b59b commit e804d01

5 files changed

Lines changed: 28 additions & 23 deletions

File tree

conformance/third_party/conformance.exp

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10087,16 +10087,6 @@
1008710087
"stop_column": 15,
1008810088
"stop_line": 40
1008910089
},
10090-
{
10091-
"code": -2,
10092-
"column": 14,
10093-
"concise_description": "`TypedDict[C2]` is not assignable to `TypedDict[A2]`",
10094-
"description": "`TypedDict[C2]` is not assignable to `TypedDict[A2]`",
10095-
"line": 79,
10096-
"name": "bad-assignment",
10097-
"stop_column": 15,
10098-
"stop_line": 79
10099-
},
1010010090
{
1010110091
"code": -2,
1010210092
"column": 14,

conformance/third_party/conformance.result

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -470,9 +470,7 @@
470470
],
471471
"typeddicts_operations.py": [],
472472
"typeddicts_readonly.py": [],
473-
"typeddicts_readonly_consistency.py": [
474-
"Line 79: Unexpected errors ['`TypedDict[C2]` is not assignable to `TypedDict[A2]`']"
475-
],
473+
"typeddicts_readonly_consistency.py": [],
476474
"typeddicts_readonly_inheritance.py": [
477475
"Line 98: Expected 1 errors",
478476
"Line 106: Expected 1 errors",

conformance/third_party/results.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"total": 136,
3-
"pass": 71,
4-
"fail": 65,
5-
"pass_rate": 0.52,
6-
"differences": 288,
3+
"pass": 72,
4+
"fail": 64,
5+
"pass_rate": 0.53,
6+
"differences": 287,
77
"passing": [
88
"aliases_explicit.py",
99
"aliases_newtype.py",
@@ -73,6 +73,7 @@
7373
"typeddicts_final.py",
7474
"typeddicts_operations.py",
7575
"typeddicts_readonly.py",
76+
"typeddicts_readonly_consistency.py",
7677
"typeddicts_readonly_kwargs.py",
7778
"typeddicts_readonly_update.py",
7879
"typeddicts_usage.py"
@@ -139,7 +140,6 @@
139140
"tuples_type_compat.py": 2,
140141
"typeddicts_class_syntax.py": 4,
141142
"typeddicts_inheritance.py": 1,
142-
"typeddicts_readonly_consistency.py": 1,
143143
"typeddicts_readonly_inheritance.py": 4,
144144
"typeddicts_required.py": 2,
145145
"typeddicts_type_consistency.py": 1

pyrefly/lib/solver/subset.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -740,8 +740,8 @@ impl<'a, Ans: LookupAnswer> Subset<'a, Ans> {
740740
(Type::TypedDict(got), Type::TypedDict(want)) => {
741741
// For each key in `want`, `got` has the corresponding key
742742
// and the corresponding value type in `got` is consistent with the value type in `want`.
743-
// For each required key in `got`, the corresponding key is required in `want`.
744-
// For each non-required key in `got`, the corresponding key is not required in `want`.
743+
// For each required key in `want`, the corresponding key is required in `got`.
744+
// For each non-required, non-readonly key in `want`, the corresponding key is not required in `got`.
745745
let got_fields = self.type_order.typed_dict_fields(got);
746746
let want_fields = self.type_order.typed_dict_fields(want);
747747

@@ -757,9 +757,13 @@ impl<'a, Ans: LookupAnswer> Subset<'a, Ans> {
757757
}
758758
})
759759
}) && got_fields.iter().all(|(k, got_v)| {
760-
want_fields
761-
.get(k)
762-
.is_none_or(|want_v| got_v.required == want_v.required)
760+
want_fields.get(k).is_none_or(|want_v| {
761+
if want_v.required {
762+
got_v.required
763+
} else {
764+
want_v.is_read_only() || !got_v.required
765+
}
766+
})
763767
})
764768
}
765769
(Type::TypedDict(got), Type::PartialTypedDict(want)) => {

pyrefly/lib/test/typed_dict.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,3 +1003,16 @@ class TD3(TypedDict, bar="test", baz=False): # E: TypedDict does not support ke
10031003
x: int
10041004
"#,
10051005
);
1006+
1007+
testcase!(
1008+
test_nonrequired_readonly,
1009+
r#"
1010+
from typing import NotRequired, ReadOnly, Required, TypedDict
1011+
class A(TypedDict):
1012+
x: NotRequired[ReadOnly[str]]
1013+
class B(TypedDict):
1014+
x: Required[str]
1015+
b: B = {'x': ''}
1016+
a: A = b
1017+
"#,
1018+
);

0 commit comments

Comments
 (0)