Repository: python/mypy

Issue:
TypedDict is recognized only when imported directly
* Are you reporting a bug, or opening a feature request?

This is a bug.

* Please insert below the code you are checking with mypy,
  or a mock-up repro if the source is private. We would appreciate
  if you try to simplify your case to a minimal repro.

The code to reproduce this bug should have one module nested in one or two directories and the main module in the root. Something like this will do:

* `/typed_dict_test.py`
* `/foo`
    * `/foo/__init__.py`
    * `/foo/bar`
        * `/foo/bar/__init__.py`
        * `/foo/bar/kitty.py`

The `kitty.py` module defines a single `TypedDict` object:

```
#!/usr/bin/env python3

from typing_extensions import TypedDict

BadKitty = TypedDict(
    "BadKitty",
    {
        "foo": str,
        "bar": str,
    },
)
```

This is used in the `typed_dict_test.py` file in the root:

```
#!/usr/bin/env python3

import foo.bar.kitty
from foo import bar
from foo.bar import kitty
from foo.bar.kitty import BadKitty

print(foo.bar.kitty.BadKitty(foo="abc", bar="def"))
print(bar.kitty.BadKitty(foo="abc", bar="def"))
print(kitty.BadKitty(foo="abc", bar="def"))
print(BadKitty(foo="abc", bar="def"))
```

* What is the actual behavior/output?

When this code is executed by Python, it works as expected:

```
C:\test>python typed_dict_test.py
{'foo': 'abc', 'bar': 'def'}
{'foo': 'abc', 'bar': 'def'}
{'foo': 'abc', 'bar': 'def'}
{'foo': 'abc', 'bar': 'def'}
```

However, `mypy` does not like all instances of `BadKitty`, which import this class indirectly. Only the last instance, which is imported as a top-level name, makes `mypy` happy:

```
C:\test>mypy typed_dict_test.py
typed_dict_test.py:8:7: error: Unexpected keyword argument "foo" for "BadKitty"  [call-arg]
    print(foo.bar.kitty.BadKitty(foo="abc", bar="def"))
          ^
typed_dict_test.py:8:7: error: Unexpected keyword argument "bar" for "BadKitty"  [call-arg]
    print(foo.bar.kitty.BadKitty(foo="abc", bar="def"))
          ^
typed_dict_test.py:9:7: error: Unexpected keyword argument "foo" for "BadKitty"  [call-arg]
    print(bar.kitty.BadKitty(foo="abc", bar="def"))
          ^
typed_dict_test.py:9:7: error: Unexpected keyword argument "bar" for "BadKitty"  [call-arg]
    print(bar.kitty.BadKitty(foo="abc", bar="def"))
          ^
typed_dict_test.py:10:7: error: Unexpected keyword argument "foo" for "BadKitty"  [call-arg]
    print(kitty.BadKitty(foo="abc", bar="def"))
          ^
typed_dict_test.py:10:7: error: Unexpected keyword argument "bar" for "BadKitty"  [call-arg]
    print(kitty.BadKitty(foo="abc", bar="def"))
```

