summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Cristau <julien.cristau@logilab.fr>2014-06-10 18:48:00 +0200
committerJulien Cristau <julien.cristau@logilab.fr>2014-06-10 18:48:00 +0200
commit405891c46d714aa0880e4151d53e6cb919d7ab06 (patch)
treee8fae06a6693bb5cb250f89dc7eefba9a8b9eda1
parent4b7dfdddac5349e3ca5e3738e0a7dfa4c7958b1c (diff)
downloadastroid-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.py20
-rw-r--r--scoped_nodes.py32
-rw-r--r--test/unittest_scoped_nodes.py35
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):