Skip to content

Commit 3ef1e18

Browse files
elazargilevkivskyi
authored andcommitted
Support six.add_metaclass (#3842)
Fix #3365 There are no special checks for improper use (similar to six.with_metaclass). Consistency checks are handled like in any other metaclass.
1 parent 60527de commit 3ef1e18

File tree

3 files changed

+119
-36
lines changed

3 files changed

+119
-36
lines changed

mypy/semanal.py

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,7 @@ def visit_class_def(self, defn: ClassDef) -> None:
679679
def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]:
680680
with self.tvar_scope_frame(self.tvar_scope.class_frame()):
681681
is_protocol = self.detect_protocol_base(defn)
682+
self.update_metaclass(defn)
682683
self.clean_up_bases_and_infer_type_variables(defn)
683684
self.analyze_class_keywords(defn)
684685
if self.analyze_typeddict_classdef(defn):
@@ -832,12 +833,8 @@ def clean_up_bases_and_infer_type_variables(self, defn: ClassDef) -> None:
832833
Now we will remove Generic[T] from bases of Foo and infer that the
833834
type variable 'T' is a type argument of Foo.
834835
835-
We also process six.with_metaclass() here.
836-
837836
Note that this is performed *before* semantic analysis.
838837
"""
839-
# First process six.with_metaclass if present and well-formed
840-
defn.base_type_exprs, defn.metaclass = self.check_with_metaclass(defn)
841838
removed = [] # type: List[int]
842839
declared_tvars = [] # type: TypeVarList
843840
for i, base_expr in enumerate(defn.base_type_exprs):
@@ -1090,18 +1087,56 @@ def analyze_base_classes(self, defn: ClassDef) -> None:
10901087
if defn.info.is_enum and defn.type_vars:
10911088
self.fail("Enum class cannot be generic", defn)
10921089

1093-
def check_with_metaclass(self, defn: ClassDef) -> Tuple[List[Expression],
1094-
Optional[Expression]]:
1095-
# Special-case six.with_metaclass(M, B1, B2, ...).
1096-
if defn.metaclass is None and len(defn.base_type_exprs) == 1:
1090+
def update_metaclass(self, defn: ClassDef) -> None:
1091+
"""Lookup for special metaclass declarations, and update defn fields accordingly.
1092+
1093+
* __metaclass__ attribute in Python 2
1094+
* six.with_metaclass(M, B1, B2, ...)
1095+
* @six.add_metaclass(M)
1096+
"""
1097+
1098+
# Look for "__metaclass__ = <metaclass>" in Python 2
1099+
python2_meta_expr = None # type: Optional[Expression]
1100+
if self.options.python_version[0] == 2:
1101+
for body_node in defn.defs.body:
1102+
if isinstance(body_node, ClassDef) and body_node.name == "__metaclass__":
1103+
self.fail("Metaclasses defined as inner classes are not supported", body_node)
1104+
break
1105+
elif isinstance(body_node, AssignmentStmt) and len(body_node.lvalues) == 1:
1106+
lvalue = body_node.lvalues[0]
1107+
if isinstance(lvalue, NameExpr) and lvalue.name == "__metaclass__":
1108+
python2_meta_expr = body_node.rvalue
1109+
1110+
# Look for six.with_metaclass(M, B1, B2, ...)
1111+
with_meta_expr = None # type: Optional[Expression]
1112+
if len(defn.base_type_exprs) == 1:
10971113
base_expr = defn.base_type_exprs[0]
10981114
if isinstance(base_expr, CallExpr) and isinstance(base_expr.callee, RefExpr):
10991115
base_expr.callee.accept(self)
11001116
if (base_expr.callee.fullname == 'six.with_metaclass'
11011117
and len(base_expr.args) >= 1
11021118
and all(kind == ARG_POS for kind in base_expr.arg_kinds)):
1103-
return (base_expr.args[1:], base_expr.args[0])
1104-
return (defn.base_type_exprs, defn.metaclass)
1119+
with_meta_expr = base_expr.args[0]
1120+
defn.base_type_exprs = base_expr.args[1:]
1121+
1122+
# Look for @six.add_metaclass(M)
1123+
add_meta_expr = None # type: Optional[Expression]
1124+
for dec_expr in defn.decorators:
1125+
if isinstance(dec_expr, CallExpr) and isinstance(dec_expr.callee, RefExpr):
1126+
dec_expr.callee.accept(self)
1127+
if (dec_expr.callee.fullname == 'six.add_metaclass'
1128+
and len(dec_expr.args) == 1
1129+
and dec_expr.arg_kinds[0] == ARG_POS):
1130+
add_meta_expr = dec_expr.args[0]
1131+
break
1132+
1133+
metas = {defn.metaclass, python2_meta_expr, with_meta_expr, add_meta_expr} - {None}
1134+
if len(metas) == 0:
1135+
return
1136+
if len(metas) > 1:
1137+
self.fail("Multiple metaclass definitions", defn)
1138+
return
1139+
defn.metaclass = metas.pop()
11051140

11061141
def expr_to_analyzed_type(self, expr: Expression) -> Type:
11071142
if isinstance(expr, CallExpr):
@@ -1150,16 +1185,6 @@ def is_base_class(self, t: TypeInfo, s: TypeInfo) -> bool:
11501185
return False
11511186

11521187
def analyze_metaclass(self, defn: ClassDef) -> None:
1153-
if defn.metaclass is None and self.options.python_version[0] == 2:
1154-
# Look for "__metaclass__ = <metaclass>" in Python 2.
1155-
for body_node in defn.defs.body:
1156-
if isinstance(body_node, ClassDef) and body_node.name == "__metaclass__":
1157-
self.fail("Metaclasses defined as inner classes are not supported", body_node)
1158-
return
1159-
elif isinstance(body_node, AssignmentStmt) and len(body_node.lvalues) == 1:
1160-
lvalue = body_node.lvalues[0]
1161-
if isinstance(lvalue, NameExpr) and lvalue.name == "__metaclass__":
1162-
defn.metaclass = body_node.rvalue
11631188
if defn.metaclass:
11641189
if isinstance(defn.metaclass, NameExpr):
11651190
metaclass_name = defn.metaclass.name
@@ -1193,7 +1218,7 @@ def analyze_metaclass(self, defn: ClassDef) -> None:
11931218
if defn.info.metaclass_type is None:
11941219
# Inconsistency may happen due to multiple baseclasses even in classes that
11951220
# do not declare explicit metaclass, but it's harder to catch at this stage
1196-
if defn.metaclass:
1221+
if defn.metaclass is not None:
11971222
self.fail("Inconsistent metaclass structure for '%s'" % defn.name, defn)
11981223

11991224
def object_type(self) -> Instance:

test-data/unit/check-classes.test

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3470,64 +3470,94 @@ reveal_type(y['b']) # E: Revealed type is '__main__.B'
34703470
-- Special support for six
34713471
-- -----------------------
34723472

3473-
[case testSixWithMetaclass]
3473+
[case testSixMetaclass]
34743474
import six
34753475
class M(type):
34763476
x = 5
34773477
class A(six.with_metaclass(M)): pass
3478+
@six.add_metaclass(M)
3479+
class B: pass
34783480
reveal_type(type(A).x) # E: Revealed type is 'builtins.int'
3481+
reveal_type(type(B).x) # E: Revealed type is 'builtins.int'
34793482

3480-
[case testSixWithMetaclass_python2]
3483+
[case testSixMetaclass_python2]
34813484
import six
34823485
class M(type):
34833486
x = 5
34843487
class A(six.with_metaclass(M)): pass
3488+
@six.add_metaclass(M)
3489+
class B: pass
34853490
reveal_type(type(A).x) # E: Revealed type is 'builtins.int'
3491+
reveal_type(type(B).x) # E: Revealed type is 'builtins.int'
34863492

3487-
[case testFromSixWithMetaclass]
3488-
from six import with_metaclass
3493+
[case testFromSixMetaclass]
3494+
from six import with_metaclass, add_metaclass
34893495
class M(type):
34903496
x = 5
34913497
class A(with_metaclass(M)): pass
3498+
@add_metaclass(M)
3499+
class B: pass
34923500
reveal_type(type(A).x) # E: Revealed type is 'builtins.int'
3501+
reveal_type(type(B).x) # E: Revealed type is 'builtins.int'
34933502

3494-
[case testSixWithMetaclassImportFrom]
3503+
[case testSixMetaclassImportFrom]
34953504
import six
34963505
from metadefs import M
34973506
class A(six.with_metaclass(M)): pass
3507+
@six.add_metaclass(M)
3508+
class B: pass
34983509
reveal_type(type(A).x) # E: Revealed type is 'builtins.int'
3510+
reveal_type(type(B).x) # E: Revealed type is 'builtins.int'
34993511
[file metadefs.py]
35003512
class M(type):
35013513
x = 5
35023514

3503-
[case testSixWithMetaclassImport]
3515+
[case testSixMetaclassImport]
35043516
import six
35053517
import metadefs
35063518
class A(six.with_metaclass(metadefs.M)): pass
3519+
@six.add_metaclass(metadefs.M)
3520+
class B: pass
35073521
reveal_type(type(A).x) # E: Revealed type is 'builtins.int'
3522+
reveal_type(type(B).x) # E: Revealed type is 'builtins.int'
35083523
[file metadefs.py]
35093524
class M(type):
35103525
x = 5
35113526

3512-
[case testSixWithMetaclassAndBase]
3527+
[case testSixMetaclassAndBase]
3528+
from typing import Iterable, Iterator
35133529
import six
3514-
class M(type):
3530+
class M(type, Iterable[int]):
35153531
x = 5
3532+
def __iter__(self) -> Iterator[int]: ...
35163533
class A:
35173534
def foo(self): pass
35183535
class B:
35193536
def bar(self): pass
35203537
class C1(six.with_metaclass(M, A)): pass
3538+
@six.add_metaclass(M)
3539+
class D1(A): pass
35213540
class C2(six.with_metaclass(M, A, B)): pass
3541+
@six.add_metaclass(M)
3542+
class D2(A, B): pass
35223543
reveal_type(type(C1).x) # E: Revealed type is 'builtins.int'
3544+
reveal_type(type(D1).x) # E: Revealed type is 'builtins.int'
35233545
reveal_type(type(C2).x) # E: Revealed type is 'builtins.int'
3546+
reveal_type(type(D2).x) # E: Revealed type is 'builtins.int'
35243547
C1().foo()
3548+
D1().foo()
35253549
C1().bar() # E: "C1" has no attribute "bar"
3550+
D1().bar() # E: "D1" has no attribute "bar"
3551+
for x in C1: reveal_type(x) # E: Revealed type is 'builtins.int*'
3552+
for x in C2: reveal_type(x) # E: Revealed type is 'builtins.int*'
35263553
C2().foo()
3554+
D2().foo()
35273555
C2().bar()
3556+
D2().bar()
35283557
C2().baz() # E: "C2" has no attribute "baz"
3558+
D2().baz() # E: "D2" has no attribute "baz"
35293559

3530-
[case testSixWithMetaclassGenerics]
3560+
[case testSixMetaclassGenerics]
35313561
from typing import Generic, GenericMeta, TypeVar
35323562
import six
35333563
class DestroyableMeta(type):
@@ -3539,25 +3569,52 @@ class ArcMeta(GenericMeta, DestroyableMeta):
35393569
pass
35403570
class Arc(six.with_metaclass(ArcMeta, Generic[T_co], Destroyable)):
35413571
pass
3572+
@six.add_metaclass(ArcMeta)
3573+
class Arc1(Generic[T_co], Destroyable):
3574+
pass
35423575
class MyDestr(Destroyable):
35433576
pass
35443577
reveal_type(Arc[MyDestr]()) # E: Revealed type is '__main__.Arc[__main__.MyDestr*]'
3578+
reveal_type(Arc1[MyDestr]()) # E: Revealed type is '__main__.Arc1[__main__.MyDestr*]'
35453579
[builtins fixtures/bool.pyi]
35463580

3547-
[case testSixWithMetaclassErrors]
3581+
[case testSixMetaclassErrors]
35483582
import six
35493583
class M(type): pass
35503584
class A(object): pass
35513585
def f() -> type: return M
35523586
class C1(six.with_metaclass(M), object): pass # E: Invalid base class
35533587
class C2(C1, six.with_metaclass(M)): pass # E: Invalid base class
35543588
class C3(six.with_metaclass(A)): pass # E: Metaclasses not inheriting from 'type' are not supported
3555-
class C4(six.with_metaclass(M), metaclass=M): pass # E: Invalid base class
3589+
@six.add_metaclass(A) # E: Metaclasses not inheriting from 'type' are not supported
3590+
class D3(A): pass
3591+
class C4(six.with_metaclass(M), metaclass=M): pass # E: Multiple metaclass definitions
3592+
@six.add_metaclass(M) # E: Multiple metaclass definitions
3593+
class D4(metaclass=M): pass
35563594
class C5(six.with_metaclass(f())): pass # E: Dynamic metaclass not supported for 'C5'
3595+
@six.add_metaclass(f()) # E: Dynamic metaclass not supported for 'D5'
3596+
class D5: pass
3597+
3598+
@six.add_metaclass(M) # E: Multiple metaclass definitions
3599+
class CD(six.with_metaclass(M)): pass
35573600

3558-
[case testSixWithMetaclassErrors_python2-skip]
3559-
# No error here yet
3601+
class M1(type): pass
3602+
class Q1(metaclass=M1): pass
3603+
@six.add_metaclass(M) # E: Inconsistent metaclass structure for 'CQA'
3604+
class CQA(Q1): pass
3605+
class CQW(six.with_metaclass(M, Q1)): pass # E: Inconsistent metaclass structure for 'CQW'
3606+
3607+
[case testSixMetaclassErrors_python2]
3608+
# flags: --python-version 2.7
35603609
import six
35613610
class M(type): pass
3562-
class C4(six.with_metaclass(M)):
3611+
class C4(six.with_metaclass(M)): # E: Multiple metaclass definitions
35633612
__metaclass__ = M
3613+
3614+
[case testSixMetaclassAny]
3615+
import t # type: ignore
3616+
import six
3617+
class E(metaclass=t.M): pass
3618+
class F(six.with_metaclass(t.M)): pass
3619+
@six.add_metaclass(t.M)
3620+
class G: pass

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

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

0 commit comments

Comments
 (0)