Repository: Instagram/MonkeyType

Issue:
`NewType` generated into invalid type signatures
Given

```python
UserId = NewType('UserId', int)

def get_user(self, user_id: UserId) -> 'User':
```

MonkeyType generates

```python
def get_user(
    self,
    user_id: <function NewType.<locals>.new_type at 0x1090a52f0>
) -> 'User': ...
```

which then causes retype to fail with this error message:

```
error: /path/to/module.py: invalid syntax (<unknown>, line 18)
```

This happens even when `--exclude-unparsable-defaults` is passed.

I was able to come up with a solution to this, which I'll submit a PR for. However, it seems hacky to me and I'd appreciate feedback on it.

Discussion:
carljm: This approach looks fine to me; thanks for the report and fix!

Normally we try to automatically provide the right imports for annotations too, so `monkeytype apply` doesn't result in modules with missing imports. However, I don't think that's possible with the current runtime implementation of `NewType`; the calling module is not stored anywhere in the resulting function object, so there's no way we can introspect it. So unless we change that in `typing.py` we're stuck with the limitation of not being able to automatically add NewType imports.

It would be great to add tests for NewType in `test_stubs.py`; thanks!
carljm: Er, never mind about generating imports; `NewType` annotations can only occur as existing annotations, never as traced types. So the import should always be present already.
folz: Added a test for this and updated the changelog. I think you're right that this doesn't need to worry about imports - if the import isn't present, it wasn't valid to mypy in the first place.
carljm: Awesome, thank you!

Patch:
diff --git a/.gitignore b/.gitignore
index a31079d..2f4a0c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ htmlcov/
 doc/_build
 dist/
 build/
+.idea/
\ No newline at end of file
diff --git a/CHANGES.rst b/CHANGES.rst
index 3350f81..c3d23d3 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,7 +5,7 @@ master
 ------
 
 * Fix passing args to script run with ``monkeytype run`` (#18; merge of #21).
-
+* Fix generated annotations for NewType types (#22; merge of #23).
 
 17.12.1
 -------
diff --git a/monkeytype/stubs.py b/monkeytype/stubs.py
index 574fc35..8abc90b 100644
--- a/monkeytype/stubs.py
+++ b/monkeytype/stubs.py
@@ -319,6 +319,8 @@ def render_annotation(anno: Any) -> str:
         elem_type = _get_optional_elem(anno)
         rendered = render_annotation(elem_type)
         return 'Optional[' + rendered + ']'
+    elif hasattr(anno, '__supertype__'):
+        return anno.__name__
     elif getattr(anno, '__module__', None) == 'typing':
         return repr(anno).replace('typing.', '')
     elif anno is NoneType:


Test patch:
diff --git a/tests/test_stubs.py b/tests/test_stubs.py
index bbe5913..acfa771 100644
--- a/tests/test_stubs.py
+++ b/tests/test_stubs.py
@@ -11,6 +11,7 @@ from typing import (
     Generator,
     Iterator,
     List,
+    NewType,
     Optional,
     Set,
     Tuple,
@@ -42,6 +43,8 @@ from monkeytype.tracing import CallTrace
 from monkeytype.typing import NoneType
 from .util import Dummy
 
+UserId = NewType('UserId', int)
+
 
 class TestImportMap:
     def test_merge(self):
@@ -133,6 +136,10 @@ def has_length_exceeds_120_chars(
     return None
 
 
+def has_newtype_param(user_id: UserId) -> None:
+    pass
+
+
 class TestHasUnparsableDefaults:
     @pytest.mark.parametrize(
         'func, expected',
@@ -241,6 +248,11 @@ class TestFunctionStub:
         expected = 'def test(x: Optional[int] = None) -> None: ...'
         assert stub.render() == expected
 
+    def test_newtype_parameter_annotation(self):
+        stub = FunctionStub('test', inspect.signature(has_newtype_param), FunctionKind.MODULE)
+        expected = 'def test(user_id: UserId) -> None: ...'
+        assert stub.render() == expected
+
 
 def _func_stub_from_callable(func: Callable, strip_modules: List[str] = None):
     kind = FunctionKind.from_callable(func)