Repository: python/mypy

Issue:
Descriptors are not resolved within type unions
It appears that mypy does not properly resolve descriptors when used within type unions.  If I have an object (e.g. `Test` below) that makes use of descriptors for properties (e.g. `StringDescriptor` and `ListDescriptor` below) in `Union`s mypy returns errors.  If the descriptors are used directly without any `Union` type mypy resolves the type.

I believe this is a mypy bug.  I would expect that descriptors are resolved the same both within and without type unions.

There are likely better, and less contrived, examples but if I have the following code:

```
from typing import Optional, List, Any, Union


class StringDescriptor(object):
    """Type descriptor for `Optional[str]`"""
    def __init__(self):
        # type: () -> None
        pass

    def __get__(self, inst, owner):
        # type: (object, type) -> Optional[str]
        pass

    def __set__(self, inst, value):
        # type: (object, Optional[str]) -> None
        pass


class ListDescriptor(object):
    """Type descriptor for `Optional[List[Any]]`"""
    def __init__(self):
        # type: () -> None
        pass

    def __get__(self, obj, owner):
        # type: (object, type) -> Optional[List[Any]]
        pass

    def __set__(self, obj, value):
        # type: (object, Optional[List[Any]]) -> None
        pass


class Test(object):
    def __init__(self):
        # type: () -> None
        self.string_union_param = 'this is a string'  # type: Union[str, StringDescriptor]
        self.string_param = 'this is a string'  # type: str
        self.list_union_param = []  # type: Union[List[Any], ListDescriptor]
        self.list_param = []  # type: List[Any]

    # error: Incompatible return value type (got "Union[str, StringDescriptor]", expected "Optional[str]")
    def get_string_union(self):
        # type: () -> Optional[str]
        return self.string_union_param

    # no error
    def get_string(self):
        # type: () -> Optional[str]
        return self.string_param

    # error: Iterable expected
    # error: Item "ListDescriptor" of "Union[List[Any], ListDescriptor]" has no attribute "__iter__"
    def get_list_union(self):
        # type: () -> Optional[List[Any]]
        if self.list_union_param:
            return [str(element) for element in self.list_union_param]
        return []

    # no error
    def get_list(self):
        # type: () -> Optional[List[Any]]
        if self.list_param:
            return [str(element) for element in self.list_param]
        return []

```

When I run mypy I see the following errors:

```
descriptor_test.py:45: error: Incompatible return value type (got "Union[str, StringDescriptor]", expected "Optional[str]")
descriptor_test.py:57: error: Iterable expected
descriptor_test.py:57: error: Item "ListDescriptor" of "Union[List[Any], ListDescriptor]" has no attribute "__iter__"
```

The descriptor is resolved to the expected type by itself but not when it is part of a Union.


Discussion:
Thanks for reporting! What you show here is not a good repro, it is hard to imagine code that does this:
```python
self.string_union_param = 'this is a string'  # type: Union[str, StringDescriptor]
```
TBH I would say we should just prohibit such code :-) A more realistic, and a simpler repro is below:
```python
class String:
    def __get__(self, inst, owner) -> str:
        ...

class A:
    attr: str

class B:
    attr = String()

x: Union[A, B]

reveal_type(x.attr) # Revealed type is 'Union[builtins.str, tstd.String]'
```
While the latter should be just `builtins.str`. The reason is trivial, unions are just ignored in `analyze_descriptor_access`:
```python
        if not isinstance(descriptor_type, Instance):
            return descriptor_type
```
Instead this function just should map the rest of the logic over the elements of the union. Thus this is easy to fix.

Resolving patch:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -1278,7 +1278,13 @@ def analyze_descriptor_access(self, instance_type: Type, descriptor_type: Type,
         Return:
             The return type of the appropriate ``__get__`` overload for the descriptor.
         """
-        if not isinstance(descriptor_type, Instance):
+        if isinstance(descriptor_type, UnionType):
+            # Map the access over union types
+            return UnionType.make_simplified_union([
+                self.analyze_descriptor_access(instance_type, typ, context)
+                for typ in descriptor_type.items
+            ])
+        elif not isinstance(descriptor_type, Instance):
             return descriptor_type
 
         if not descriptor_type.type.has_readable_member('__get__'):


Test patch:
diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test
--- a/test-data/unit/check-classes.test
+++ b/test-data/unit/check-classes.test
@@ -1495,6 +1495,20 @@ a = A()
 a.f = 1
 reveal_type(a.f)  # E: Revealed type is 'builtins.str'
 
+[case testDescriptorGetUnion]
+from typing import Any, Union
+class String:
+    def __get__(self, inst: Any, owner: Any) -> str:
+        return ''
+
+class A:
+    attr: str
+
+class B:
+    attr = String()
+
+def foo(x: Union[A, B]) -> None:
+    reveal_type(x.attr)  # E: Revealed type is 'builtins.str'
 
 -- _promote decorators
 -- -------------------