Repository: python/mypy

Issue:
Interaction between NamedTuple and @property
```
Base = NamedTuple('Base', [('i', int), ('f', float)])

class Derived(Base):
    @property
    def inverse(self):
        # type: () -> Derived
        return Derived(-self.i, -self.f)

x = Derived(1, 1.5)
i = x.inverse.i  # error: Invalid method type
```

This error doesn't seem to occur if `inverse` isn't a property or if Base is a normal class.



Discussion:
This diff fixes this issue:

``` diff
--- a/mypy/checkmember.py
+++ b/mypy/checkmember.py
@@ -237,6 +237,10 @@ def check_method_type(functype: FunctionLike, itype: Instance, is_classmethod: b
         elif not is_classmethod:
             # Check that self argument has type 'Any' or valid instance type.
             selfarg = item.arg_types[0]
+            # If this is a method of a tuple class, correct for the fact that
+            # we passed to typ.fallback in analyze_member_access.
+            if isinstance(selfarg, TupleType):
+                selfarg = selfarg.fallback
             if not subtypes.is_equivalent(selfarg, itype):
                 msg.invalid_method_type(item, context)
         else:
```

But it's awkward that we have both `Tuple[int, float, fallback=Derived]` and `Derived` as distinct types. Maybe `is_equivalent` should consider them equivalent? I'm not sure I totally understand the design around NamedTuple types.

`Tuple[int, float, fallback=Derived]` is the actual named tuple type, which approximates an intersection of an `Instance` (`Derived`) and the special features of the corresponding tuple type. This is not entirely true as the tuple type features take precedence over `Instance` features when there is ambiguity.

`Tuple[...]` itself, if we ignore the fallback, only provides a few tuple-specific magic features such as `t[n]` where `n` is an integer literal. When these features aren't important we can safely fall back to the fallback instance type.

Your diff seems right, as there is conceptually a one-to-one mapping between `Tuple[int, float, fallback=Derived]` and `Derived`. They aren't equivalent but one can't normally exist without the other, except in intermediate steps within the type checker, as seems to be the case here.

Okay, that's more or less what I thought. I guess I should imagine subtyping relationships like this?

```
    Base --> Tuple[int, float, fallback=Base]
      |                       |
      v                       v
 Derived --> Tuple[int, float, fallback=Derived]
```

It sounds like at least `Tuple[int, float, fallback=Derived]` should be treated as a subtype of `Derived`, so I made an issue #1558 for that; let me know if you disagree.

I'll turn that diff into a proper PR.

Off-topic: Is it customary to draw arrows from base class to subclass? I'm
used to the UML inheritance diagrams where AFAIK the arrows point to the
base class (and the base class is typically somewhere above its
subclasses). I realize the latter is hard to do in ASCII art. :-)

I just picked a direction arbitrarily. Thinking about it, I think the other direction (pointing to the base class) makes more sense.


Resolving patch:
diff --git a/mypy/checkmember.py b/mypy/checkmember.py
--- a/mypy/checkmember.py
+++ b/mypy/checkmember.py
@@ -237,6 +237,10 @@ def check_method_type(functype: FunctionLike, itype: Instance, is_classmethod: b
         elif not is_classmethod:
             # Check that self argument has type 'Any' or valid instance type.
             selfarg = item.arg_types[0]
+            # If this is a method of a tuple class, correct for the fact that
+            # we passed to typ.fallback in analyze_member_access. See #1432.
+            if isinstance(selfarg, TupleType):
+                selfarg = selfarg.fallback
             if not subtypes.is_equivalent(selfarg, itype):
                 msg.invalid_method_type(item, context)
         else:


Test patch:
diff --git a/mypy/test/data/check-namedtuple.test b/mypy/test/data/check-namedtuple.test
--- a/mypy/test/data/check-namedtuple.test
+++ b/mypy/test/data/check-namedtuple.test
@@ -173,3 +173,15 @@ MyNamedTuple.x # E: "MyNamedTuple" has no attribute "x"
 from typing import NamedTuple
 A = NamedTuple('A', [])
 [builtins fixtures/list.py]
+
+[case testNamedTupleProperty]
+from typing import NamedTuple
+A = NamedTuple('A', [('a', int)])
+class B(A):
+    @property
+    def b(self) -> int:
+        return self.a
+class C(B): pass
+B(1).b
+C(2).b
+[builtins fixtures/property.py]
diff --git a/mypy/test/data/fixtures/property.py b/mypy/test/data/fixtures/property.py
--- a/mypy/test/data/fixtures/property.py
+++ b/mypy/test/data/fixtures/property.py
@@ -13,3 +13,4 @@ class function: pass
 class int: pass
 class str: pass
 class bytes: pass
+class tuple: pass