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
12 changes: 6 additions & 6 deletions conformance/third_party/conformance.exp
Original file line number Diff line number Diff line change
Expand Up @@ -1715,8 +1715,8 @@
{
"code": -2,
"column": 5,
"concise_description": "Cannot yield from `Generator[A, None, None]`, which is not assignable to declared return type `Generator[B, None, Unknown]`",
"description": "Cannot yield from `Generator[A, None, None]`, which is not assignable to declared return type `Generator[B, None, Unknown]`",
"concise_description": "Cannot yield from `Generator[A]`, which is not assignable to declared return type `Generator[B, None, Unknown]`",
"description": "Cannot yield from `Generator[A]`, which is not assignable to declared return type `Generator[B, None, Unknown]`",
"line": 118,
"name": "invalid-yield",
"severity": "error",
Expand All @@ -1726,8 +1726,8 @@
{
"code": -2,
"column": 5,
"concise_description": "Cannot yield from `Generator[int, None, None]`, which is not assignable to declared return type `Generator[B, None, Unknown]`",
"description": "Cannot yield from `Generator[int, None, None]`, which is not assignable to declared return type `Generator[B, None, Unknown]`",
"concise_description": "Cannot yield from `Generator[int]`, which is not assignable to declared return type `Generator[B, None, Unknown]`",
"description": "Cannot yield from `Generator[int]`, which is not assignable to declared return type `Generator[B, None, Unknown]`",
"line": 119,
"name": "invalid-yield",
"severity": "error",
Expand All @@ -1737,8 +1737,8 @@
{
"code": -2,
"column": 5,
"concise_description": "Cannot yield from `Generator[None, int, None]`, which is not assignable to declared return type `Generator[None, str, Unknown]`",
"description": "Cannot yield from `Generator[None, int, None]`, which is not assignable to declared return type `Generator[None, str, Unknown]`",
"concise_description": "Cannot yield from `Generator[None, int]`, which is not assignable to declared return type `Generator[None, str, Unknown]`",
"description": "Cannot yield from `Generator[None, int]`, which is not assignable to declared return type `Generator[None, str, Unknown]`",
"line": 135,
"name": "invalid-yield",
"severity": "error",
Expand Down
4 changes: 3 additions & 1 deletion crates/pyrefly_types/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,14 @@ impl<'a> TypeDisplayContext<'a> {
}

pub(crate) fn fmt_targs(&self, targs: &TArgs, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !targs.is_empty() {
let display_count = targs.display_count();
if display_count > 0 {
write!(
f,
"[{}]",
commas_iter(|| targs
.iter_paired()
.take(display_count)
.map(|(param, arg)| Fmt(|f| self.fmt_targ(param, arg, f))))
)
} else {
Expand Down
5 changes: 3 additions & 2 deletions crates/pyrefly_types/src/type_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,10 @@ impl TypeOutput for OutputWithLocations<'_> {
// Write each type argument separately with its own location
// This ensures that each type in a union (e.g., int | str) gets its own
// clickable part with a link to its definition
if !targs.is_empty() {
let display_count = targs.display_count();
if display_count > 0 {
self.write_str("[")?;
for (i, ty) in targs.as_slice().iter().enumerate() {
for (i, ty) in targs.as_slice().iter().take(display_count).enumerate() {
if i > 0 {
self.write_str(", ")?;
}
Expand Down
12 changes: 12 additions & 0 deletions crates/pyrefly_types/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,18 @@ impl TArgs {
self.0.1.is_empty()
}

/// Returns the number of type arguments to display, stripping trailing args
/// that match their parameter defaults (WYSIWYG display per issue #2461).
pub fn display_count(&self) -> usize {
let mut last_non_default = 0;
for (i, (param, arg)) in self.iter_paired().enumerate() {
if param.default().is_none() || arg != &param.as_gradual_type() {
last_non_default = i + 1;
}
}
last_non_default
}

/// Apply a substitution to type arguments.
///
/// This is useful mainly to re-express ancestors (which, in the MRO, are in terms of class
Expand Down
2 changes: 1 addition & 1 deletion pyrefly/lib/test/constructors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ class B[T: int | A[Any] = Any]:
def __new__(cls, x: list[A[T]]) -> B[A[T]]: ...

assert_type(B([A(0)]), B[A[int]])
B([A("oops")]) # E: `str` is not assignable to upper bound `A[Any] | int` of type variable `T`
B([A("oops")]) # E: `str` is not assignable to upper bound `A | int` of type variable `T`
"#,
);

Expand Down
8 changes: 4 additions & 4 deletions pyrefly/lib/test/contextual.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,16 +236,16 @@ from typing import Generator, Iterable
class A: ...
class B(A): ...
x0 = ([B()] for _ in [0])
x1a: Generator[list[A], None, None] = x0 # E: `Generator[list[B], None, None]` is not assignable to `Generator[list[A], None, None]`
x1a: Generator[list[A], None, None] = x0 # E: `Generator[list[B]]` is not assignable to `Generator[list[A]]`
x1b: Generator[list[A], None, None] = ([B()] for _ in [0])
x2a: Iterable[list[A]] = x0 # E: `Generator[list[B], None, None]` is not assignable to `Iterable[list[A]]`
x2a: Iterable[list[A]] = x0 # E: `Generator[list[B]]` is not assignable to `Iterable[list[A]]`
x2b: Iterable[list[A]] = ([B()] for _ in [0])

# In theory, we should allow this, since the generator expression accepts _any_ send type,
# but both Mypy and Pyright assume that the send type is `None`.
x3: Generator[int, int, None] = (1 for _ in [1]) # E: `Generator[Literal[1], None, None]` is not assignable to `Generator[int, int, None]`
x3: Generator[int, int, None] = (1 for _ in [1]) # E: `Generator[Literal[1]]` is not assignable to `Generator[int, int]`

x4: Generator[int, None, int] = (1 for _ in [1]) # E: `Generator[Literal[1], None, None]` is not assignable to `Generator[int, None, int]`
x4: Generator[int, None, int] = (1 for _ in [1]) # E: `Generator[Literal[1]]` is not assignable to `Generator[int, None, int]`
"#,
);

Expand Down
4 changes: 2 additions & 2 deletions pyrefly/lib/test/flow_branching.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,9 +332,9 @@ try:
except* int as e1: # E: Invalid exception class
reveal_type(e1) # E: revealed type: ExceptionGroup[int]
except* Exception as e2:
reveal_type(e2) # E: revealed type: ExceptionGroup[Exception]
reveal_type(e2) # E: revealed type: ExceptionGroup
except* ExceptionGroup as e3: # E: Exception handler annotation in `except*` clause may not extend `BaseExceptionGroup`
reveal_type(e3) # E: ExceptionGroup[ExceptionGroup[Exception]]
reveal_type(e3) # E: ExceptionGroup[ExceptionGroup]
except* (Exception1, Exception2) as e4:
reveal_type(e4) # E: ExceptionGroup[Exception1 | Exception2]
except* Exception1 as e5:
Expand Down
78 changes: 78 additions & 0 deletions pyrefly/lib/test/generic_legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,84 @@ def f9(c1: C[int, str], c2: C[str]):
"#,
);

// WYSIWYG display tests for issue #2461:
// When generic type params have defaults, display should omit trailing
// args that match their defaults.

testcase!(
test_wysiwyg_bare_generic_all_defaults,
r#"
from typing import Generic, TypeVar, reveal_type
T = TypeVar('T', default=int)
U = TypeVar('U', default=str)
class MyClass(Generic[T, U]): ...
def f(x: MyClass) -> None:
reveal_type(x) # E: revealed type: MyClass
"#,
);

testcase!(
test_wysiwyg_partial_generic_one_default,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd prefer if we renamed the tests to compact_display or something like that instead of wysiwyg, to avoid it being misleading

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can patch that after import. the rest of it looks fine to me

r#"
from typing import Generic, TypeVar, reveal_type
T = TypeVar('T')
U = TypeVar('U', default=int)
class MyClass(Generic[T, U]): ...
def f(x: MyClass[float]) -> None:
reveal_type(x) # E: revealed type: MyClass[float]
"#,
);

testcase!(
test_wysiwyg_fully_specified_generic,
r#"
from typing import Generic, TypeVar, reveal_type
T = TypeVar('T', default=int)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're missing a test case here for when the explicit user-provided type arguments match the defaults

U = TypeVar('U', default=str)
class MyClass(Generic[T, U]): ...
def f(x: MyClass[float, bool]) -> None:
reveal_type(x) # E: revealed type: MyClass[float, bool]
"#,
);

testcase!(
test_wysiwyg_no_defaults_shows_all,
r#"
from typing import Generic, TypeVar, reveal_type
T = TypeVar('T')
U = TypeVar('U')
class MyClass(Generic[T, U]): ...
def f(x: MyClass[int, str]) -> None:
reveal_type(x) # E: revealed type: MyClass[int, str]
"#,
);

testcase!(
test_wysiwyg_middle_default_not_stripped,
r#"
from typing import Generic, TypeVar, reveal_type
T = TypeVar('T')
U = TypeVar('U', default=int)
V = TypeVar('V')
class MyClass(Generic[T, U, V]): # E: Type parameter `V` without a default cannot follow type parameter `U` with a default
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any assertions on what type is printed

...
def f(x: MyClass[str, int, bool]) -> None:
reveal_type(x) # E: revealed type: MyClass[str, int, bool]
"#,
);

testcase!(
test_wysiwyg_explicit_args_match_defaults,
r#"
from typing import Generic, TypeVar, reveal_type
T = TypeVar('T', default=int)
U = TypeVar('U', default=str)
class MyClass(Generic[T, U]): ...
def f(x: MyClass[int, str]) -> None:
reveal_type(x) # E: revealed type: MyClass
"#,
);

testcase!(
test_bad_default_order,
r#"
Expand Down
2 changes: 1 addition & 1 deletion pyrefly/lib/test/operators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ testcase!(
test_in_generator,
r#"
'x' in (x for x in ['y'])
42 in (x for x in ['y']) # E: `in` is not supported between `Literal[42]` and `Generator[str, None, None]`
42 in (x for x in ['y']) # E: `in` is not supported between `Literal[42]` and `Generator[str]`
"#,
);

Expand Down
6 changes: 3 additions & 3 deletions pyrefly/lib/test/yields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@ def bar() -> Generator[Y, S2, None]:
assert_type(r, R)

def baz() -> Generator[Y2, S2, None]:
s = yield from bar() # E: Cannot yield from `Generator[Y, S2, None]`, which is not assignable to declared return type `Generator[Y2, S2, Unknown]`
s = yield from bar() # E: Cannot yield from `Generator[Y, S2]`, which is not assignable to declared return type `Generator[Y2, S2, Unknown]`
assert_type(s, None)

def qux() -> Generator[Y, S, None]:
s = yield from bar() # E: Cannot yield from `Generator[Y, S2, None]`, which is not assignable to declared return type `Generator[Y, S, Unknown]`
s = yield from bar() # E: Cannot yield from `Generator[Y, S2]`, which is not assignable to declared return type `Generator[Y, S, Unknown]`
assert_type(s, None)
"#,
);
Expand Down Expand Up @@ -405,7 +405,7 @@ async def test() -> None:
assert_type(x, int)
async for y in [1, 2, 3]: # E: Type `list[int]` is not an async iterable
pass
for z in gen(): # E: Type `AsyncGenerator[int, None]` is not iterable
for z in gen(): # E: Type `AsyncGenerator[int]` is not iterable
pass
"#,
);
Expand Down