Skip to content

Commit 6f1fdeb

Browse files
MarcoGorellimeta-codesync[bot]
authored andcommitted
Allow building type qualifiers like NotRequired[...] at runtime (#5450)
Summary: The idea is: - If we have a subscript expression, like `x = my_list[0]` where we know that `my_list` is a `list[int]`, then `x` gets inferred as `int`. That's what https://github.com/meta/pyrefly/blob/8003f9b3d96b207c916c4a5c5ba039fb568cb857/pyrefly/lib/alt/expr.rs#L945-L951 does - If however we have a subscript assignment like `x = NotRequired[int]`, then `NotRequired` is a `_SpecialForm` which is typed to return `object` for `__getitem__`, so we need to infer `x` as `object` rather than reporting it as an invalid annotation - For cases in class definitions, like `class MyClass(TypedDict, total=False)`, however, we want to preserve the current behaviour and raise on something like `extra_items=NotRequired[int]`, which is why in `fn expr_class_keyword` we need to pass down `Some(TypeFormContext::ClassKeyword)` so that `if type_form_context.is_none()` in `expr_infer_without_shape_comment` doesn't evaluate to `true` Note that `x: NotRequired[int]` still gets checked as-before, as this change only covers the `if type_form_context.is_none()` case, and in the `x: NotRequired[int]` case, there is a type form context Fixes #5429 - [x] I am a human, and I fully understand the code I am submitting. I used Claude to generate the code, but the description here is fully written by me Pull Request resolved: #5450 Reviewed By: rchen152 Differential Revision: D128312510 fbshipit-source-id: a6ffbb2c8dc35645e4270cdffc18a112963ef627
1 parent bb5c70b commit 6f1fdeb

4 files changed

Lines changed: 57 additions & 13 deletions

File tree

‎conformance/third_party/conformance.exp‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13742,21 +13742,21 @@
1374213742
{
1374313743
"code": -2,
1374413744
"column": 18,
13745-
"concise_description": "`ClassVar` is not allowed in this context",
13746-
"description": "`ClassVar` is not allowed in this context",
13745+
"concise_description": "`object` is not assignable to `TypeForm[Any]`",
13746+
"description": "`object` is not assignable to `TypeForm[Any]`",
1374713747
"line": 71,
13748-
"name": "invalid-annotation",
13748+
"name": "bad-assignment",
1374913749
"severity": "error",
1375013750
"stop_column": 31,
1375113751
"stop_line": 71
1375213752
},
1375313753
{
1375413754
"code": -2,
1375513755
"column": 18,
13756-
"concise_description": "`Final` is not allowed in this context",
13757-
"description": "`Final` is not allowed in this context",
13756+
"concise_description": "`object` is not assignable to `TypeForm[Any]`",
13757+
"description": "`object` is not assignable to `TypeForm[Any]`",
1375813758
"line": 72,
13759-
"name": "invalid-annotation",
13759+
"name": "bad-assignment",
1376013760
"severity": "error",
1376113761
"stop_column": 28,
1376213762
"stop_line": 72

‎pyrefly/lib/alt/expr.rs‎

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -946,13 +946,32 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
946946
// already been checked.
947947
check_deprecation = false;
948948
}
949-
self.subscript_infer(
950-
&base,
951-
&x.slice,
952-
x.range(),
953-
type_form_context.unwrap_or(TypeFormContext::TypeExpression),
954-
errors,
955-
)
949+
// A qualifier like `NotRequired[int]` is a legal runtime value, which
950+
// typeshed types as `object`.
951+
if type_form_context.is_none()
952+
&& let Type::Type(inner) = base.ty()
953+
&& matches!(
954+
**inner,
955+
Type::SpecialForm(
956+
SpecialForm::ClassVar
957+
| SpecialForm::Final
958+
| SpecialForm::ReadOnly
959+
| SpecialForm::Required
960+
| SpecialForm::NotRequired
961+
)
962+
)
963+
{
964+
self.expr_infer(&x.slice, errors);
965+
TypeInfo::of_ty(self.heap.mk_class_type(self.stdlib.object().clone()))
966+
} else {
967+
self.subscript_infer(
968+
&base,
969+
&x.slice,
970+
x.range(),
971+
type_form_context.unwrap_or(TypeFormContext::TypeExpression),
972+
errors,
973+
)
974+
}
956975
}
957976
}
958977
Expr::Named(x) => match &*x.target {

‎pyrefly/lib/alt/solve.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,11 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
796796
ty: Some(self.expr_infer(&x.slice, errors)),
797797
}
798798
}
799+
// Check subscripts as type forms so that misplaced qualifiers are reported.
800+
Expr::Subscript(_) => Annotation::new_type(
801+
self.expr_infer_impl(x, None, errors, Some(TypeFormContext::ClassKeyword))
802+
.into_ty(),
803+
),
799804
_ => Annotation::new_type(self.expr_infer(x, errors)),
800805
}
801806
}

‎pyrefly/lib/test/simple.rs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2940,6 +2940,26 @@ Z = Annotated[0, 'not a type'] # E: Expected a type form, got instance of `Lite
29402940
"#,
29412941
);
29422942

2943+
// https://github.com/facebook/pyrefly/issues/5429
2944+
testcase!(
2945+
test_qualifier_subscript_as_runtime_value,
2946+
r#"
2947+
from typing import Any, ClassVar, Final, assert_type
2948+
from typing_extensions import NotRequired, ReadOnly, Required
2949+
def mark_optional(hints: dict[str, Any]) -> dict[str, Any]:
2950+
return {key: NotRequired[hint] for key, hint in hints.items()}
2951+
def f(hint: Any) -> None:
2952+
assert_type(NotRequired[hint], object)
2953+
assert_type(Required[int], object)
2954+
assert_type(ReadOnly[int], object)
2955+
assert_type(ClassVar[int], object)
2956+
assert_type(Final[int], object)
2957+
NotRequired[undefined] # E: Could not find name `undefined`
2958+
X = NotRequired[int]
2959+
def g(x: X) -> None: ... # E: Expected a type form, got instance of `object`
2960+
"#,
2961+
);
2962+
29432963
testcase!(
29442964
test_typing_annotated_alias,
29452965
r#"

0 commit comments

Comments
 (0)