Skip to content

Commit d8e7928

Browse files
migeed-zmeta-codesync[bot]
authored andcommitted
Support invariant containers
Summary: For issue #1799 Invariant containers have to be handled differently than the rest of the containers. There are two options I can think of 1- Do not expand the inner type of the container. This wil make lax mode more strict 2- Choose a covariant type as the output type to invariant container type. This option is more flexible but can accept containers that shouldn't be accepted I went with option 2 for this solution because 1- It's more flexible and 2- The signature will look better on the IDE If there are any other ideas people think I should consider, I'd be happy to look into those as well. I tested the example list/set example on pydantic as well, and it works as expected. So Pydantic does provide this level of flexibility. Reviewed By: yangdanny97 Differential Revision: D88768895 fbshipit-source-id: a2929229eee3c4206ff2617d6eaf20bdfa2c981d
1 parent 70e7e29 commit d8e7928

File tree

3 files changed

+72
-16
lines changed

3 files changed

+72
-16
lines changed

crates/pyrefly_types/src/stdlib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,10 @@ impl Stdlib {
370370
Self::apply(&self.list, vec![x])
371371
}
372372

373+
pub fn list_object(&self) -> &Class {
374+
&Self::unwrap(&self.list).0
375+
}
376+
373377
pub fn deque(&self, x: Type) -> ClassType {
374378
Self::apply(&self.deque, vec![x])
375379
}
@@ -410,6 +414,10 @@ impl Stdlib {
410414
Self::apply(&self.set, vec![x])
411415
}
412416

417+
pub fn set_object(&self) -> &Class {
418+
&Self::unwrap(&self.set).0
419+
}
420+
413421
pub fn iterable(&self, x: Type) -> ClassType {
414422
Self::apply(&self.iterable, vec![x])
415423
}

pyrefly/lib/alt/class/pydantic_lax.rs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,17 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
137137
class_obj: &Class,
138138
expanded_targs: &[Type],
139139
) -> Option<Type> {
140-
// Extract first type argument (element type for most containers, key type for dict)
140+
// Extract first type argument
141141
let first_ty = expanded_targs
142142
.first()
143143
.cloned()
144144
.unwrap_or_else(Type::any_implicit);
145145

146+
// Invariant single-element containers: use Iterable to allow covariance
147+
if class_obj == self.stdlib.list_object() || class_obj == self.stdlib.set_object() {
148+
return Some(self.stdlib.iterable(first_ty).to_type());
149+
}
150+
146151
// Single-element containers
147152
if class_obj.has_toplevel_qname(ModuleName::collections().as_str(), "deque") {
148153
return Some(self.class_types_to_union(vec![
@@ -165,19 +170,6 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
165170
self.stdlib.tuple(first_ty),
166171
]));
167172
}
168-
169-
// Two-element containers
170-
if class_obj == self.stdlib.dict_object() {
171-
let val_ty = expanded_targs
172-
.get(1)
173-
.cloned()
174-
.unwrap_or_else(Type::any_implicit);
175-
return Some(self.class_types_to_union(vec![
176-
self.stdlib.dict(first_ty.clone(), val_ty.clone()),
177-
self.stdlib.mapping(first_ty, val_ty),
178-
]));
179-
}
180-
181173
None
182174
}
183175

