Skip to content
Open
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
4 changes: 3 additions & 1 deletion pyrefly/lib/alt/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4236,7 +4236,9 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
),
Type::Type(f) if matches!(&*f, Type::Quantified(q) if q.is_type_var()) => {
// Repeated match because pattern guards cannot move out of bindings.
let Type::Quantified(quantified) = *f else { unreachable!("guarded by matches! above") };
let Type::Quantified(quantified) = *f else {
unreachable!("guarded by matches! above")
};
let quantified = *quantified;
let base_display_ty =
self.heap.mk_type_of(self.heap.mk_quantified(quantified.clone()));
Expand Down
24 changes: 18 additions & 6 deletions pyrefly/lib/alt/solve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2956,18 +2956,30 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
if iterable_ty.ty().is_never() {
return EmptyAnswer;
}
// String and bytes literals have known length, so generate a fixed-length iterable
let iterables = match iterable_ty.ty() {
let mut iterables = Vec::new();
self.map_over_union(iterable_ty.ty(), |ty| match ty {
// String and bytes literals have known length.
Type::Literal(lit) if let Lit::Str(s) = &lit.value => {
let char_ty = self.heap.mk_literal_string(LitStyle::Implicit);
vec![Iterable::FixedLen(vec![char_ty; s.chars().count()])]
iterables.push(Iterable::FixedLen(vec![char_ty; s.chars().count()]));
}
Type::Literal(lit) if let Lit::Bytes(b) = &lit.value => {
let elem_ty = self.heap.mk_class_type(self.stdlib.int().clone());
vec![Iterable::FixedLen(vec![elem_ty; b.len()])]
iterables.push(Iterable::FixedLen(vec![elem_ty; b.len()]));
}
_ => self.iterate(iterable_ty.ty(), *range, errors, None),
};
_ if !matches!(expect, SizeExpectation::Ge(0))
&& (matches!(ty, Type::ClassType(cls) if cls == self.stdlib.str())
|| matches!(ty, Type::LiteralString(_))) =>
{
self.error(
errors,
*range,
ErrorKind::BadUnpacking,
format!("Cannot unpack {} into {}", ty, expect.message()),
);
}
_ => iterables.extend(self.iterate(ty, *range, errors, None)),
});
for iterable in iterables {
match iterable {
Iterable::OfType(_) => {}
Expand Down
2 changes: 2 additions & 0 deletions pyrefly/lib/alt/unwrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::solver::solver::SubsetError;
use crate::types::callable::Param;
use crate::types::callable::Required;
use crate::types::class::ClassType;
use crate::types::literal::Lit;
use crate::types::tuple::Tuple;
use crate::types::types::Type;
use crate::types::types::Var;
Expand Down Expand Up @@ -254,6 +255,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
{
return false;
}
Type::Literal(lit) if matches!(lit.value, Lit::Str(_) | Lit::Bytes(_)) => return false,
Type::LiteralString(_) => return false,
// Tuples are always sequences for pattern matching
Type::Tuple(_) => return true,
Expand Down
68 changes: 68 additions & 0 deletions pyrefly/lib/test/assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,7 @@ testcase!(
test_unpack_string_exact,
r#"
a, b, c = "abc"
x, y = "12"
"#,
);

Expand All @@ -1009,6 +1010,10 @@ testcase!(
r#"
a, *b = "abc"
x, y, *z = "w" # E: Cannot unpack Literal['w'] (of size 1) into 2+ values
x, *y = "123"
x, *y = "12"
x, *y = "1"
x, *y = "" # E: Cannot unpack Literal[''] (of size 0) into 1+ values
"#,
);

Expand All @@ -1034,6 +1039,69 @@ bad = items[::0] # E: Slice step cannot be zero
"#,
);

testcase!(
test_for_loop_unpack_string,
r#"
for k, v in {"x": 1}: # E: Cannot unpack
pass

xs: list[str] = []
for a, b in xs: # E: Cannot unpack
pass
"#,
);

testcase!(
test_for_loop_unpack_tuple_annotation,
r#"
from __future__ import annotations

x: list[tuple[str, str]] = []
for a, b in x:
pass
"#,
);

testcase!(
test_unpack_string_star_only,
r#"
from typing import LiteralString, assert_type

def f(s: str, literal: LiteralString) -> None:
*chars, = s
assert_type(chars, list[str])
*literal_chars, = literal
assert_type(literal_chars, list[LiteralString])

*empty, = ""
*nonempty, = "123"
"#,
);

testcase!(
test_unpack_string_union,
r#"
from typing import Literal, LiteralString

def f(xs: list[str | tuple[int, int]], literals: list[LiteralString | tuple[int, int]]) -> None:
for a, b in xs: # E: Cannot unpack
pass
for a, *rest in xs: # E: Cannot unpack
pass
for *chars, in xs:
pass
for a, b in literals: # E: Cannot unpack
pass
for *literal_chars, in literals:
pass

def g(s: Literal["12", "34"], short: Literal["", "1"], b: Literal[b"12", b"34"]) -> None:
a, c = s
a, *rest = short # E: Cannot unpack
x, y = b
"#,
);

testcase!(
test_union_gradual_narrow_list_any_or_none,
r#"
Expand Down
33 changes: 33 additions & 0 deletions pyrefly/lib/test/generic_basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,39 @@ use pyrefly_python::sys_info::PythonVersion;
use crate::test::util::TestEnv;
use crate::testcase;

testcase!(
test_parenthesized_generic_arguments,
r#"
from __future__ import annotations
from typing import TypeVar, assert_type

class Box[T]:
def __init__(self, value: T) -> None: ...

type Mapping[K, V] = dict[K, V]
type Wrapped[T] = Box[T]

K = TypeVar("K")
V = TypeVar("V")
T = TypeVar("T")
LegacyMapping = dict[K, V]
LegacyWrapped = Box[T]

def f(d: dict[(str, int)], b: Box[(int,)], m: Mapping[(str, int)], w: Wrapped[(int,)]):
assert_type(d, dict[str, int])
assert_type(b, Box[int])
assert_type(m, dict[str, int])
assert_type(w, Box[int])

assert_type(dict[(str, int)](), dict[str, int])
assert_type(Box[(int,)](1), Box[int])
assert_type(LegacyMapping[(str, int)](), dict[str, int])
assert_type(LegacyWrapped[(int,)](1), Box[int])

x: list[(str, str)] = [] # E: Expected 1 type argument for `list`, got 2
"#,
);

testcase!(
test_tyvar_constructor,
r#"
Expand Down
26 changes: 26 additions & 0 deletions pyrefly/lib/test/pattern_match.rs
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,32 @@ def test_str_not_sequence(x: str | Sequence[int]) -> None:
"#,
);

testcase!(
test_sequence_pattern_string_and_bytes_literals_excluded,
r#"
from typing import Literal, assert_type

type Text = Literal["", "1", "12", "closed", b"", b"1", b"12", b"closed"]

def fixed(x: Text | tuple[int, int]) -> None:
match x:
case [first, second]:
assert_type(x, tuple[int, int])
assert_type(first, int)
assert_type(second, int)
case _:
assert_type(x, Text)

def starred(x: Text | tuple[int, int]) -> None:
match x:
case [*values]:
assert_type(x, tuple[int, int])
assert_type(values, list[int])
case _:
assert_type(x, Text)
"#,
);

testcase!(
test_sequence_pattern_list,
r#"
Expand Down
Loading