Skip to content

Conversation

@asukaminato0721
Copy link
Contributor

Summary

Fixes #650

this is stack on #2252

Implemented ellipsis guards (is, is not, ==, !=) by treating ... as a singleton and also handling EllipsisType as its class counterpart for negative narrowing.

Test Plan

Added tests that validate narrowing via assignment to types.EllipsisType/int.

@meta-cla meta-cla bot added the cla signed label Jan 29, 2026
@github-actions
Copy link

Diff from mypy_primer, showing the effect of this PR on open source code:

urllib3 (https://github.com/urllib3/urllib3)
- ERROR src/urllib3/util/timeout.py:128:16-79: Returned type `_TYPE_DEFAULT | float | None` is not assignable to declared return type `float | None` [bad-return]
- ERROR src/urllib3/util/timeout.py:150:19-24: Argument `_TYPE_DEFAULT | float` is not assignable to parameter `x` with type `Buffer | SupportsFloat | SupportsIndex | str` in function `float.__new__` [bad-argument-type]
- ERROR src/urllib3/util/timeout.py:158:16-26: `<=` is not supported between `_TYPE_DEFAULT` and `Literal[0]` [unsupported-operation]
- ERROR src/urllib3/util/timeout.py:270:24-34: Returned type `_TYPE_DEFAULT | float` is not assignable to declared return type `float | None` [bad-return]
- ERROR src/urllib3/util/timeout.py:271:30-84: No matching overload found for function `min` called with arguments: (float, _TYPE_DEFAULT | float) [no-matching-overload]
+ ERROR src/urllib3/util/timeout.py:271:23-85: No matching overload found for function `max` called with arguments: (Literal[0], float) [no-matching-overload]
- ERROR src/urllib3/util/timeout.py:271:31-71: `-` is not supported between `_TYPE_DEFAULT` and `float` [unsupported-operation]
- ERROR src/urllib3/util/timeout.py:273:27-67: `-` is not supported between `_TYPE_DEFAULT` and `float` [unsupported-operation]
- ::error file=src/urllib3/util/timeout.py,line=128,col=16,endLine=128,endColumn=79,title=Pyrefly bad-return::Returned type `_TYPE_DEFAULT | float | None` is not assignable to declared return type `float | None`
- ::error file=src/urllib3/util/timeout.py,line=150,col=19,endLine=150,endColumn=24,title=Pyrefly bad-argument-type::Argument `_TYPE_DEFAULT | float` is not assignable to parameter `x` with type `Buffer | SupportsFloat | SupportsIndex | str` in function `float.__new__`
- ::error file=src/urllib3/util/timeout.py,line=158,col=16,endLine=158,endColumn=26,title=Pyrefly unsupported-operation::`<=` is not supported between `_TYPE_DEFAULT` and `Literal[0]`%0A  Argument `_TYPE_DEFAULT` is not assignable to parameter `value` with type `int` in function `int.__ge__`
- ::error file=src/urllib3/util/timeout.py,line=270,col=24,endLine=270,endColumn=34,title=Pyrefly bad-return::Returned type `_TYPE_DEFAULT | float` is not assignable to declared return type `float | None`
- ::error file=src/urllib3/util/timeout.py,line=271,col=30,endLine=271,endColumn=84,title=Pyrefly no-matching-overload::No matching overload found for function `min` called with arguments: (float, _TYPE_DEFAULT | float)%0A  Possible overloads:%0A  (arg1: SupportsRichComparisonT, arg2: SupportsRichComparisonT, /, *_args: SupportsRichComparisonT, *, key: None = None) -> SupportsRichComparisonT [closest match]%0A  (arg1: _T, arg2: _T, /, *_args: _T, *, key: (_T) -> SupportsRichComparison) -> _T%0A  (iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None) -> SupportsRichComparisonT%0A  (iterable: Iterable[_T], /, *, key: (_T) -> SupportsRichComparison) -> _T%0A  (iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None, default: _T) -> SupportsRichComparisonT | _T%0A  (iterable: Iterable[_T1], /, *, key: (_T1) -> SupportsRichComparison, default: _T2) -> _T1 | _T2
+ ::error file=src/urllib3/util/timeout.py,line=271,col=23,endLine=271,endColumn=85,title=Pyrefly no-matching-overload::No matching overload found for function `max` called with arguments: (Literal[0], float)%0A  Possible overloads:%0A  (arg1: SupportsRichComparisonT, arg2: SupportsRichComparisonT, /, *_args: SupportsRichComparisonT, *, key: None = None) -> SupportsRichComparisonT [closest match]%0A  (arg1: _T, arg2: _T, /, *_args: _T, *, key: (_T) -> SupportsRichComparison) -> _T%0A  (iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None) -> SupportsRichComparisonT%0A  (iterable: Iterable[_T], /, *, key: (_T) -> SupportsRichComparison) -> _T%0A  (iterable: Iterable[SupportsRichComparisonT], /, *, key: None = None, default: _T) -> SupportsRichComparisonT | _T%0A  (iterable: Iterable[_T1], /, *, key: (_T1) -> SupportsRichComparison, default: _T2) -> _T1 | _T2
- ::error file=src/urllib3/util/timeout.py,line=271,col=31,endLine=271,endColumn=71,title=Pyrefly unsupported-operation::`-` is not supported between `_TYPE_DEFAULT` and `float`%0A  Argument `_TYPE_DEFAULT` is not assignable to parameter `value` with type `float` in function `float.__rsub__`
- ::error file=src/urllib3/util/timeout.py,line=273,col=27,endLine=273,endColumn=67,title=Pyrefly unsupported-operation::`-` is not supported between `_TYPE_DEFAULT` and `float`%0A  Argument `_TYPE_DEFAULT` is not assignable to parameter `value` with type `float` in function `float.__rsub__`

xarray (https://github.com/pydata/xarray)
- ERROR xarray/core/dataset.py:8274:23-28: No matching overload found for function `set.__init__` called with arguments: (Collection[Hashable] | EllipsisType) [no-matching-overload]
- ERROR xarray/core/groupby.py:1107:32-35: Argument `Collection[Hashable] | EllipsisType` is not assignable to parameter `iterable` with type `Iterable[Hashable]` in function `tuple.__new__` [bad-argument-type]
- ERROR xarray/core/utils.py:1025:24-29: No matching overload found for function `set.__init__` called with arguments: (Collection[Hashable] | EllipsisType | tuple[str]) [no-matching-overload]
- ERROR xarray/core/utils.py:1026:18-21: Argument `Collection[Hashable] | EllipsisType | tuple[str]` is not assignable to parameter `iterable` with type `Iterable[Hashable]` in function `tuple.__new__` [bad-argument-type]
- ERROR xarray/core/utils.py:1064:14-19: No matching overload found for function `set.__init__` called with arguments: (Collection[Hashable] | EllipsisType | set[Hashable]) [no-matching-overload]
- ERROR xarray/core/utils.py:1122:76-86: `in` is not supported between `Ellipsis` and `EllipsisType` [not-iterable]
- ERROR xarray/core/utils.py:1123:53-58: No matching overload found for function `set.__init__` called with arguments: (Collection[Hashable] | EllipsisType) [no-matching-overload]
- ERROR xarray/core/utils.py:1129:22-25: Argument `Collection[Hashable] | EllipsisType` is not assignable to parameter `iterable` with type `Iterable[Hashable]` in function `tuple.__new__` [bad-argument-type]
- ERROR xarray/namedarray/core.py:916:28-35: `object` is not assignable to variable `axis` with type `Sequence[int] | int | None` [bad-assignment]
+ ERROR xarray/namedarray/core.py:916:28-35: `int | object` is not assignable to variable `axis` with type `Sequence[int] | int | None` [bad-assignment]
- ::error file=xarray/core/dataset.py,line=8274,col=23,endLine=8274,endColumn=28,title=Pyrefly no-matching-overload::No matching overload found for function `set.__init__` called with arguments: (Collection[Hashable] | EllipsisType)%0A  Possible overloads:%0A  () -> None%0A  (iterable: Iterable[Hashable], /) -> None [closest match]
- ::error file=xarray/core/groupby.py,line=1107,col=32,endLine=1107,endColumn=35,title=Pyrefly bad-argument-type::Argument `Collection[Hashable] | EllipsisType` is not assignable to parameter `iterable` with type `Iterable[Hashable]` in function `tuple.__new__`%0A  Protocol `Iterable` requires attribute `__iter__`
- ::error file=xarray/core/utils.py,line=1025,col=24,endLine=1025,endColumn=29,title=Pyrefly no-matching-overload::No matching overload found for function `set.__init__` called with arguments: (Collection[Hashable] | EllipsisType | tuple[str])%0A  Possible overloads:%0A  () -> None%0A  (iterable: Iterable[Hashable], /) -> None [closest match]
- ::error file=xarray/core/utils.py,line=1026,col=18,endLine=1026,endColumn=21,title=Pyrefly bad-argument-type::Argument `Collection[Hashable] | EllipsisType | tuple[str]` is not assignable to parameter `iterable` with type `Iterable[Hashable]` in function `tuple.__new__`%0A  Protocol `Iterable` requires attribute `__iter__`
- ::error file=xarray/core/utils.py,line=1064,col=14,endLine=1064,endColumn=19,title=Pyrefly no-matching-overload::No matching overload found for function `set.__init__` called with arguments: (Collection[Hashable] | EllipsisType | set[Hashable])%0A  Possible overloads:%0A  () -> None%0A  (iterable: Iterable[Hashable], /) -> None [closest match]
- ::error file=xarray/core/utils.py,line=1122,col=76,endLine=1122,endColumn=86,title=Pyrefly not-iterable::`in` is not supported between `Ellipsis` and `EllipsisType`
- ::error file=xarray/core/utils.py,line=1123,col=53,endLine=1123,endColumn=58,title=Pyrefly no-matching-overload::No matching overload found for function `set.__init__` called with arguments: (Collection[Hashable] | EllipsisType)%0A  Possible overloads:%0A  () -> None%0A  (iterable: Iterable[EllipsisType | Hashable], /) -> None [closest match]
- ::error file=xarray/core/utils.py,line=1129,col=22,endLine=1129,endColumn=25,title=Pyrefly bad-argument-type::Argument `Collection[Hashable] | EllipsisType` is not assignable to parameter `iterable` with type `Iterable[Hashable]` in function `tuple.__new__`%0A  Protocol `Iterable` requires attribute `__iter__`
- ::error file=xarray/namedarray/core.py,line=916,col=28,endLine=916,endColumn=35,title=Pyrefly bad-assignment::`object` is not assignable to variable `axis` with type `Sequence[int] | int | None`
+ ::error file=xarray/namedarray/core.py,line=916,col=28,endLine=916,endColumn=35,title=Pyrefly bad-assignment::`int | object` is not assignable to variable `axis` with type `Sequence[int] | int | None`

sphinx (https://github.com/sphinx-doc/sphinx)
- ERROR sphinx/theming.py:542:65-81: Object of class `EllipsisType` has no attribute `split` [missing-attribute]
- ERROR sphinx/theming.py:550:62-75: Object of class `EllipsisType` has no attribute `split` [missing-attribute]
+ ERROR sphinx/theming.py:542:53-87: No matching overload found for function `map.__new__` called with arguments: (type[map[_S]], Overload[
+   (self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString
+   (self: str, chars: str | None = None, /) -> str
+ ], list[str] | Unknown) [no-matching-overload]
+ ERROR sphinx/theming.py:550:50-81: No matching overload found for function `map.__new__` called with arguments: (type[map[_S]], Overload[
+   (self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString
+   (self: str, chars: str | None = None, /) -> str
+ ], list[str] | Unknown) [no-matching-overload]
- ::error file=sphinx/theming.py,line=542,col=65,endLine=542,endColumn=81,title=Pyrefly missing-attribute::Object of class `EllipsisType` has no attribute `split`
- ::error file=sphinx/theming.py,line=550,col=62,endLine=550,endColumn=75,title=Pyrefly missing-attribute::Object of class `EllipsisType` has no attribute `split`
+ ::error file=sphinx/theming.py,line=542,col=53,endLine=542,endColumn=87,title=Pyrefly no-matching-overload::No matching overload found for function `map.__new__` called with arguments: (type[map[_S]], Overload[%0A  (self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString%0A  (self: str, chars: str | None = None, /) -> str%0A], list[str] | Unknown)%0A  Possible overloads:%0A  (cls: type[map[_S]], func: (_T1) -> _S, iterable: Iterable[_T1], /) -> map[_S] [closest match]%0A  (cls: type[map[_S]], func: (_T1, _T2) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], /) -> map[_S]%0A  (cls: type[map[_S]], func: (_T1, _T2, _T3) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], /) -> map[_S]%0A  (cls: type[map[_S]], func: (_T1, _T2, _T3, _T4) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], /) -> map[_S]%0A  (cls: type[map[_S]], func: (_T1, _T2, _T3, _T4, _T5) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], iter5: Iterable[_T5], /) -> map[_S]%0A  (cls: type[map[_S]], func: (...) -> _S, iterable: Iterable[Any], iter2: Iterable[Any], iter3: Iterable[Any], iter4: Iterable[Any], iter5: Iterable[Any], iter6: Iterable[Any], /, *iterables: Iterable[Any]) -> map[_S]
+ ::error file=sphinx/theming.py,line=550,col=50,endLine=550,endColumn=81,title=Pyrefly no-matching-overload::No matching overload found for function `map.__new__` called with arguments: (type[map[_S]], Overload[%0A  (self: LiteralString, chars: LiteralString | None = None, /) -> LiteralString%0A  (self: str, chars: str | None = None, /) -> str%0A], list[str] | Unknown)%0A  Possible overloads:%0A  (cls: type[map[_S]], func: (_T1) -> _S, iterable: Iterable[_T1], /) -> map[_S] [closest match]%0A  (cls: type[map[_S]], func: (_T1, _T2) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], /) -> map[_S]%0A  (cls: type[map[_S]], func: (_T1, _T2, _T3) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], /) -> map[_S]%0A  (cls: type[map[_S]], func: (_T1, _T2, _T3, _T4) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], /) -> map[_S]%0A  (cls: type[map[_S]], func: (_T1, _T2, _T3, _T4, _T5) -> _S, iterable: Iterable[_T1], iter2: Iterable[_T2], iter3: Iterable[_T3], iter4: Iterable[_T4], iter5: Iterable[_T5], /) -> map[_S]%0A  (cls: type[map[_S]], func: (...) -> _S, iterable: Iterable[Any], iter2: Iterable[Any], iter3: Iterable[Any], iter4: Iterable[Any], iter5: Iterable[Any], iter6: Iterable[Any], /, *iterables: Iterable[Any]) -> map[_S]

