Repository: pydantic/pydantic

Issue:
MyPy plugin doesn't work with MyPy 0.750
# Bug

Please complete:
* OS: Debian Linux & MacOS
* Python version `import sys; print(sys.version)`: **3.8**
* Pydantic version `import pydantic; print(pydantic.VERSION)`: **1.1**, **1.2**


MyPy 0.750 has just been released and it looks like it has changed plugin system, causing plugin to error:

```console
mypy misago --ignore-missing-imports --show-traceback
/Users/rafalpiton/github/misago-starlette/venv/lib/python3.8/site-packages/mypy/typeshed/stdlib/3/typing.pyi:146: error: INTERNAL ERROR -- Please try using mypy master on Github:
https://mypy.rtfd.io/en/latest/common_issues.html#using-a-development-mypy-build
Please report a bug at https://github.com/python/mypy/issues
version: 0.750
Traceback (most recent call last):
  File "mypy/semanal.py", line 4656, in accept
  File "mypy/nodes.py", line 939, in accept
  File "mypy/semanal.py", line 1029, in visit_class_def
  File "mypy/semanal.py", line 1106, in analyze_class
  File "mypy/semanal.py", line 1115, in analyze_class_body_common
  File "mypy/semanal.py", line 1173, in apply_class_plugin_hooks
  File "mypy/plugin.py", line 818, in get_base_class_hook
  File "mypy/plugin.py", line 830, in _find_hook
  File "mypy/plugin.py", line 818, in <lambda>
  File "mypy/plugin.py", line 743, in get_base_class_hook
  File "/Users/rafalpiton/github/misago-starlette/venv/lib/python3.8/site-packages/pydantic/mypy.py", line 79, in get_base_class_hook
    if any(base.fullname() == BASEMODEL_FULLNAME for base in sym.node.mro):
  File "/Users/rafalpiton/github/misago-starlette/venv/lib/python3.8/site-packages/pydantic/mypy.py", line 79, in <genexpr>
    if any(base.fullname() == BASEMODEL_FULLNAME for base in sym.node.mro):
TypeError: 'str' object is not callable
/Users/rafalpiton/github/misago-starlette/venv/lib/python3.8/site-packages/mypy/typeshed/stdlib/3/typing.pyi:146: : note: use --pdb to drop into pdb```



Discussion:
Thanks for reporting this. This is due to a noted breaking change in mypy 0.750. I’ll submit a patch PR shortly.

Any suggestions about how we could better handle breaking changes in mypy going forward would be appreciated!
In particular, it would be easy to log a warning if using a version of mypy that is not one against which the pydantic plugin has been tested. Would this be desirable?

Resolving patch:
diff --git a/pydantic/mypy.py b/pydantic/mypy.py
--- a/pydantic/mypy.py
+++ b/pydantic/mypy.py
@@ -1,5 +1,5 @@
 from configparser import ConfigParser
-from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type as TypingType
+from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type as TypingType, Union
 
 from mypy.errorcodes import ErrorCode
 from mypy.nodes import (
@@ -17,6 +17,7 @@
     Context,
     Decorator,
     EllipsisExpr,
+    FuncBase,
     FuncDef,
     JsonDict,
     MemberExpr,
@@ -25,6 +26,7 @@
     PlaceholderNode,
     RefExpr,
     StrExpr,
+    SymbolNode,
     SymbolTableNode,
     TempNode,
     TypeInfo,
@@ -47,6 +49,7 @@
     TypeVarDef,
     TypeVarType,
     UnionType,
+    get_proper_type,
 )
 from mypy.typevars import fill_typevars
 from mypy.util import get_unique_redefinition_name
@@ -78,7 +81,7 @@ def get_base_class_hook(self, fullname: str) -> 'Optional[Callable[[ClassDefCont
         sym = self.lookup_fully_qualified(fullname)
         if sym and isinstance(sym.node, TypeInfo):  # pragma: no branch
             # No branching may occur if the mypy cache has not been cleared
-            if any(base.fullname() == BASEMODEL_FULLNAME for base in sym.node.mro):
+            if any(get_fullname(base) == BASEMODEL_FULLNAME for base in sym.node.mro):
                 return self._pydantic_model_class_maker_callback
         return None
 
@@ -132,7 +135,7 @@ def from_orm_callback(ctx: MethodContext) -> Type:
         return ctx.default_return_type
     orm_mode = pydantic_metadata.get('config', {}).get('orm_mode')
     if orm_mode is not True:
-        error_from_orm(model_type.type.name(), ctx.api, ctx.context)
+        error_from_orm(get_name(model_type.type), ctx.api, ctx.context)
     return ctx.default_return_type
 
 
@@ -168,7 +171,7 @@ def transform(self) -> None:
             if info[field.name].type is None:
                 if not ctx.api.final_iteration:
                     ctx.api.defer()
-        is_settings = any(base.fullname() == BASESETTINGS_FULLNAME for base in info.mro[:-1])
+        is_settings = any(get_fullname(base) == BASESETTINGS_FULLNAME for base in info.mro[:-1])
         self.add_initializer(fields, config, is_settings)
         self.add_construct_method(fields)
         self.set_frozen(fields, frozen=config.allow_mutation is False)
