Skip to content

Commit c6d1070

Browse files
authored
Run ty on typeshed stubs in CI (#16013)
Add a pinned ty check for the standard-library and third-party stubs across Python 3.10-3.14 and the supported target platforms. The runner honors stdlib/VERSIONS, resolves checked-in stub packages and their external dependencies, and avoids duplicate published stub packages that shadow local sources. Match pyright's policy for non-actionable override and deprecation diagnostics, add narrowly scoped ignores beside existing checker exceptions, and exclude only the obsolete requests and legacy distutils stubs. Check geopandas, seaborn, and shapely on every target version, with two existing pandas-stubs type-bound exceptions mirrored for ty. Also fix the remaining Windows-only dateutil builtin-name collision discovered by the new check. Closes #15999.
1 parent 9f9e8a7 commit c6d1070

24 files changed

Lines changed: 257 additions & 30 deletions

File tree

.github/workflows/tests.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,39 @@ jobs:
8282
--all \
8383
--verbosity=QUIET
8484
85+
ty:
86+
name: "ty: Check stubs"
87+
runs-on: ubuntu-latest
88+
strategy:
89+
matrix:
90+
platform: ["linux", "win32", "darwin"]
91+
# TODO: Add 3.15 once third-party runtime dependencies provide compatible wheels.
92+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
93+
fail-fast: false
94+
steps:
95+
- uses: actions/checkout@v7
96+
- uses: actions/setup-python@v6
97+
with:
98+
python-version: "3.14"
99+
- uses: astral-sh/setup-uv@v8.2.0
100+
with:
101+
version-file: "requirements-tests.txt"
102+
- name: Install typeshed test-suite requirements
103+
run: uv pip install -r requirements-tests.txt --system
104+
- name: Create an isolated venv for testing
105+
run: uv venv .venv
106+
- name: Install third-party stub dependencies
107+
run: |
108+
PACKAGES=$(python tests/get_external_stub_requirements.py)
109+
if [ -n "$PACKAGES" ]; then
110+
uv pip install --python-version ${{ matrix.python-version }} $PACKAGES
111+
fi
112+
# Published stub packages can shadow the checked-in stubs when ty
113+
# resolves their relative imports.
114+
uv pip uninstall types-PyYAML types-pytz
115+
- name: Run ty on all stubs
116+
run: python tests/ty_test.py --platform=${{ matrix.platform }} --python-version=${{ matrix.python-version }} --python=.venv
117+
85118
pyright:
86119
name: "pyright: Run test cases"
87120
runs-on: ubuntu-latest

requirements-tests.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# be pinned to a specific version to make failure reproducible.
33
mypy==2.3.0
44
pyright==1.1.411
5+
ty==0.0.59
56

67
# Libraries used by our various scripts.
78
aiohttp==3.14.1

stdlib/_asyncio.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ else:
5353
# since the only reason why `asyncio.Future` is invariant is the `set_result()` method,
5454
# and `asyncio.Task.set_result()` always raises.
5555
@disjoint_base
56-
class Task(Future[_T_co]): # type: ignore[type-var] # pyright: ignore[reportInvalidTypeArguments]
56+
class Task(Future[_T_co]): # type: ignore[type-var] # pyright: ignore[reportInvalidTypeArguments] # ty:ignore[invalid-generic-class]
5757
if sys.version_info >= (3, 12):
5858
def __init__(
5959
self,

stdlib/collections/__init__.pyi

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -364,17 +364,17 @@ class _OrderedDictValuesView(ValuesView[_VT_co]):
364364
# pyright doesn't have a specific error code for subclassing error!
365365
@final
366366
@type_check_only
367-
class _odict_keys(dict_keys[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]
367+
class _odict_keys(dict_keys[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[subclass-of-final-class]
368368
def __reversed__(self) -> Iterator[_KT_co]: ...
369369

370370
@final
371371
@type_check_only
372-
class _odict_items(dict_items[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]
372+
class _odict_items(dict_items[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[subclass-of-final-class]
373373
def __reversed__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ...
374374

375375
@final
376376
@type_check_only
377-
class _odict_values(dict_values[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]
377+
class _odict_values(dict_values[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[subclass-of-final-class]
378378
def __reversed__(self) -> Iterator[_VT_co]: ...
379379

380380
@disjoint_base

stdlib/contextlib.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ _CM_EF = TypeVar("_CM_EF", bound=AbstractContextManager[Any, Any] | _ExitFunc)
4343
# At runtime it inherits from ABC and is not a Protocol, but it is on the
4444
# allowlist for use as a Protocol.
4545
@runtime_checkable
46-
class AbstractContextManager(ABC, Protocol[_T_co, _ExitT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]
46+
class AbstractContextManager(ABC, Protocol[_T_co, _ExitT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-protocol]
4747
__slots__ = ()
4848
def __enter__(self) -> _T_co: ...
4949
@abstractmethod
@@ -55,7 +55,7 @@ class AbstractContextManager(ABC, Protocol[_T_co, _ExitT_co]): # type: ignore[m
5555
# At runtime it inherits from ABC and is not a Protocol, but it is on the
5656
# allowlist for use as a Protocol.
5757
@runtime_checkable
58-
class AbstractAsyncContextManager(ABC, Protocol[_T_co, _ExitT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]
58+
class AbstractAsyncContextManager(ABC, Protocol[_T_co, _ExitT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-protocol]
5959
__slots__ = ()
6060
async def __aenter__(self) -> _T_co: ...
6161
@abstractmethod

stdlib/dataclasses.pyi

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ def fields(class_or_instance: DataclassInstance | type[DataclassInstance]) -> tu
298298

299299
# HACK: `obj: Never` typing matches if object argument is using `Any` type.
300300
@overload
301-
def is_dataclass(obj: Never) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ... # type: ignore[narrowed-type-not-subtype] # pyright: ignore[reportGeneralTypeIssues]
301+
def is_dataclass(obj: Never) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ... # type: ignore[narrowed-type-not-subtype] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-type-guard-definition]
302302
@overload
303303
def is_dataclass(obj: type) -> TypeIs[type[DataclassInstance]]: ...
304304
@overload
@@ -308,13 +308,17 @@ class FrozenInstanceError(AttributeError): ...
308308

309309
class InitVar(Generic[_T]):
310310
__slots__ = ("type",)
311-
type: Type[_T]
311+
type: Type[_T] # ty:ignore[unbound-type-variable]
312312
def __init__(self, type: Type[_T]) -> None: ...
313313

314314
@overload
315-
def __class_getitem__(cls, type: Type[_T]) -> InitVar[_T]: ... # pyright: ignore[reportInvalidTypeForm]
315+
def __class_getitem__(
316+
cls, type: Type[_T]
317+
) -> InitVar[_T]: ... # pyright: ignore[reportInvalidTypeForm] # ty:ignore[invalid-type-form]
316318
@overload
317-
def __class_getitem__(cls, type: Any) -> InitVar[Any]: ... # pyright: ignore[reportInvalidTypeForm]
319+
def __class_getitem__(
320+
cls, type: Any
321+
) -> InitVar[Any]: ... # pyright: ignore[reportInvalidTypeForm] # ty:ignore[invalid-type-form]
318322

319323
if sys.version_info >= (3, 14):
320324
def make_dataclass(

stdlib/os/__init__.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,7 @@ In the future, this property will contain the last metadata change time.""")
940940
# At runtime it inherits from ABC and is not a Protocol, but it will be
941941
# on the allowlist for use as a Protocol starting in 3.14.
942942
@runtime_checkable
943-
class PathLike(ABC, Protocol[AnyStr_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]
943+
class PathLike(ABC, Protocol[AnyStr_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-protocol]
944944
__slots__ = ()
945945
@abstractmethod
946946
def __fspath__(self) -> AnyStr_co: ...

stdlib/platform.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,15 @@ if sys.version_info >= (3, 12):
4444

4545
def __new__(_cls, system: str, node: str, release: str, version: str, machine: str) -> Self: ...
4646
@property
47-
def processor(self) -> str: ...
47+
def processor(self) -> str: ... # ty:ignore[invalid-named-tuple-override]
4848

4949
else:
5050
@disjoint_base
5151
class uname_result(_uname_result_base):
5252
__match_args__ = ("system", "node", "release", "version", "machine") # pyright: ignore[reportAssignmentType]
5353
def __new__(_cls, system: str, node: str, release: str, version: str, machine: str) -> Self: ...
5454
@property
55-
def processor(self) -> str: ...
55+
def processor(self) -> str: ... # ty:ignore[invalid-named-tuple-override]
5656

5757
def uname() -> uname_result: ...
5858
def system() -> str: ...

stdlib/ssl.pyi

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,18 @@ class Purpose(_ASN1Object, enum.Enum):
262262
# because this is an enum, the inherited __new__ is replaced at runtime with
263263
# Enum.__new__.
264264
def __new__(cls, value: object) -> Self: ...
265-
SERVER_AUTH = (129, "serverAuth", "TLS Web Server Authentication", "1.3.6.1.5.5.7.3.2") # pyright: ignore[reportCallIssue]
266-
CLIENT_AUTH = (130, "clientAuth", "TLS Web Client Authentication", "1.3.6.1.5.5.7.3.1") # pyright: ignore[reportCallIssue]
265+
SERVER_AUTH = ( # ty:ignore[invalid-assignment]
266+
129,
267+
"serverAuth",
268+
"TLS Web Server Authentication",
269+
"1.3.6.1.5.5.7.3.2",
270+
) # pyright: ignore[reportCallIssue]
271+
CLIENT_AUTH = ( # ty:ignore[invalid-assignment]
272+
130,
273+
"clientAuth",
274+
"TLS Web Client Authentication",
275+
"1.3.6.1.5.5.7.3.1",
276+
) # pyright: ignore[reportCallIssue]
267277

268278
class SSLSocket(socket.socket):
269279
context: SSLContext

stdlib/types.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ class CodeType:
268268
__replace__ = replace
269269

270270
@final
271-
class MappingProxyType(Mapping[_KT_co, _VT_co]): # type: ignore[type-var] # pyright: ignore[reportInvalidTypeArguments]
271+
class MappingProxyType(Mapping[_KT_co, _VT_co]): # type: ignore[type-var] # pyright: ignore[reportInvalidTypeArguments] # ty:ignore[invalid-generic-class]
272272
__hash__: ClassVar[None] # type: ignore[assignment]
273273
def __new__(cls, mapping: SupportsKeysAndGetItem[_KT_co, _VT_co]) -> Self: ...
274274
def __getitem__(self, key: _KT_co, /) -> _VT_co: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]

0 commit comments

Comments
 (0)