Skip to content

Commit 3f2668d

Browse files
authored
Support six.with_metaclass (#3364)
Fixes #1764. This doesn't add support for @six.add_metaclass(M) -- if that's required we'll add it later (we should open a separate issue for that). It does support generics (at least in the base classes).
1 parent 2ca2eb4 commit 3f2668d

File tree

4 files changed

+121
-0
lines changed

4 files changed

+121
-0
lines changed

mypy/semanal.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,8 +758,12 @@ def clean_up_bases_and_infer_type_variables(self, defn: ClassDef) -> None:
758758
Now we will remove Generic[T] from bases of Foo and infer that the
759759
type variable 'T' is a type argument of Foo.
760760
761+
We also process six.with_metaclass() here.
762+
761763
Note that this is performed *before* semantic analysis.
762764
"""
765+
# First process six.with_metaclass if present and well-formed
766+
defn.base_type_exprs, defn.metaclass = self.check_with_metaclass(defn)
763767
removed = [] # type: List[int]
764768
declared_tvars = [] # type: TypeVarList
765769
for i, base_expr in enumerate(defn.base_type_exprs):
@@ -938,6 +942,7 @@ def analyze_base_classes(self, defn: ClassDef) -> None:
938942

939943
base_types = [] # type: List[Instance]
940944
info = defn.info
945+
941946
for base_expr in defn.base_type_exprs:
942947
try:
943948
base = self.expr_to_analyzed_type(base_expr)
@@ -989,6 +994,27 @@ def analyze_base_classes(self, defn: ClassDef) -> None:
989994
if defn.info.is_enum and defn.type_vars:
990995
self.fail("Enum class cannot be generic", defn)
991996

997+
def check_with_metaclass(self, defn: ClassDef) -> Tuple[List[Expression], Optional[str]]:
998+
# Special-case six.with_metaclass(M, B1, B2, ...).
999+
base_type_exprs, metaclass = defn.base_type_exprs, defn.metaclass
1000+
if metaclass is None and len(base_type_exprs) == 1:
1001+
base_expr = base_type_exprs[0]
1002+
if isinstance(base_expr, CallExpr) and isinstance(base_expr.callee, RefExpr):
1003+
base_expr.callee.accept(self)
1004+
if (base_expr.callee.fullname == 'six.with_metaclass'
1005+
and len(base_expr.args) >= 1
1006+
and all(kind == ARG_POS for kind in base_expr.arg_kinds)):
1007+
metaclass_expr = base_expr.args[0]
1008+
if isinstance(metaclass_expr, NameExpr):
1009+
metaclass = metaclass_expr.name
1010+
elif isinstance(metaclass_expr, MemberExpr):
1011+
metaclass = get_member_expr_fullname(metaclass_expr)
1012+
else:
1013+
self.fail("Dynamic metaclass not supported for '%s'" % defn.name,
1014+
metaclass_expr)
1015+
return (base_expr.args[1:], metaclass)
1016+
return (base_type_exprs, metaclass)
1017+
9921018
def expr_to_analyzed_type(self, expr: Expression) -> Type:
9931019
if isinstance(expr, CallExpr):
9941020
expr.accept(self)

test-data/unit/check-classes.test

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3137,3 +3137,94 @@ class M(type):
31373137
class A(metaclass=M): pass
31383138
reveal_type(type(A).x) # E: Revealed type is 'builtins.int'
31393139

3140+
[case testSixWithMetaclass]
3141+
import six
3142+
class M(type):
3143+
x = 5
3144+
class A(six.with_metaclass(M)): pass
3145+
reveal_type(type(A).x) # E: Revealed type is 'builtins.int'
3146+
3147+
[case testSixWithMetaclass_python2]
3148+
import six
3149+
class M(type):
3150+
x = 5
3151+
class A(six.with_metaclass(M)): pass
3152+
reveal_type(type(A).x) # E: Revealed type is 'builtins.int'
3153+
3154+
[case testFromSixWithMetaclass]
3155+
from six import with_metaclass
3156+
class M(type):
3157+
x = 5
3158+
class A(with_metaclass(M)): pass
3159+
reveal_type(type(A).x) # E: Revealed type is 'builtins.int'
3160+
3161+
[case testSixWithMetaclassImportFrom]
3162+
import six
3163+
from metadefs import M
3164+
class A(six.with_metaclass(M)): pass
3165+
reveal_type(type(A).x) # E: Revealed type is 'builtins.int'
3166+
[file metadefs.py]
3167+
class M(type):
3168+
x = 5
3169+
3170+
[case testSixWithMetaclassImport]
3171+
import six
3172+
import metadefs
3173+
class A(six.with_metaclass(metadefs.M)): pass
3174+
reveal_type(type(A).x) # E: Revealed type is 'builtins.int'
3175+
[file metadefs.py]
3176+
class M(type):
3177+
x = 5
3178+
3179+
[case testSixWithMetaclassAndBase]
3180+
import six
3181+
class M(type):
3182+
x = 5
3183+
class A:
3184+
def foo(self): pass
3185+
class B:
3186+
def bar(self): pass
3187+
class C1(six.with_metaclass(M, A)): pass
3188+
class C2(six.with_metaclass(M, A, B)): pass
3189+
reveal_type(type(C1).x) # E: Revealed type is 'builtins.int'
3190+
reveal_type(type(C2).x) # E: Revealed type is 'builtins.int'
3191+
C1().foo()
3192+
C1().bar() # E: "C1" has no attribute "bar"
3193+
C2().foo()
3194+
C2().bar()
3195+
C2().baz() # E: "C2" has no attribute "baz"
3196+
3197+
[case testSixWithMetaclassGenerics]
3198+
from typing import Generic, GenericMeta, TypeVar
3199+
import six
3200+
class DestroyableMeta(type):
3201+
pass
3202+
class Destroyable(six.with_metaclass(DestroyableMeta)):
3203+
pass
3204+
T_co = TypeVar('T_co', bound='Destroyable', covariant=True)
3205+
class ArcMeta(GenericMeta, DestroyableMeta):
3206+
pass
3207+
class Arc(six.with_metaclass(ArcMeta, Generic[T_co], Destroyable)):
3208+
pass
3209+
class MyDestr(Destroyable):
3210+
pass
3211+
reveal_type(Arc[MyDestr]()) # E: Revealed type is '__main__.Arc[__main__.MyDestr*]'
3212+
[builtins fixtures/bool.pyi]
3213+
3214+
[case testSixWithMetaclassErrors]
3215+
import six
3216+
class M(type): pass
3217+
class A(object): pass
3218+
def f() -> type: return M
3219+
class C1(six.with_metaclass(M), object): pass # E: Invalid base class
3220+
class C2(C1, six.with_metaclass(M)): pass # E: Invalid base class
3221+
class C3(six.with_metaclass(A)): pass # E: Metaclasses not inheriting from 'type' are not supported
3222+
class C4(six.with_metaclass(M), metaclass=M): pass # E: Invalid base class
3223+
class C5(six.with_metaclass(f())): pass # E: Dynamic metaclass not supported for 'C5'
3224+
3225+
[case testSixWithMetaclassErrors_python2-skip]
3226+
# No error here yet
3227+
import six
3228+
class M(type): pass
3229+
class C4(six.with_metaclass(M)):
3230+
__metaclass__ = M

test-data/unit/lib-stub/six.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from typing import Type
2+
def with_metaclass(mcls: Type[type], *args: type) -> type: pass

test-data/unit/lib-stub/typing.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from abc import abstractmethod
55

6+
class GenericMeta(type): pass
7+
68
cast = 0
79
overload = 0
810
Any = 0

0 commit comments

Comments
 (0)