Skip to content
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

Add |= and | operators support for TypedDict #16249

Merged
merged 15 commits into from
Oct 23, 2023
19 changes: 15 additions & 4 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -7450,13 +7450,24 @@ def infer_operator_assignment_method(typ: Type, operator: str) -> tuple[bool, st
For example, if operator is '+', return (True, '__iadd__') or (False, '__add__')
depending on which method is supported by the type.
"""

def find_method(inst: Instance, method: str) -> str | None:
sobolevn marked this conversation as resolved.
Show resolved Hide resolved
if operator in operators.ops_with_inplace_method:
inplace_method = "__i" + method[2:]
if inst.type.has_readable_member(inplace_method):
return inplace_method
return None

typ = get_proper_type(typ)
method = operators.op_methods[operator]
existing_method = None
if isinstance(typ, Instance):
if operator in operators.ops_with_inplace_method:
inplace_method = "__i" + method[2:]
if typ.type.has_readable_member(inplace_method):
return True, inplace_method
existing_method = find_method(typ, method)
elif isinstance(typ, TypedDictType):
existing_method = find_method(typ.fallback, method)

if existing_method is not None:
return True, existing_method
return False, method


Expand Down
53 changes: 53 additions & 0 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -3236,3 +3236,56 @@ def foo(x: int) -> Foo: ...
f: Foo = {**foo("no")} # E: Argument 1 to "foo" has incompatible type "str"; expected "int"
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]


[case testTypedDictWith__or__method]
from mypy_extensions import TypedDict

class Foo(TypedDict):
key: int

foo1: Foo = {'key': 1}
foo2: Foo = {'key': 2}

reveal_type(foo1 | foo2) # N: Revealed type is "TypedDict('__main__.Foo', {'key': builtins.int})"
reveal_type(foo1 | {'key': 1}) # N: Revealed type is "TypedDict('__main__.Foo', {'key': builtins.int})"
reveal_type(foo1 | {'key': 'a'}) # N: Revealed type is "typing.Mapping[builtins.str, builtins.object]"
reveal_type(foo1 | {}) # N: Revealed type is "typing.Mapping[builtins.str, builtins.object]"
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

[case testTypedDictWith__or__method_error]
from mypy_extensions import TypedDict

class Foo(TypedDict):
key: int

foo: Foo = {'key': 1}

foo | 1
[out]
main:8: error: No overload variant of "__or__" of "TypedDict" matches argument type "int"
main:8: note: Possible overload variants:
main:8: note: def __or__(self, Foo, /) -> Foo
main:8: note: def __or__(self, Mapping[str, object], /) -> Mapping[str, object]
Copy link
Member

Choose a reason for hiding this comment

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

These tests both pass with the master branch checked out

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure what's being fixed here

Yes, this is just the extra test that was missing. Please, see #16244 for the bug the user had.

[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]

# TODO: add `__ror__` method check, after `__ror__` definition is fixed.

[case testTypedDictWith__ior__method]
from mypy_extensions import TypedDict

class Foo(TypedDict):
key: int
sobolevn marked this conversation as resolved.
Show resolved Hide resolved

foo: Foo = {'key': 1}
foo |= {'key': 2}

foo |= {} # E: Missing key "key" for TypedDict "Foo"
foo |= {'key': 'a', 'b': 'a'} # E: Extra key "b" for TypedDict "Foo" \
# E: Incompatible types (expression has type "str", TypedDict item "key" has type "int")
foo |= {'b': 2} # E: Missing key "key" for TypedDict "Foo" \
# E: Extra key "b" for TypedDict "Foo"
[builtins fixtures/dict.pyi]
[typing fixtures/typing-typeddict.pyi]
sobolevn marked this conversation as resolved.
Show resolved Hide resolved
14 changes: 14 additions & 0 deletions test-data/unit/fixtures/typing-typeddict.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,17 @@ class _TypedDict(Mapping[str, object]):
def pop(self, k: NoReturn, default: T = ...) -> object: ...
def update(self: T, __m: T) -> None: ...
def __delitem__(self, k: NoReturn) -> None: ...
# It is a bit of a lie:
# 1. it is only supported since 3.9 in runtime
# 2. `__or__` users `dict[str, Any]`, not `Mapping[str, object]`
sobolevn marked this conversation as resolved.
Show resolved Hide resolved
@overload
def __or__(self, __value: Self) -> Self: ...
@overload
def __or__(self, __value: Mapping[str, object]) -> Mapping[str, object]: ...
# TODO: re-enable after `__ror__` definition is fixed
sobolevn marked this conversation as resolved.
Show resolved Hide resolved
# @overload
# def __ror__(self, __value: Self) -> Self: ...
# @overload
# def __ror__(self, __value: dict[str, Any]) -> dict[str, object]: ...
# supposedly incompatible definitions of __or__ and __ior__
def __ior__(self, __value: Self) -> Self: ... # type: ignore[misc]