Skip to content

Commit

Permalink
Add support of 'six.with_metaclass' (#841)
Browse files Browse the repository at this point in the history
Closes #713
  • Loading branch information
fmigneault authored Feb 7, 2021
1 parent 5f67396 commit 4629b93
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 0 deletions.
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ Release Date: TBA

Fixes #843

* Fix interpretation of ``six.with_metaclass`` class definitions.

Fixes #713

* Reduce memory usage of astroid's module cache.

* Remove dependency on `imp`.
Expand Down
39 changes: 39 additions & 0 deletions astroid/brain/brain_six.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@


SIX_ADD_METACLASS = "six.add_metaclass"
SIX_WITH_METACLASS = "six.with_metaclass"


def _indent(text, prefix, predicate=None):
Expand Down Expand Up @@ -190,6 +191,39 @@ def transform_six_add_metaclass(node):
return node


def _looks_like_nested_from_six_with_metaclass(node):
if len(node.bases) != 1:
return False
base = node.bases[0]
if not isinstance(base, nodes.Call):
return False
try:
if hasattr(base.func, "expr"):
# format when explicit 'six.with_metaclass' is used
mod = base.func.expr.name
func = base.func.attrname
func = "{}.{}".format(mod, func)
else:
# format when 'with_metaclass' is used directly (local import from six)
# check reference module to avoid 'with_metaclass' name clashes
mod = base.parent.parent
import_from = mod.locals["with_metaclass"][0]
func = "{}.{}".format(import_from.modname, base.func.name)
except (AttributeError, KeyError, IndexError):
return False
return func == SIX_WITH_METACLASS


def transform_six_with_metaclass(node):
"""Check if the given class node is defined with *six.with_metaclass*
If so, inject its argument as the metaclass of the underlying class.
"""
call = node.bases[0]
node._metaclass = call.args[0]
node.bases = call.args[1:]


register_module_extender(MANAGER, "six", six_moves_transform)
register_module_extender(
MANAGER, "requests.packages.urllib3.packages.six", six_moves_transform
Expand All @@ -200,3 +234,8 @@ def transform_six_add_metaclass(node):
transform_six_add_metaclass,
_looks_like_decorated_with_six_add_metaclass,
)
MANAGER.register_transform(
nodes.ClassDef,
transform_six_with_metaclass,
_looks_like_nested_from_six_with_metaclass,
)
84 changes: 84 additions & 0 deletions tests/unittest_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -2997,6 +2997,23 @@ class A(object):
self.assertIsInstance(inferred, nodes.Const)
self.assertEqual(inferred.value, 24)

def test_with_metaclass__getitem__(self):
ast_node = extract_node(
"""
class Meta(type):
def __getitem__(cls, arg):
return 24
import six
class A(six.with_metaclass(Meta)):
pass
A['Awesome'] #@
"""
)
inferred = next(ast_node.infer())
self.assertIsInstance(inferred, nodes.Const)
self.assertEqual(inferred.value, 24)

def test_bin_op_classes(self):
ast_node = extract_node(
"""
Expand All @@ -3015,6 +3032,23 @@ class A(object):
self.assertIsInstance(inferred, nodes.Const)
self.assertEqual(inferred.value, 24)

def test_bin_op_classes_with_metaclass(self):
ast_node = extract_node(
"""
class Meta(type):
def __or__(self, other):
return 24
import six
class A(six.with_metaclass(Meta)):
pass
A | A
"""
)
inferred = next(ast_node.infer())
self.assertIsInstance(inferred, nodes.Const)
self.assertEqual(inferred.value, 24)

def test_bin_op_supertype_more_complicated_example(self):
ast_node = extract_node(
"""
Expand Down Expand Up @@ -3354,6 +3388,22 @@ class A(object):
self.assertIsInstance(inferred, nodes.Const)
self.assertEqual(inferred.value, 42)

def test_unary_op_classes_with_metaclass(self):
ast_node = extract_node(
"""
import six
class Meta(type):
def __invert__(self):
return 42
class A(six.with_metaclass(Meta)):
pass
~A
"""
)
inferred = next(ast_node.infer())
self.assertIsInstance(inferred, nodes.Const)
self.assertEqual(inferred.value, 42)

def _slicing_test_helper(self, pairs, cls, get_elts):
for code, expected in pairs:
ast_node = extract_node(code)
Expand Down Expand Up @@ -3750,6 +3800,40 @@ class B(object):
self.assertIsInstance(inferred, nodes.ClassDef)
self.assertEqual(inferred.name, "B")

def test_With_metaclass_subclasses_arguments_are_classes_not_instances(self):
ast_node = extract_node(
"""
class A(type):
def test(cls):
return cls
import six
class B(six.with_metaclass(A)):
pass
B.test() #@
"""
)
inferred = next(ast_node.infer())
self.assertIsInstance(inferred, nodes.ClassDef)
self.assertEqual(inferred.name, "B")

def test_With_metaclass_with_partial_imported_name(self):
ast_node = extract_node(
"""
class A(type):
def test(cls):
return cls
from six import with_metaclass
class B(with_metaclass(A)):
pass
B.test() #@
"""
)
inferred = next(ast_node.infer())
self.assertIsInstance(inferred, nodes.ClassDef)
self.assertEqual(inferred.name, "B")

def test_infer_cls_in_class_methods(self):
ast_nodes = extract_node(
"""
Expand Down

0 comments on commit 4629b93

Please sign in to comment.