diff options
| author | Julien Cristau <julien.cristau@logilab.fr> | 2014-06-10 18:48:00 +0200 |
|---|---|---|
| committer | Julien Cristau <julien.cristau@logilab.fr> | 2014-06-10 18:48:00 +0200 |
| commit | 405891c46d714aa0880e4151d53e6cb919d7ab06 (patch) | |
| tree | e8fae06a6693bb5cb250f89dc7eefba9a8b9eda1 | |
| parent | 4b7dfdddac5349e3ca5e3738e0a7dfa4c7958b1c (diff) | |
| download | astroid-405891c46d714aa0880e4151d53e6cb919d7ab06.tar.gz | |
Handle __metaclass__ defined at the module level
According to the doc
(https://docs.python.org/2/reference/datamodel.html#customizing-class-creation):
The appropriate metaclass is determined by the following precedence rules:
- If dict['__metaclass__'] exists, it is used.
- Otherwise, if there is at least one base class, its metaclass is used
(this looks for a __class__ attribute first and if not found, uses its
type).
- Otherwise, if a global variable named __metaclass__ exists, it is used.
- Otherwise, the old-style, classic metaclass (types.ClassType) is used.
The third case was not handled by node.metaclass().
Remove metaclass lookup from the rebuilder, handle it all in
Class._explicit_metaclass() instead, and use that in _newstyle_impl if
possible.
Remove test_newstyle_and_metaclass_bad, as I think the returned value in
this test is irrelevant (it's a TypeError anyway, so you can't actually
build that class), and replace it with a test using nested classes.
Closes issue#33
| -rw-r--r-- | rebuilder.py | 20 | ||||
| -rw-r--r-- | scoped_nodes.py | 32 | ||||
| -rw-r--r-- | test/unittest_scoped_nodes.py | 35 |
3 files changed, 52 insertions, 35 deletions
diff --git a/rebuilder.py b/rebuilder.py index 40a614f..47eff50 100644 --- a/rebuilder.py +++ b/rebuilder.py @@ -116,12 +116,6 @@ def _set_infos(oldnode, newnode, parent): newnode.col_offset = oldnode.col_offset newnode.set_line_info(newnode.last_child()) # set_line_info accepts None -def _infer_metaclass(node): - if isinstance(node, Name): - return node.id - elif isinstance(node, Attribute): - return node.attr - def _create_yield_node(node, parent, rebuilder, factory): newnode = factory() _lineno_parent(node, newnode, parent) @@ -137,7 +131,6 @@ class TreeRebuilder(object): def __init__(self, manager): self._manager = manager self.asscontext = None - self._metaclass = [''] self._global_names = [] self._from_nodes = [] self._delayed_assattr = [] @@ -246,9 +239,6 @@ class TreeRebuilder(object): meth.extra_decorators.append(newnode.value) except (AttributeError, KeyError): continue - elif getattr(newnode.targets[0], 'name', None) == '__metaclass__': - # XXX check more... - self._metaclass[-1] = _infer_metaclass(node.value) newnode.set_line_info(newnode.last_child()) return newnode @@ -321,7 +311,6 @@ class TreeRebuilder(object): def visit_class(self, node, parent): """visit a Class node to become astroid""" - self._metaclass.append(self._metaclass[-1]) newnode = new.Class(node.name, None) _lineno_parent(node, newnode, parent) _init_set_doc(node, newnode) @@ -330,14 +319,6 @@ class TreeRebuilder(object): if 'decorator_list' in node._fields and node.decorator_list:# py >= 2.6 newnode.decorators = self.visit_decorators(node, newnode) newnode.set_line_info(newnode.last_child()) - metaclass = self._metaclass.pop() - if PY3K: - newnode._newstyle = True - else: - if not newnode.bases: - # no base classes, detect new / style old style according to - # current scope - newnode._newstyle = metaclass in ('type', 'ABCMeta') newnode.parent.frame().set_local(newnode.name, newnode) return newnode @@ -942,6 +923,7 @@ class TreeRebuilder3k(TreeRebuilder): def visit_class(self, node, parent): newnode = super(TreeRebuilder3k, self).visit_class(node, parent) + newnode._newstyle = True for keyword in node.keywords: if keyword.arg == 'metaclass': newnode._metaclass = self.visit(keyword, newnode).value diff --git a/scoped_nodes.py b/scoped_nodes.py index 889baa0..ac77583 100644 --- a/scoped_nodes.py +++ b/scoped_nodes.py @@ -476,7 +476,7 @@ else: """class representing a ListComp node""" # Function ################################################################### - + def _function_type(self): """ Function type, possible values are: @@ -633,7 +633,7 @@ class Function(Statement, Lambda): def is_abstract(self, pass_is_abstract=True): """Returns True if the method is abstract. - + A method is considered abstract if - the only statement is 'raise NotImplementedError', or - the only statement is 'pass' and pass_is_abstract is True, or @@ -805,6 +805,11 @@ class Class(Statement, LocalsDictNodeNG, FilterStmtsMixin): if base._newstyle_impl(context): self._newstyle = True break + klass = self._explicit_metaclass() + # could be any callable, we'd need to infer the result of klass(name, + # bases, dict). punt if it's not a class node. + if klass is not None and isinstance(klass, Class): + self._newstyle = klass._newstyle_impl(context) if self._newstyle is None: self._newstyle = False return self._newstyle @@ -1080,7 +1085,8 @@ class Class(Statement, LocalsDictNodeNG, FilterStmtsMixin): An explicit defined metaclass is defined either by passing the ``metaclass`` keyword argument in the class definition line (Python 3) or by - having a ``__metaclass__`` class attribute. + having a ``__metaclass__`` class attribute, or (Python 2) if there are + no explicit bases but there is a global ``__metaclass__`` variable. """ if self._metaclass: # Expects this from Py3k TreeRebuilder @@ -1088,14 +1094,22 @@ class Class(Statement, LocalsDictNodeNG, FilterStmtsMixin): return next(node for node in self._metaclass.infer() if node is not YES) except (InferenceError, StopIteration): - return + return None + + if '__metaclass__' in self.locals: + assignment = self.locals['__metaclass__'][-1] + elif self.bases or sys.version_info >= (3, ): + return None + elif '__metaclass__' in self.root().locals: + assignments = [ass for ass in self.root().locals['__metaclass__'] if ass.lineno < self.lineno] + if not assignments: + return None + assignment = assignments[-1] + else: + return None try: - meta = self.getattr('__metaclass__')[0] - except NotFoundError: - return - try: - infered = meta.infer().next() + infered = assignment.infer().next() except InferenceError: return if infered is YES: # don't expose this diff --git a/test/unittest_scoped_nodes.py b/test/unittest_scoped_nodes.py index 4d0bc59..62674f0 100644 --- a/test/unittest_scoped_nodes.py +++ b/test/unittest_scoped_nodes.py @@ -767,17 +767,38 @@ def g2(): """)) klass = astroid['Test'] self.assertTrue(klass.newstyle) - - def test_newstyle_and_metaclass_bad(self): + self.assertEqual(klass.metaclass().name, 'ABCMeta') astroid = abuilder.string_build(dedent(""" + from abc import ABCMeta + __metaclass__ = ABCMeta class Test: - __metaclass__ = int + pass """)) klass = astroid['Test'] - if PY3K: - self.assertTrue(klass.newstyle) - else: - self.assertFalse(klass.newstyle) + self.assertTrue(klass.newstyle) + self.assertEqual(klass.metaclass().name, 'ABCMeta') + + def test_nested_metaclass(self): + astroid = abuilder.string_build(dedent(""" + from abc import ABCMeta + class A(object): + __metaclass__ = ABCMeta + class B: pass + + __metaclass__ = ABCMeta + class C: + __metaclass__ = type + class D: pass + """)) + a = astroid['A'] + b = a.locals['B'][0] + c = astroid['C'] + d = c.locals['D'][0] + self.assertEqual(a.metaclass().name, 'ABCMeta') + self.assertFalse(b.newstyle) + self.assertIsNone(b.metaclass()) + self.assertEqual(c.metaclass().name, 'type') + self.assertEqual(d.metaclass().name, 'ABCMeta') @require_version('2.7') def test_parent_metaclass(self): |
