Index: lib2to3/tests/test_fixers.py =================================================================== --- lib2to3/tests/test_fixers.py (revision 65337) +++ lib2to3/tests/test_fixers.py (working copy) @@ -3328,7 +3328,84 @@ """ self.check_both(b, a) +class Test_metaclass(FixerTestCase): + fixer = 'metaclass' + def test_nometa(self): + self.unchanged('class X(): pass') + self.unchanged('class X(object): pass') + self.unchanged('class X(object1, object2): pass') + self.unchanged('class X(object1, object2, object3): pass') + def test_meta(self): + # no-parent class, odd body + old = ''' + class X(): + __metaclass__ = Q + pass + ''' + new = ''' + class X(metaclass = Q): + pass + ''' + self.check(old, new) + + # one parent class, no body + old = 'class X(object): __metaclass__ = Q\n' + new = 'class X(object, metaclass = Q): pass\n' + self.check(old, new) + + + # one parent, simple body + old = ''' + class X(object): + __metaclass__ = Meta + bar = 7 + ''' + new = ''' + class X(object, metaclass = Meta): + bar = 7 + ''' + self.check(old, new) + + # one parent, simple body, __metaclass__ last + old = ''' + class X(object): + bar = 7 + __metaclass__ = Meta + ''' + + new = ''' + class X(object, metaclass = Meta): + bar = 7 + ''' + self.check(old, new) + + # redefining __metaclass__ + old = ''' + class X(): + __metaclass__ = A + __metaclass__ = B + bar = 7 + ''' + new = ''' + class X(metaclass = B): + bar = 7 + ''' + self.check(old, new) + + # multiple inheritance, simple body + old = ''' + class X(clsA, clsB): + __metaclass__ = Meta + bar = 7 + ''' + new = ''' + class X(clsA, clsB, metaclass = Meta): + bar = 7 + ''' + self.check(old, new) + + if __name__ == "__main__": import __main__ support.run_all_tests(__main__) Index: lib2to3/fixes/fix_metaclass.py =================================================================== --- lib2to3/fixes/fix_metaclass.py (revision 0) +++ lib2to3/fixes/fix_metaclass.py (revision 0) @@ -0,0 +1,221 @@ +"""Fixer for __metaclass__ = X -> (metaclass=X) methods. + + The various forms of classef (inherits nothing, inherits once, inherints + many) don't parse the same in the CST so we look at ALL classes for + a __metaclass__ and if we find one normalize the inherits to all be + an arglist. + + For one-liner classes ('class X: pass') there is no indent/dedent so + we normalize those into having a suite. + + Moving the __metaclass__ into the classdef can also cause the class + body to be empty so there is some special casing for that as well. + + This fixer also tries very hard to keep original indenting and spacing + in all those corner cases. + +""" +# Author: Jack Diederich + +# Local imports +from .. import fixer_base +from ..fixer_util import Name, syms, Node, Leaf +import os +token = fixer_base.pygram.token # for LPAR, RPAR, etc + +def has_metaclass(parent): + """ we have to check the cls_node without changing it. + There are two possiblities: + 1) clsdef => suite => simple_stmt => expr_stmt => Leaf('__meta') + 2) clsdef => simple_stmt => expr_stmt => Leaf('__meta') + """ + for node in parent.children: + if node.type == syms.suite: + return has_metaclass(node) + elif node.type == syms.simple_stmt and node.children: + expr_node = node.children[0] + if expr_node.type == syms.expr_stmt and expr_node.children: + leaf_node = expr_node.children[0] + if leaf_node.value == '__metaclass__': + return True + return False + +def fixup_parse_tree(cls_node): + """ one-line classes don't get a suite in the parse tree so we add + one to normalize the tree + """ + for node in cls_node.children: + if node.type == syms.suite: + # already in the prefered format, do nothing + return + + # !%@#! oneliners have no suite node, we have to fake one up + for i, node in enumerate(cls_node.children): + if node.type == token.COLON: + break + else: + raise ValueError("No class suite and no ':'!") + + # move everything into a suite node + suite = Node(syms.suite, []) + while cls_node.children[i+1:]: + move_node = cls_node.children[i+1] + suite.append_child(move_node.clone()) + move_node.remove() + cls_node.append_child(suite) + node = suite + return + +def fixup_simple_stmt(parent, i, stmt_node): + """ if there is a semi-colon all the parts count as part of the same + simple_stmt. We just want the __metaclass__ part so we move + everything efter the semi-colon into its own simple_stmt node + """ + for semi_ind, node in enumerate(stmt_node.children): + if node.type == token.SEMI: # *sigh* + break + else: + return + + node.remove() # kill the semicolon + new_expr = Node(syms.expr_stmt, []) + new_stmt = Node(syms.simple_stmt, [new_expr]) + while stmt_node.children[semi_ind:]: + move_node = stmt_node.children[semi_ind] + new_expr.append_child(move_node.clone()) + move_node.remove() + parent.insert_child(i, new_stmt) + new_leaf1 = new_stmt.children[0].children[0] + old_leaf1 = stmt_node.children[0].children[0] + new_leaf1.prefix = old_leaf1.prefix + return + +def remove_trailing_newline(node): + if node.children and node.children[-1].type == token.NEWLINE: + node.children[-1].remove() + return + +def find_metas(cls_node): + # find the suite node (Mmm, sweet nodes) + for node in cls_node.children: + if node.type == syms.suite: + break + else: + raise ValueError("No class suite!") + + # look for simple_stmt[ expr_stmt[ Leaf('__metaclass__') ] ] + for i, simple_node in list(enumerate(node.children)): + if simple_node.type == syms.simple_stmt and simple_node.children: + expr_node = simple_node.children[0] + if expr_node.type == syms.expr_stmt and expr_node.children: + leaf_node = expr_node.children[0] + if leaf_node.value == '__metaclass__': + fixup_simple_stmt(node, i, simple_node) + remove_trailing_newline(simple_node) + yield (node, i, simple_node) + return + +def fixup_indent(suite): + """ If an INDENT is followed by a thing with a prefix then nuke the prefix + Otherwise we get in trouble when removing __metaclass__ at suite start + """ + kids = suite.children[::-1] + # find the first indent + while kids: + node = kids.pop() + if node.type == token.INDENT: + break + + # find the first Leaf + while kids: + node = kids.pop() + if isinstance(node, Leaf): + if node.prefix: + node.prefix = '' + return + else: + kids.extend(node.children[::-1]) + + return + +class FixMetaclass(fixer_base.BaseFix): + PATTERN = """ + classdef + + """ + def transform(self, node, results): + if not has_metaclass(node): + return node + + fixup_parse_tree(node) + + # find metaclasses, keep the last one + last_metaclass = None + for suite, i, stmt in find_metas(node): + last_metaclass = stmt + stmt.remove() + + text_type = node.children[0].type # always Leaf(nnn, 'class') + + # figure out what kind of classdef we have + arglist = None + if len(node.children) == 7: + # Node(classdef, ['class', 'name', '(', arglist, ')', ':', suite]) + # 0 1 2 3 4 5 6 + if node.children[3].type == syms.arglist: + arglist = node.children[3] + # Node(classdef, ['class', 'name', '(', 'Parent', ')', ':', suite]) + elif isinstance(node.children[3], Leaf): + parent = node.children[3].clone() + arglist = Node(syms.arglist, [parent]) + node.set_child(3, arglist) + else: + raise ValueError("Unexpected class inheritance arglist") + elif len(node.children) == 6: + # Node(classdef, ['class', 'name', '(', ')', ':', suite]) + # 0 1 2 3 4 5 + arglist = Node(syms.arglist, []) + node.insert_child(3, arglist) + elif len(node.children) == 4: + # Node(classdef, ['class', 'name', ':', suite]) + # 0 1 2 3 + arglist = Node(syms.arglist, []) + node.insert_child(2, Leaf(token.RPAR, ')')) + node.insert_child(2, arglist) + node.insert_child(2, Leaf(token.LPAR, '(')) + else: + raise ValueError("Unexpected class definition") + + # now stick the metaclass in the arglist + meta_txt = last_metaclass.children[0].children[0] + meta_txt.value = 'metaclass' + orig_meta_prefix = meta_txt.prefix + + if arglist.children: + arglist.append_child(Leaf(token.COMMA, ',')) + meta_txt.prefix = ' ' + else: + meta_txt.prefix = '' + + arglist.append_child(last_metaclass) + + fixup_indent(suite) + + # check for empty suite + if not suite.children: + # one-liner that was just __meta + suite.remove() + pass_leaf = Leaf(text_type, 'pass') + pass_leaf.prefix = orig_meta_prefix + node.append_child(pass_leaf) + node.append_child(Leaf(token.NEWLINE, os.linesep)) + + elif len(suite.children) > 1 and \ + (suite.children[-2].type == token.INDENT and + suite.children[-1].type == token.DEDENT): + # there was only one line in the calss body and it was __meta + pass_leaf = Leaf(text_type, 'pass') + suite.insert_child(-1, pass_leaf) + suite.insert_child(-1, Leaf(token.NEWLINE, os.linesep)) + + return node Index: README =================================================================== --- README (revision 65337) +++ README (working copy) @@ -114,7 +114,9 @@ * **fix_xreadlines** - "for x in f.xreadlines():" -> "for x in f:". Also, "g(f.xreadlines)" -> "g(f.__iter__)". +* **fix_metaclass** - move __metaclass__ = M to class X(metaclass=M) + Limitations ===========