Repository: python/mypy
Pull request: Add recommendation to use string literal escaping
This commit introduces a recommendation for classes that don't support indexing at runtime to be escaped as string literals on missing required generic type args (BARE_GENERIC) error.

It should resolve https://github.com/python/mypy/issues/7539

This is my first time contributing here, so please let me know if there is anything I could have done better. :)

Problem statement:
`OrderedDict` fails disallow_any_generics, but `OrderedDict[Any, Any]` doesn't run
I've been trying to introduce `disallow_any_generics = true` to an already extensively typed code base of ~48kSLOC, and the only remaining issue is typing `OrderedDict`s.

# Actual behaviour

Given this configuration:

```
[mypy]
check_untyped_defs = true
disallow_any_generics = true
disallow_untyped_defs = true
ignore_missing_imports = true
no_implicit_optional = true
warn_redundant_casts = true
warn_return_any = true
warn_unused_ignores = true
```

Code with implicit `Any`:

```python
from collections import OrderedDict
from typing import Any

def name(value: OrderedDict) -> None:
    pass
```

This fails validation because of `disallow_any_generics = true`:

```text
$ mypy example.py
example.py:4: error: Missing type parameters for generic type
```

Code with typed `OrderedDict`:

```python
from collections import OrderedDict
from typing import Any

def name(value: OrderedDict[Any, Any]) -> None:
    pass
```

This passes validation, but doesn't run:

```text
$ python example.py 
Traceback (most recent call last):
  File "example.py", line 4, in <module>
    def name(value: OrderedDict[Any, Any]) -> None:
TypeError: 'type' object is not subscriptable
```

Versions:

```
$ python --version
Python 3.6.8
$ mypy --version
mypy 0.720
```

(Also failed on mypy 0.711.)

# Expected behaviour

I'd expect either the first snippet to be valid or the second snippet to run. As it is I'm stuck for how to proceed.

Hints from issue discussion:
You can use string literal escapes, such as `value: 'OrderedDict[Any, Any]'`. Another option is `from __future__ import annotations` (introduced in Python 3.7).
@JukkaL Is there no way to do this without quoting the type? Because even if I `if TYPE_CHECKING: from collections import OrderedDict` that triggers [an issue in `flake8`](https://github.com/PyCQA/pyflakes/issues/469):

> F401 'collections.OrderedDict' imported but unused


You can use `from __future__ import annotations` in 3.7+.
do you want `typing.OrderedDict` and not `collections.OrderedDict` for that annotation?
I was going to say that too but it turns out there is no `typing.OrderedDict`.
there is in 3.7 iirc
Oh, you're right, it is in 3.7 (https://github.com/python/cpython/blob/3.7/Lib/typing.py) but not in the typing repo (https://github.com/python/typing/blob/master/src/typing.py) where I checked first. I filed python/typing#679 to add it.
If mypy sees that `OrderedDict` (or another class that doesn't support indexing at runtime) is missing required generic type args, we could give a better error message that suggests using string literal escaping.
Relevant docs https://mypy.readthedocs.io/en/latest/common_issues.html#using-classes-that-are-generic-in-stubs-but-not-at-runtime

Commit message: PEP8 compliant2

Patch to review:
diff --git a/mypy/message_registry.py b/mypy/message_registry.py
index 195807216a2b..74202c6d26da 100644
--- a/mypy/message_registry.py
+++ b/mypy/message_registry.py
@@ -89,6 +89,9 @@
 GENERIC_INSTANCE_VAR_CLASS_ACCESS = \
     'Access to generic instance variables via class is ambiguous'  # type: Final
 BARE_GENERIC = 'Missing type parameters for generic type {}'  # type: Final
+BARE_GENERIC_STRING_LITERAL_ESCAPE = \
+    'Missing type parameters for generic type {}, ' \
+    'also consider using string literal escaping'  # type: Final
 IMPLICIT_GENERIC_ANY_BUILTIN = \
     'Implicit generic "Any". Use "{}" and specify generic parameters'  # type: Final
 
diff --git a/mypy/typeanal.py b/mypy/typeanal.py
index ca300d125cb8..ab991174a4b9 100644
--- a/mypy/typeanal.py
+++ b/mypy/typeanal.py
@@ -55,6 +55,10 @@
     'mypy_extensions.KwArg': ARG_STAR2,
 }  # type: Final
 
+GENERIC_STUB_NOT_AT_RUNTIME_TYPES = {
+    'OrderedDict'
+}  # type: Final
+
 
 def analyze_type_alias(node: Expression,
                        api: SemanticAnalyzerCoreInterface,
@@ -907,8 +911,13 @@ def get_omitted_any(disallow_any: bool, fail: FailCallback,
             typ = unexpanded_type or typ
             type_str = typ.name if isinstance(typ, UnboundType) else format_type_bare(typ)
 
-            fail(message_registry.BARE_GENERIC.format(quote_type_string(type_str)), typ,
-                 code=codes.TYPE_ARG)
+            if type_str in GENERIC_STUB_NOT_AT_RUNTIME_TYPES:
+                # Recommend to put type in quotes (string literal escaping)
+                fail(message_registry.BARE_GENERIC_STRING_LITERAL_ESCAPE.format(
+                    quote_type_string(type_str)), typ, code=codes.TYPE_ARG)
+            else:
+                fail(message_registry.BARE_GENERIC.format(quote_type_string(type_str)), typ,
+                     code=codes.TYPE_ARG)
         any_type = AnyType(TypeOfAny.from_error, line=typ.line, column=typ.column)
     else:
         any_type = AnyType(TypeOfAny.from_omitted_generics, line=typ.line, column=typ.column)


Code review comments:
diff_hunk: @@ -907,8 +911,13 @@ def get_omitted_any(disallow_any: bool, fail: FailCallback,
             typ = unexpanded_type or typ
             type_str = typ.name if isinstance(typ, UnboundType) else format_type_bare(typ)
 
-            fail(message_registry.BARE_GENERIC.format(quote_type_string(type_str)), typ,
-                 code=codes.TYPE_ARG)
+            if type_str in GENERIC_STUB_NOT_AT_RUNTIME_TYPES:
path: mypy/typeanal.py
text: @user1:
I'm not sure I understand what's special about OrderedDict. The same thing would happen with any type that's defined both in collections and in typing. And in general I'm not sure that this particular hint belongs in the error message.

@author:
@user2 wanted this little reminder in the error message:
https://github.com/python/mypy/issues/7539#issuecomment-534124529

Indeed OrderedDicts are not unique that way, Queue would also have to go into the list.
This error message is useful because OderedDict[Any, Any] would pass mypy checks atm. but wouldn’t run. Many users would probably have difficulties understanding the problem and finding a fix.

So this is purely for usability.