@asukaminato0721 asukaminato0721 marked this pull request as ready for review January 29, 2026 12:48
Copilot AI review requested due to automatic review settings January 29, 2026 12:48
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements type guard narrowing for ellipsis literals (...) to support identity and equality checks as specified in issue #650. The implementation treats ... as a singleton and handles both the Ellipsis type and EllipsisType class for comprehensive narrowing support.

Changes:

  • Added ellipsis narrowing logic for is, is not, ==, and != operators
  • Extended narrowing to handle EllipsisType class in negative narrowing scenarios
  • Added test cases for ellipsis identity and equality checks

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pyrefly/lib/alt/narrow.rs Core narrowing logic for ellipsis guards, including is_ellipsis_class_type helper and updates to all four comparison operators
pyrefly/lib/test/narrow.rs Added test cases for is ... and == ... / != ... narrowing
crates/pyrefly_types/src/literal.rs Added LitSentinel struct (from stacked PR #2252)
crates/pyrefly_types/src/display.rs Display formatting for Lit::Sentinel
crates/pyrefly_types/src/type_output.rs Output handling for Lit::Sentinel
Cargo.lock Checksum update for backtrace crate (automated)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +183 to +203
testcase!(
test_ellipsis_is,
r#"
from types import EllipsisType
def f(x: object):
if x is ...:
y: EllipsisType = x
"#,
);

testcase!(
test_ellipsis_eq,
r#"
from types import EllipsisType
def f(x: int | EllipsisType):
if x == ...:
y_ellipsis: EllipsisType = x
if x != ...:
y_int: int = x
"#,
);
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

Test coverage is incomplete. The implementation supports is not ... narrowing (lines 560-595, 970-1008), but there's no test case validating this behavior. Consider adding a test case like:

def f(x: int | EllipsisType):
    if x is not ...:
        y_int: int = x

to ensure the negative narrowing works correctly.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tracker: Support all type guards from Pyright

1 participant