diff --git a/stdlib/@tests/test_cases/builtins/check_dict-py39.py b/stdlib/@tests/test_cases/builtins/check_dict-py39.py deleted file mode 100644 index 20ac4e622071..000000000000 --- a/stdlib/@tests/test_cases/builtins/check_dict-py39.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Tests for `dict.__(r)or__`. - -`dict.__or__` and `dict.__ror__` were only added in py39, -hence why these are in a separate file to the other test cases for `dict`. -""" - -from __future__ import annotations - -import os -import sys -from typing import Mapping, TypeVar, Union -from typing_extensions import Self, assert_type - -_KT = TypeVar("_KT") -_VT = TypeVar("_VT") - -if sys.version_info >= (3, 9): - - class CustomDictSubclass(dict[_KT, _VT]): - pass - - class CustomMappingWithDunderOr(Mapping[_KT, _VT]): - def __or__(self, other: Mapping[_KT, _VT]) -> dict[_KT, _VT]: - return {} - - def __ror__(self, other: Mapping[_KT, _VT]) -> dict[_KT, _VT]: - return {} - - def __ior__(self, other: Mapping[_KT, _VT]) -> Self: - return self - - def test_dict_dot_or( - a: dict[int, int], - b: CustomDictSubclass[int, int], - c: dict[str, str], - d: Mapping[int, int], - e: CustomMappingWithDunderOr[str, str], - ) -> None: - # dict.__(r)or__ always returns a dict, even if called on a subclass of dict: - assert_type(a | b, dict[int, int]) - assert_type(b | a, dict[int, int]) - - assert_type(a | c, dict[Union[int, str], Union[int, str]]) - - # arbitrary mappings are not accepted by `dict.__or__`; - # it has to be a subclass of `dict` - a | d # type: ignore - - # but Mappings such as `os._Environ` or `CustomMappingWithDunderOr`, - # which define `__ror__` methods that accept `dict`, are fine: - assert_type(a | os.environ, dict[Union[str, int], Union[str, int]]) - assert_type(os.environ | a, dict[Union[str, int], Union[str, int]]) - - assert_type(c | os.environ, dict[str, str]) - assert_type(c | e, dict[str, str]) - - assert_type(os.environ | c, dict[str, str]) - assert_type(e | c, dict[str, str]) - - # store "untainted" `CustomMappingWithDunderOr[str, str]` to test `__ior__` against ` dict[str, str]` later - # Invalid `e |= a` causes pyright to join `Unknown` to `e`'s type - f = e - - e |= c - e |= a # type: ignore - - c |= f - - c |= a # type: ignore diff --git a/stdlib/@tests/test_cases/builtins/check_dict.py b/stdlib/@tests/test_cases/builtins/check_dict.py index d89c3a27d489..2944730c7c67 100644 --- a/stdlib/@tests/test_cases/builtins/check_dict.py +++ b/stdlib/@tests/test_cases/builtins/check_dict.py @@ -1,7 +1,8 @@ from __future__ import annotations -from typing import Any, Dict, Generic, Iterable, TypeVar, Union -from typing_extensions import assert_type +import os +from typing import Any, Dict, Generic, Iterable, Mapping, TypeVar, Union +from typing_extensions import Self, assert_type # These do follow `__init__` overloads order: # mypy and pyright have different opinions about this one: @@ -148,3 +149,61 @@ def test11() -> str: def test12() -> str: return d_str.get("key", int_value) # type: ignore[arg-type] + + +# Tests for `dict.__(r)or__`. + + +class CustomDictSubclass(dict[_KT, _VT]): + pass + + +class CustomMappingWithDunderOr(Mapping[_KT, _VT]): + def __or__(self, other: Mapping[_KT, _VT]) -> dict[_KT, _VT]: + return {} + + def __ror__(self, other: Mapping[_KT, _VT]) -> dict[_KT, _VT]: + return {} + + def __ior__(self, other: Mapping[_KT, _VT]) -> Self: + return self + + +def test_dict_dot_or( + a: dict[int, int], + b: CustomDictSubclass[int, int], + c: dict[str, str], + d: Mapping[int, int], + e: CustomMappingWithDunderOr[str, str], +) -> None: + # dict.__(r)or__ always returns a dict, even if called on a subclass of dict: + assert_type(a | b, dict[int, int]) + assert_type(b | a, dict[int, int]) + + assert_type(a | c, dict[Union[int, str], Union[int, str]]) + + # arbitrary mappings are not accepted by `dict.__or__`; + # it has to be a subclass of `dict` + a | d # type: ignore + + # but Mappings such as `os._Environ` or `CustomMappingWithDunderOr`, + # which define `__ror__` methods that accept `dict`, are fine: + assert_type(a | os.environ, dict[Union[str, int], Union[str, int]]) + assert_type(os.environ | a, dict[Union[str, int], Union[str, int]]) + + assert_type(c | os.environ, dict[str, str]) + assert_type(c | e, dict[str, str]) + + assert_type(os.environ | c, dict[str, str]) + assert_type(e | c, dict[str, str]) + + # store "untainted" `CustomMappingWithDunderOr[str, str]` to test `__ior__` against ` dict[str, str]` later + # Invalid `e |= a` causes pyright to join `Unknown` to `e`'s type + f = e + + e |= c + e |= a # type: ignore + + c |= f + + c |= a # type: ignore diff --git a/stdlib/builtins.pyi b/stdlib/builtins.pyi index 4e2484509c1d..c9f6efafd6e9 100644 --- a/stdlib/builtins.pyi +++ b/stdlib/builtins.pyi @@ -32,7 +32,7 @@ from _typeshed import ( ) from collections.abc import Awaitable, Callable, Iterable, Iterator, MutableSet, Reversible, Set as AbstractSet, Sized from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper -from types import CellType, CodeType, TracebackType +from types import CellType, CodeType, GenericAlias, TracebackType # mypy crashes if any of {ByteString, Sequence, MutableSequence, Mapping, MutableMapping} # are imported from collections.abc in builtins.pyi @@ -72,9 +72,6 @@ from typing_extensions import ( # noqa: Y023 deprecated, ) -if sys.version_info >= (3, 9): - from types import GenericAlias - _T = TypeVar("_T") _I = TypeVar("_I", default=int) _T_co = TypeVar("_T_co", covariant=True) @@ -377,10 +374,8 @@ class float: def __rpow__(self, value: float, mod: None = None, /) -> Any: ... def __getnewargs__(self) -> tuple[float]: ... def __trunc__(self) -> int: ... - if sys.version_info >= (3, 9): - def __ceil__(self) -> int: ... - def __floor__(self) -> int: ... - + def __ceil__(self) -> int: ... + def __floor__(self) -> int: ... @overload def __round__(self, ndigits: None = None, /) -> int: ... @overload @@ -519,16 +514,15 @@ class str(Sequence[str]): ) -> LiteralString: ... @overload def replace(self, old: str, new: str, count: SupportsIndex = -1, /) -> str: ... # type: ignore[misc] - if sys.version_info >= (3, 9): - @overload - def removeprefix(self: LiteralString, prefix: LiteralString, /) -> LiteralString: ... - @overload - def removeprefix(self, prefix: str, /) -> str: ... # type: ignore[misc] - @overload - def removesuffix(self: LiteralString, suffix: LiteralString, /) -> LiteralString: ... - @overload - def removesuffix(self, suffix: str, /) -> str: ... # type: ignore[misc] + @overload + def removeprefix(self: LiteralString, prefix: LiteralString, /) -> LiteralString: ... + @overload + def removeprefix(self, prefix: str, /) -> str: ... # type: ignore[misc] + @overload + def removesuffix(self: LiteralString, suffix: LiteralString, /) -> LiteralString: ... + @overload + def removesuffix(self, suffix: str, /) -> str: ... # type: ignore[misc] def rfind(self, sub: str, start: SupportsIndex | None = ..., end: SupportsIndex | None = ..., /) -> int: ... def rindex(self, sub: str, start: SupportsIndex | None = ..., end: SupportsIndex | None = ..., /) -> int: ... @overload @@ -666,10 +660,8 @@ class bytes(Sequence[int]): def lstrip(self, bytes: ReadableBuffer | None = None, /) -> bytes: ... def partition(self, sep: ReadableBuffer, /) -> tuple[bytes, bytes, bytes]: ... def replace(self, old: ReadableBuffer, new: ReadableBuffer, count: SupportsIndex = -1, /) -> bytes: ... - if sys.version_info >= (3, 9): - def removeprefix(self, prefix: ReadableBuffer, /) -> bytes: ... - def removesuffix(self, suffix: ReadableBuffer, /) -> bytes: ... - + def removeprefix(self, prefix: ReadableBuffer, /) -> bytes: ... + def removesuffix(self, suffix: ReadableBuffer, /) -> bytes: ... def rfind( self, sub: ReadableBuffer | SupportsIndex, start: SupportsIndex | None = ..., end: SupportsIndex | None = ..., / ) -> int: ... @@ -771,10 +763,8 @@ class bytearray(MutableSequence[int]): def partition(self, sep: ReadableBuffer, /) -> tuple[bytearray, bytearray, bytearray]: ... def pop(self, index: int = -1, /) -> int: ... def remove(self, value: int, /) -> None: ... - if sys.version_info >= (3, 9): - def removeprefix(self, prefix: ReadableBuffer, /) -> bytearray: ... - def removesuffix(self, suffix: ReadableBuffer, /) -> bytearray: ... - + def removeprefix(self, prefix: ReadableBuffer, /) -> bytearray: ... + def removesuffix(self, suffix: ReadableBuffer, /) -> bytearray: ... def replace(self, old: ReadableBuffer, new: ReadableBuffer, count: SupportsIndex = -1, /) -> bytearray: ... def rfind( self, sub: ReadableBuffer | SupportsIndex, start: SupportsIndex | None = ..., end: SupportsIndex | None = ..., / @@ -1009,8 +999,7 @@ class tuple(Sequence[_T_co]): def __rmul__(self, value: SupportsIndex, /) -> tuple[_T_co, ...]: ... def count(self, value: Any, /) -> int: ... def index(self, value: Any, start: SupportsIndex = 0, stop: SupportsIndex = sys.maxsize, /) -> int: ... - if sys.version_info >= (3, 9): - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... # Doesn't exist at runtime, but deleting this breaks mypy and pyright. See: # https://github.com/python/typeshed/issues/7580 @@ -1092,8 +1081,7 @@ class list(MutableSequence[_T]): def __lt__(self, value: list[_T], /) -> bool: ... def __le__(self, value: list[_T], /) -> bool: ... def __eq__(self, value: object, /) -> bool: ... - if sys.version_info >= (3, 9): - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... class dict(MutableMapping[_KT, _VT]): # __init__ should be kept roughly in line with `collections.UserDict.__init__`, which has similar semantics @@ -1162,21 +1150,20 @@ class dict(MutableMapping[_KT, _VT]): def __eq__(self, value: object, /) -> bool: ... def __reversed__(self) -> Iterator[_KT]: ... __hash__: ClassVar[None] # type: ignore[assignment] - if sys.version_info >= (3, 9): - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... - @overload - def __or__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... - @overload - def __or__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... - @overload - def __ror__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... - @overload - def __ror__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... - # dict.__ior__ should be kept roughly in line with MutableMapping.update() - @overload # type: ignore[misc] - def __ior__(self, value: SupportsKeysAndGetItem[_KT, _VT], /) -> Self: ... - @overload - def __ior__(self, value: Iterable[tuple[_KT, _VT]], /) -> Self: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + @overload + def __or__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... + @overload + def __or__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + @overload + def __ror__(self, value: dict[_KT, _VT], /) -> dict[_KT, _VT]: ... + @overload + def __ror__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: ... + # dict.__ior__ should be kept roughly in line with MutableMapping.update() + @overload # type: ignore[misc] + def __ior__(self, value: SupportsKeysAndGetItem[_KT, _VT], /) -> Self: ... + @overload + def __ior__(self, value: Iterable[tuple[_KT, _VT]], /) -> Self: ... class set(MutableSet[_T]): @overload @@ -1215,8 +1202,7 @@ class set(MutableSet[_T]): def __gt__(self, value: AbstractSet[object], /) -> bool: ... def __eq__(self, value: object, /) -> bool: ... __hash__: ClassVar[None] # type: ignore[assignment] - if sys.version_info >= (3, 9): - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... class frozenset(AbstractSet[_T_co]): @overload @@ -1244,15 +1230,13 @@ class frozenset(AbstractSet[_T_co]): def __gt__(self, value: AbstractSet[object], /) -> bool: ... def __eq__(self, value: object, /) -> bool: ... def __hash__(self) -> int: ... - if sys.version_info >= (3, 9): - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... class enumerate(Generic[_T]): def __new__(cls, iterable: Iterable[_T], start: int = 0) -> Self: ... def __iter__(self) -> Self: ... def __next__(self) -> tuple[int, _T]: ... - if sys.version_info >= (3, 9): - def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... + def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... @final class range(Sequence[int]):