From e1443bbade91118794055449cc8b4b4f7fd08b7d Mon Sep 17 00:00:00 2001 From: Matthieu Devlin Date: Mon, 15 Apr 2024 21:16:51 -0700 Subject: [PATCH] fix: incorrect returned type of access descriptors on unions of types (#16604) Fixes https://github.com/python/mypy/issues/16603 This change maps over union types when determining the types of access descriptors. Previously, the because [this conditional](https://github.com/md384/mypy/blob/c2a55afcef32ecb11a4c76c4c79539f6ba36d55c/mypy/checkmember.py#L697-L701) would fall through to the `else` case because instance type was not a singular `TypeType` (it was a Union), so we'd end up with an instance value being passed to `__get__` instead of `None`. --- mypy/checkmember.py | 13 +++++++++++ test-data/unit/check-unions.test | 38 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index afa8f37ff7d5..64d6733f5309 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -123,6 +123,7 @@ def copy_modified( messages: MessageBuilder | None = None, self_type: Type | None = None, is_lvalue: bool | None = None, + original_type: Type | None = None, ) -> MemberContext: mx = MemberContext( self.is_lvalue, @@ -142,6 +143,8 @@ def copy_modified( mx.self_type = self_type if is_lvalue is not None: mx.is_lvalue = is_lvalue + if original_type is not None: + mx.original_type = original_type return mx @@ -644,6 +647,16 @@ def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type: return make_simplified_union( [analyze_descriptor_access(typ, mx) for typ in descriptor_type.items] ) + elif isinstance(instance_type, UnionType): + # map over the instance types + return make_simplified_union( + [ + analyze_descriptor_access( + descriptor_type, mx.copy_modified(original_type=original_type) + ) + for original_type in instance_type.items + ] + ) elif not isinstance(descriptor_type, Instance): return orig_descriptor_type diff --git a/test-data/unit/check-unions.test b/test-data/unit/check-unions.test index d79ab14184c6..2e69a96f0c78 100644 --- a/test-data/unit/check-unions.test +++ b/test-data/unit/check-unions.test @@ -1220,3 +1220,41 @@ nc: Union[Container[str], int] 'x' in nc # E: Unsupported right operand type for in ("Union[Container[str], int]") [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] + +[case testDescriptorAccessForUnionOfTypes] +from typing import overload, Generic, Any, TypeVar, List, Optional, Union, Type + +_T_co = TypeVar("_T_co", bound=Any, covariant=True) + +class Mapped(Generic[_T_co]): + def __init__(self, value: _T_co): + self.value = value + + @overload + def __get__( + self, instance: None, owner: Any + ) -> List[_T_co]: + ... + + @overload + def __get__(self, instance: object, owner: Any) -> _T_co: + ... + + def __get__( + self, instance: Optional[object], owner: Any + ) -> Union[List[_T_co], _T_co]: + return self.value + +class A: + field_1: Mapped[int] = Mapped(1) + field_2: Mapped[str] = Mapped('1') + +class B: + field_1: Mapped[int] = Mapped(2) + field_2: Mapped[str] = Mapped('2') + +mix: Union[Type[A], Type[B]] = A +reveal_type(mix) # N: Revealed type is "Union[Type[__main__.A], Type[__main__.B]]" +reveal_type(mix.field_1) # N: Revealed type is "builtins.list[builtins.int]" +reveal_type(mix().field_1) # N: Revealed type is "builtins.int" +[builtins fixtures/list.pyi]