What is interesting is that the first three instances of `BadKitty` are recognized **DIFFERENTLY** by `mypy`. Changing one of the keyword arguments in the last instance shows that `mypy` recognizes it as `TypedDict` correctly (notice the error code in `[...]`:

```
C:\test>mypy typed_dict_test.py
typed_dict_test.py:8:7: error: Unexpected keyword argument "foo" for "BadKitty"  [call-arg]
    print(foo.bar.kitty.BadKitty(foo="abc", bar="def"))
          ^
typed_dict_test.py:8:7: error: Unexpected keyword argument "bar" for "BadKitty"  [call-arg]
    print(foo.bar.kitty.BadKitty(foo="abc", bar="def"))
          ^
typed_dict_test.py:9:7: error: Unexpected keyword argument "foo" for "BadKitty"  [call-arg]
    print(bar.kitty.BadKitty(foo="abc", bar="def"))
          ^
typed_dict_test.py:9:7: error: Unexpected keyword argument "bar" for "BadKitty"  [call-arg]
    print(bar.kitty.BadKitty(foo="abc", bar="def"))
          ^
typed_dict_test.py:10:7: error: Unexpected keyword argument "foo" for "BadKitty"  [call-arg]
    print(kitty.BadKitty(foo="abc", bar="def"))
          ^
typed_dict_test.py:10:7: error: Unexpected keyword argument "bar" for "BadKitty"  [call-arg]
    print(kitty.BadKitty(foo="abc", bar="def"))
          ^
typed_dict_test.py:11:7: error: Extra key 'xxx_foo' for TypedDict "BadKitty"  [typeddict-item]
    print(BadKitty(xxx_foo="abc", bar="def"))
```

* What is the behavior/output you expect?

`mypy`should recognize `TypedDict` objects regardless of how they are imported.

* What are the versions of mypy and Python you are using?

```
C:\test>python --version --version
Python 3.7.2 (tags/v3.7.2:9a3ffc0492, Dec 23 2018, 23:09:28) [MSC v.1916 64 bit (AMD64)]
```

```
C:\test>mypy --version
mypy 0.750
```

* Do you see the same issue after installing mypy from Git master?

I did not try that since `0.750` is the latest version as of right now.

* What are the mypy flags you are using? (For example --strict-optional)

This is the content of `mypy.ini`:

```
[mypy]
mypy_path = ./stubs

# How imports are handled
ignore_missing_imports = False
follow_imports = normal

# Enforce various type annotations
disallow_any_unimported = True
disallow_any_expr = False
disallow_any_decorated = False
disallow_any_explicit = False
disallow_any_generics = True
disallow_subclassing_any = True
disallow_untyped_calls = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
disallow_untyped_decorators = True

# Various code quality checks
no_implicit_optional = True
strict_optional = True
warn_unused_ignores = True
warn_no_return = True
warn_return_any = True
warn_unreachable = True
warn_redundant_casts = True
allow_untyped_globals = False
allow_redefinition = False
strict_equality = True

# How error messages are printed
show_error_context = False
show_column_numbers = True
show_error_codes = True
pretty = True
```


Discussion:
This is a pretty silly bug: one just needs to replace `NameExpr` with `RefExpr` in the first line of `visit_call_expr_inner()`. Would you like to make a PR?
@ilevkivskyi 

> Would you like to make a PR?

Do I have a choice to pass on this offer? :) There are way too many commitments to finish...

Resolving patch:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -263,7 +263,7 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type:
         return self.visit_call_expr_inner(e, allow_none_return=allow_none_return)
 
     def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> Type:
-        if isinstance(e.callee, NameExpr) and isinstance(e.callee.node, TypeInfo) and \
+        if isinstance(e.callee, RefExpr) and isinstance(e.callee.node, TypeInfo) and \
                 e.callee.node.typeddict_type is not None:
             # Use named fallback for better error messages.
             typeddict_type = e.callee.node.typeddict_type.copy_modified(


Test patch:
diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test
--- a/test-data/unit/check-modules.test
+++ b/test-data/unit/check-modules.test
@@ -2781,3 +2781,28 @@ aaaab: int
 aaaba: int
 aabaa: int
 [builtins fixtures/module.pyi]
+
+[case testDirectlyImportTypedDictObjectAtTopLevel]
+import foo.bar.custom_dict
+from foo import bar
+from foo.bar import custom_dict
+from foo.bar.custom_dict import CustomDict
+
+foo.bar.custom_dict.CustomDict(foo="abc", bar="def")
+bar.custom_dict.CustomDict(foo="abc", bar="def")
+custom_dict.CustomDict(foo="abc", bar="def")
+CustomDict(foo="abc", bar="def")
+
+[file foo/__init__.py]
+[file foo/bar/__init__.py]
+[file foo/bar/custom_dict.py]
+from typing_extensions import TypedDict
+
+CustomDict = TypedDict(
+    "CustomDict",
+    {
+        "foo": str,
+        "bar": str,
+    },
+)
+[typing fixtures/typing-full.pyi]