Skip to content

Improve the accuracy of (default)dict.__(r)or__ #10679

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Sep 8, 2023
8 changes: 4 additions & 4 deletions stdlib/builtins.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1120,13 +1120,13 @@ class dict(MutableMapping[_KT, _VT], Generic[_KT, _VT]):
if sys.version_info >= (3, 9):
def __class_getitem__(cls, __item: Any) -> GenericAlias: ...
@overload
def __or__(self, __value: Mapping[_KT, _VT]) -> dict[_KT, _VT]: ...
def __or__(self, __value: dict[_KT, _VT]) -> dict[_KT, _VT]: ...
@overload
def __or__(self, __value: Mapping[_T1, _T2]) -> dict[_KT | _T1, _VT | _T2]: ...
def __or__(self, __value: dict[_T1, _T2]) -> dict[_KT | _T1, _VT | _T2]: ...
@overload
def __ror__(self, __value: Mapping[_KT, _VT]) -> dict[_KT, _VT]: ...
def __ror__(self, __value: dict[_KT, _VT]) -> dict[_KT, _VT]: ...
@overload
def __ror__(self, __value: Mapping[_T1, _T2]) -> dict[_KT | _T1, _VT | _T2]: ...
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: ...
Expand Down
8 changes: 4 additions & 4 deletions stdlib/collections/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -402,13 +402,13 @@ class defaultdict(dict[_KT, _VT], Generic[_KT, _VT]):
def copy(self) -> Self: ...
if sys.version_info >= (3, 9):
@overload
def __or__(self, __value: Mapping[_KT, _VT]) -> Self: ...
def __or__(self, __value: dict[_KT, _VT]) -> Self: ...
@overload
def __or__(self, __value: Mapping[_T1, _T2]) -> defaultdict[_KT | _T1, _VT | _T2]: ...
def __or__(self, __value: dict[_T1, _T2]) -> defaultdict[_KT | _T1, _VT | _T2]: ...
@overload
def __ror__(self, __value: Mapping[_KT, _VT]) -> Self: ...
def __ror__(self, __value: dict[_KT, _VT]) -> Self: ...
@overload
def __ror__(self, __value: Mapping[_T1, _T2]) -> defaultdict[_KT | _T1, _VT | _T2]: ...
def __ror__(self, __value: dict[_T1, _T2]) -> defaultdict[_KT | _T1, _VT | _T2]: ... # type: ignore[misc]

class ChainMap(MutableMapping[_KT, _VT], Generic[_KT, _VT]):
maps: list[MutableMapping[_KT, _VT]]
Expand Down
66 changes: 66 additions & 0 deletions test_cases/stdlib/builtins/check_dict-py39.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""
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])

e |= c
e |= a # type: ignore

# TODO: this test passes mypy, but fails pyright for some reason:
# c |= e

c |= a # type: ignore
69 changes: 69 additions & 0 deletions test_cases/stdlib/collections/check_defaultdict-py39.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
Tests for `defaultdict.__or__` and `defaultdict.__ror__`.
These methods were only added in py39.
"""

from __future__ import annotations

import os
import sys
from collections import defaultdict
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 CustomDefaultDictSubclass(defaultdict[_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_defaultdict_dot_or(
a: defaultdict[int, int],
b: CustomDefaultDictSubclass[int, int],
c: defaultdict[str, str],
d: Mapping[int, int],
e: CustomMappingWithDunderOr[str, str],
) -> None:
assert_type(a | b, defaultdict[int, int])

# In contrast to `dict.__or__`, `defaultdict.__or__` returns `Self` if called on a subclass of `defaultdict`:
assert_type(b | a, CustomDefaultDictSubclass[int, int])

assert_type(a | c, defaultdict[Union[int, str], Union[int, str]])

# arbitrary mappings are not accepted by `defaultdict.__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
# (`os._Environ.__(r)or__` always returns `dict`, even if a `defaultdict` is passed):
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])

e |= c
e |= a # type: ignore

# TODO: this test passes mypy, but fails pyright for some reason:
# c |= e

c |= a # type: ignore