Repository: python/mypy

Issue:
Parse error when metaclass value is a function call
Consider this reduced test case, which is valid Python:

``` python
def make_metaclass(name):

    class RegistryMetaclass(type):
        def __new__(mcs, cls_name, bases, attrs):
            attrs['name'] = name
            cls = super(RegistryMetaclass, mcs).__new__(
                mcs,
                cls_name,
                bases,
                attrs,
            )

    return RegistryMetaclass


class TestClass(metaclass=make_metaclass(name='Test')):
    pass
```

Running mypy 0.4.5, I get this error:

```
metaclass.py: note: In class "TestClass":
metaclass.py:15: error: Parse error before (
```

Supplying a class directly fixes the issue.



Discussion:
Does it work if you run mypy with `--fast-parser`?

Nope, doesn't work either:

```
/Users/sjaensch/metaclass.py: error: INTERNAL ERROR -- please report a bug at https://github.com/python/mypy/issues
Traceback (most recent call last):
  File "/Users/sjaensch/venv/bin/mypy", line 6, in <module>
    main(__file__)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/main.py", line 38, in main
    res = type_check_only(sources, bin_dir, options)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/main.py", line 79, in type_check_only
    options=options)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/build.py", line 181, in build
    dispatch(sources, manager)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/build.py", line 1468, in dispatch
    graph = load_graph(sources, manager)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/build.py", line 1487, in load_graph
    st = State(id=bs.module, path=bs.path, source=bs.text, manager=manager)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/build.py", line 1175, in __init__
    self.parse_file()
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/build.py", line 1340, in parse_file
    self.tree = manager.parse_file(self.id, self.xpath, source)
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/contextlib.py", line 77, in __exit__
    self.gen.throw(type, value, traceback)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/build.py", line 1259, in wrap_context
    yield
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/build.py", line 1340, in parse_file
    self.tree = manager.parse_file(self.id, self.xpath, source)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/build.py", line 437, in parse_file
    tree = parse(source, path, self.errors, options=self.options)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/parse.py", line 92, in parse
    custom_typing_module=options.custom_typing_module)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/fastparse.py", line 66, in parse
    ).visit(ast)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/typed_ast/ast35.py", line 256, in visit
    return visitor(node)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/fastparse.py", line 237, in visit_Module
    body = self.fix_function_overloads(self.translate_stmt_list(mod.body))
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/fastparse.py", line 135, in translate_stmt_list
    stmt = self.visit(e)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/typed_ast/ast35.py", line 256, in visit
    return visitor(node)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/fastparse.py", line 93, in wrapper
    node = f(self, ast)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/fastparse.py", line 391, in visit_ClassDef
    metaclass = self.stringify_name(metaclass_arg.value)
  File "/Users/sjaensch/venv/lib/python3.5/site-packages/mypy/fastparse.py", line 378, in stringify_name
    assert False, "can't stringify " + str(type(n))
AssertionError: can't stringify <class '_ast35.Call'>
```

OK, I think using a dynamically generated metaclass is well beyond mypy's intended capabilities, but it should not crash. The old parser I'll ignore (it'll go away soon enough) but the new parser should be more careful here.


Resolving patch:
diff --git a/mypy/fastparse.py b/mypy/fastparse.py
--- a/mypy/fastparse.py
+++ b/mypy/fastparse.py
@@ -373,9 +373,10 @@ def stringify_name(self, n: ast35.AST) -> str:
         if isinstance(n, ast35.Name):
             return n.id
         elif isinstance(n, ast35.Attribute):
-            return "{}.{}".format(self.stringify_name(n.value), n.attr)
-        else:
-            assert False, "can't stringify " + str(type(n))
+            sv = self.stringify_name(n.value)
+            if sv is not None:
+                return "{}.{}".format(sv, n.attr)
+        return None  # Can't do it.
 
     # ClassDef(identifier name,
     #  expr* bases,
@@ -389,6 +390,8 @@ def visit_ClassDef(self, n: ast35.ClassDef) -> ClassDef:
         metaclass = None
         if metaclass_arg:
             metaclass = self.stringify_name(metaclass_arg.value)
+            if metaclass is None:
+                metaclass = '<error>'  # To be reported later
 
         cdef = ClassDef(n.name,
                         self.as_block(n.body, n.lineno),
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -846,6 +846,9 @@ def is_base_class(self, t: TypeInfo, s: TypeInfo) -> bool:
 
     def analyze_metaclass(self, defn: ClassDef) -> None:
         if defn.metaclass:
+            if defn.metaclass == '<error>':
+                self.fail("Dynamic metaclass not supported for '%s'" % defn.name, defn)
+                return
             sym = self.lookup_qualified(defn.metaclass, defn)
             if sym is not None and not isinstance(sym.node, TypeInfo):
                 self.fail("Invalid metaclass '%s'" % defn.metaclass, defn)


Test patch:
diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test
--- a/test-data/unit/check-classes.test
+++ b/test-data/unit/check-classes.test
@@ -2070,3 +2070,8 @@ class A: pass
 class B(object, A): # E: Cannot determine consistent method resolution order (MRO) for "B"
     def readlines(self): pass
     __iter__ = readlines
+
+[case testDynamicMetaclass]
+# flags: --fast-parser
+class C(metaclass=int()):  # E: Dynamic metaclass not supported for 'C'
+    pass