Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/narrow/match.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,3 +442,22 @@ def _(x: tuple[Literal["tag1"], A] | tuple[str, B]):
# But we *can* narrow with inequality
reveal_type(x) # revealed: tuple[str, B]
```

and it is also restricted to `match` patterns that solely consist of value patterns:

```py
class Config:
MODE: str = "default"

def _(u: tuple[Literal["foo"], int] | tuple[Literal["bar"], str]):
match u[0]:
case Config.MODE | "foo":
# Config.mode has type `str` (not a literal), which could match
# any string value at runtime. We cannot narrow based on "foo" alone
# because the actual match might have been against Config.mode.
reveal_type(u) # revealed: tuple[Literal["foo"], int] | tuple[Literal["bar"], str]
case "bar":
# Since the previous case could match any string, this case can
# still narrow to `tuple[Literal["bar"], str]` when `u[0]` equals "bar".
reveal_type(u) # revealed: tuple[Literal["bar"], str]
```
27 changes: 27 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/typed_dict.md
Original file line number Diff line number Diff line change
Expand Up @@ -2324,6 +2324,33 @@ def match_non_literal(u: Foo | NonLiteralTD):
reveal_type(u) # revealed: NonLiteralTD
```

and it is also restricted to `match` patterns that solely consist of value patterns:

```py
class Config:
MODE: str = "default"

class Foo(TypedDict):
tag: Literal["foo"]
data: int

class Bar(TypedDict):
tag: Literal["bar"]
data: str

def test_or_pattern_with_non_literal(u: Foo | Bar):
match u["tag"]:
case Config.MODE | "foo":
# Config.mode has type `str` (not a literal), which could match
# any string value at runtime. We cannot narrow based on "foo" alone
# because the actual match might have been against Config.mode.
reveal_type(u) # revealed: Foo | Bar
case "bar":
# Since the previous case could match any string, this case can
# still narrow to `Bar` when tag equals "bar".
reveal_type(u) # revealed: Bar
```

We can still narrow `Literal` tags even when non-`TypedDict` types are present in the union:

```py
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,15 @@ fn pattern_kind_to_type<'db>(db: &'db dyn Db, kind: &PatternPredicateKind<'db>)
match kind {
PatternPredicateKind::Singleton(singleton) => singleton_to_type(db, *singleton),
PatternPredicateKind::Value(value) => {
infer_expression_type(db, *value, TypeContext::default())
let ty = infer_expression_type(db, *value, TypeContext::default());
// Only return the type if it's single-valued. For non-single-valued types
// (like `str`), we can't definitively exclude any specific type from
// subsequent patterns because the pattern could match any value of that type.
if ty.is_single_valued(db) {
ty
} else {
Type::Never
}
}
PatternPredicateKind::Class(class_expr, kind) => {
if kind.is_irrefutable() {
Expand Down
3 changes: 2 additions & 1 deletion crates/ty_python_semantic/src/types/narrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1524,7 +1524,8 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
.evaluate_expr_compare_op(subject_ty, value_ty, ast::CmpOp::Eq, is_positive)
.map(|ty| {
NarrowingConstraints::from_iter([(place, NarrowingConstraint::intersection(ty))])
})?;
})
.unwrap_or_default();

// Narrow tagged unions of `TypedDict`s with `Literal` keys, for example:
//
Expand Down
Loading