@@ -187,6 +179,15 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
187179
Type::ClassType(cls) if !cls.targs().as_slice().is_empty() => {
188180
let class_obj = cls.class_object();
189181
let targs = cls.targs().as_slice();
182+
183+
// Special handling for dict: don't expand key type (Mapping is invariant in key)
184+
if class_obj == self.stdlib.dict_object() {
185+
let key_ty = targs.first().cloned().unwrap_or_else(Type::any_implicit);
186+
let val_ty = targs.get(1).cloned().unwrap_or_else(Type::any_implicit);
187+
let expanded_val = self.expand_type_for_lax_mode(&val_ty);
188+
return self.stdlib.mapping(key_ty, expanded_val).to_type();
189+
}
190+
190191
let expanded_targs = self.expand_types(targs);
191192

192193
// Check for container type conversions

pyrefly/lib/test/pydantic/strictness.rs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ from pydantic import BaseModel
172172
class Model(BaseModel):
173173
x: List[int] = [0, 1]
174174
175-
reveal_type(Model.__init__) # E: revealed type: (self: Model, *, x: list[LaxInt] = ..., **Unknown) -> None
175+
# list[int] is converted to Iterable[LaxInt] to handle invariance
176+
reveal_type(Model.__init__) # E: revealed type: (self: Model, *, x: Iterable[LaxInt] = ..., **Unknown) -> None
176177
177178
class Model2(BaseModel):
178179
q: deque[int]
@@ -182,12 +183,18 @@ reveal_type(Model2.__init__) # E: revealed type: (self: Model2, *, q: deque[LaxI
182183
class Model3(BaseModel):
183184
d: dict[str, int]
184185
185-
reveal_type(Model3.__init__) # E: revealed type: (self: Model3, *, d: Mapping[LaxStr, LaxInt] | dict[LaxStr, LaxInt], **Unknown) -> None
186+
reveal_type(Model3.__init__) # E: revealed type: (self: Model3, *, d: Mapping[str, LaxInt], **Unknown) -> None
186187
187188
class Model4(BaseModel):
188189
f: frozenset[int]
189190
190191
reveal_type(Model4.__init__) # E: revealed type: (self: Model4, *, f: deque[LaxInt] | dict_keys[LaxInt, LaxInt] | dict_values[LaxInt, LaxInt] | frozenset[LaxInt] | list[LaxInt] | set[LaxInt] | tuple[Decimal | bool | bytes | float | int | str, ...], **Unknown) -> None
192+
193+
class Model5(BaseModel):
194+
s: set[int]
195+
196+
# set[int] is converted to Iterable[LaxInt] to handle invariance
197+
reveal_type(Model5.__init__) # E: revealed type: (self: Model5, *, s: Iterable[LaxInt], **Unknown) -> None
191198
"#,
192199
);
193200

@@ -205,3 +212,43 @@ class Model(BaseModel):
205212
reveal_type(Model.__init__) # E: revealed type: (self: Model, *, y: Decimal | bool | bytes | float | int | str, **Unknown) -> None
206213
"#,
207214
);
215+
216+
pydantic_testcase!(
217+
test_lax_mode_list_and_set_invariance,
218+
r#"
219+
from pydantic import BaseModel
220+
from collections import deque
221+
from typing import reveal_type
222+
223+
class TestModel(BaseModel):
224+
name: str
225+
things: list[str]
226+
tags: set[str]
227+
228+
list_of_things = ["thing1", "thing2"]
229+
set_of_tags = {"tag1", "tag2"}
230+
a = TestModel(name="test", things=list_of_things, tags=set_of_tags)
231+
232+
deque_of_bytes: deque[bytes] = deque([b"thing1", b"thing2"])
233+
b = TestModel(name="test", things=deque_of_bytes, tags=set_of_tags)
234+
235+
# When reading the field back, you get the original declared type (list[str]), not Iterable[LaxStr]
236+
reveal_type(a.things) # E: revealed type: list[str]
237+
reveal_type(a.tags) # E: revealed type: set[str]
238+
"#,
239+
);
240+
241+
pydantic_testcase!(
242+
test_lax_mode_dict_invariance,
243+
r#"
244+
from pydantic import BaseModel
245+
from typing import reveal_type
246+
247+
class TestModel(BaseModel):
248+
name: str
249+
metadata: dict[str, str]
250+
251+
my_dict = {"key1": "value1", "key2": "value2"}
252+
a = TestModel(name="test", metadata=my_dict)
253+
"#,
254+
);

0 commit comments

Comments
 (0)