@@ -203,7 +206,7 @@ def collect_config(self) -> 'ModelConfigData':
                 continue
 
             # Each class depends on the set of fields in its ancestors
-            ctx.api.add_plugin_dependency(make_wildcard_trigger(info.fullname()))
+            ctx.api.add_plugin_dependency(make_wildcard_trigger(get_fullname(info)))
             for name, value in info.metadata[METADATA_KEY]['config'].items():
                 config.setdefault(name, value)
         return config
@@ -275,7 +278,7 @@ def collect_fields(self, model_config: 'ModelConfigData') -> List['PydanticModel
 
             superclass_fields = []
             # Each class depends on the set of fields in its ancestors
-            ctx.api.add_plugin_dependency(make_wildcard_trigger(info.fullname()))
+            ctx.api.add_plugin_dependency(make_wildcard_trigger(get_fullname(info)))
 
             for name, data in info.metadata[METADATA_KEY]['fields'].items():
                 if name not in known_fields:
@@ -357,8 +360,8 @@ def set_frozen(self, fields: List['PydanticModelField'], frozen: bool) -> None:
                 var = field.to_var(info, use_alias=False)
                 var.info = info
                 var.is_property = frozen
-                var._fullname = info.fullname() + '.' + var.name()
-                info.names[var.name()] = SymbolTableNode(MDEF, var)
+                var._fullname = get_fullname(info) + '.' + get_name(var)
+                info.names[get_name(var)] = SymbolTableNode(MDEF, var)
 
     def get_config_update(self, substmt: AssignmentStmt) -> Optional['ModelConfigData']:
         """
@@ -396,7 +399,7 @@ def get_is_required(cls: ClassDef, stmt: AssignmentStmt, lhs: NameExpr) -> bool:
         expr = stmt.rvalue
         if isinstance(expr, TempNode):
             # TempNode means annotation-only, so only non-required if Optional
-            value_type = cls.info[lhs.name].type
+            value_type = get_proper_type(cls.info[lhs.name].type)
             if isinstance(value_type, UnionType) and any(isinstance(item, NoneType) for item in value_type.items):
                 # Annotated as Optional, or otherwise having NoneType in the union
                 return False
@@ -618,7 +621,7 @@ def add_method(
     for arg in args:
         assert arg.type_annotation, 'All arguments must be fully typed.'
         arg_types.append(arg.type_annotation)
-        arg_names.append(arg.variable.name())
+        arg_names.append(get_name(arg.variable))
         arg_kinds.append(arg.kind)
 
     function_type = ctx.api.named_type('__builtins__.function')
@@ -631,7 +634,7 @@ def add_method(
     func.type = set_callable_name(signature, func)
     func.is_class = is_classmethod
     # func.is_static = is_staticmethod
-    func._fullname = info.fullname() + '.' + name
+    func._fullname = get_fullname(info) + '.' + name
     func.line = info.line
 
     # NOTE: we would like the plugin generated node to dominate, but we still
@@ -661,3 +664,23 @@ def add_method(
 
     info.names[name] = sym
     info.defn.defs.body.append(func)
+
+
+def get_fullname(x: Union[FuncBase, SymbolNode]) -> str:
+    """
+    Used for compatibility with mypy 0.740; can be dropped once support for 0.740 is dropped.
+    """
+    fn = x.fullname
+    if callable(fn):  # pragma: no cover
+        return fn()
+    return fn
+
+
+def get_name(x: Union[FuncBase, SymbolNode]) -> str:
+    """
+    Used for compatibility with mypy 0.740; can be dropped once support for 0.740 is dropped.
+    """
+    fn = x.name
+    if callable(fn):  # pragma: no cover
+        return fn()
+    return fn


Test patch:
diff --git a/tests/mypy/test_mypy.py b/tests/mypy/test_mypy.py
--- a/tests/mypy/test_mypy.py
+++ b/tests/mypy/test_mypy.py
@@ -52,9 +52,9 @@ def test_mypy_results(config_filename, python_filename, output_filename):
     # Specifying a different cache dir for each configuration dramatically speeds up subsequent execution
     # It also prevents cache-invalidation-related bugs in the tests
     cache_dir = f'.mypy_cache/test-{config_filename[:-4]}'
-    actual_result = mypy_api.run(
-        [full_filename, '--config-file', full_config_filename, '--cache-dir', cache_dir, '--show-error-codes']
-    )
+    command = [full_filename, '--config-file', full_config_filename, '--cache-dir', cache_dir, '--show-error-codes']
+    print(f"\nExecuting: mypy {' '.join(command)}")  # makes it easier to debug as necessary
+    actual_result = mypy_api.run(command)
     actual_out, actual_err, actual_returncode = actual_result
     # Need to strip filenames due to differences in formatting by OS
     actual_out = '\n'.join(['.py:'.join(line.split('.py:')[1:]) for line in actual_out.split('\n') if line]).strip()
diff --git a/tests/requirements.txt b/tests/requirements.txt
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -4,7 +4,7 @@ Cython==0.29.14;sys_platform!='win32'
 flake8==3.7.9
 flake8-quotes==2.1.1
 isort==4.3.21
-mypy==0.740
+mypy==0.750
 pycodestyle==2.5.0
 pyflakes==2.1.1
 pytest==5.3.1