Skip to content
Snippets Groups Projects
Commit da26488c authored by Claudiu Popa's avatar Claudiu Popa
Browse files

Add brain inference support for the `issubclass` builtin

This is in a similar vein with the `isinstance` builtin, with minor changes.
parent 518e4a9d
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -6,6 +6,10 @@ Change log for the astroid package (used to be astng)
 
Part of PyCQA/pylint#811
 
* Add brain tip for `issubclass` builtin
Close #101.
* Fix submodule imports from six
 
Close PYCQA/pylint#1640
Loading
Loading
Loading
Loading
@@ -516,6 +516,48 @@ def _infer_object__new__decorator_check(node):
return False
 
 
def infer_issubclass(callnode, context=None):
"""Infer issubclass() calls
:param nodes.Call callnode: a `issubclass` call
:param InferenceContext: the context for the inference
:rtype nodes.Const: Boolean Const value of the `issubclass` call
:raises UseInferenceDefault: If the node cannot be inferred
"""
call = arguments.CallSite.from_call(callnode)
if call.keyword_arguments:
# issubclass doesn't support keyword arguments
raise UseInferenceDefault("TypeError: issubclass() takes no keyword arguments")
if len(call.positional_arguments) != 2:
raise UseInferenceDefault(
"Expected two arguments, got {count}"
.format(count=len(call.positional_arguments)))
# The left hand argument is the obj to be checked
obj_node, class_or_tuple_node = call.positional_arguments
try:
obj_type = next(obj_node.infer(context=context))
except InferenceError as exc:
raise UseInferenceDefault from exc
if not isinstance(obj_type, nodes.ClassDef):
raise UseInferenceDefault("TypeError: arg 1 must be class")
# The right hand argument is the class(es) that the given
# object is to be checked against.
try:
class_container = _class_or_tuple_to_container(
class_or_tuple_node, context=context)
except InferenceError as exc:
raise UseInferenceDefault from exc
try:
issubclass_bool = helpers.object_issubclass(obj_type, class_container, context)
except AstroidTypeError as exc:
raise UseInferenceDefault("TypeError: " + str(exc)) from exc
except MroError as exc:
raise UseInferenceDefault from exc
return nodes.Const(issubclass_bool)
def infer_isinstance(callnode, context=None):
"""Infer isinstance calls
 
Loading
Loading
@@ -590,6 +632,7 @@ register_builtin_transform(infer_frozenset, 'frozenset')
register_builtin_transform(infer_type, 'type')
register_builtin_transform(infer_slice, 'slice')
register_builtin_transform(infer_isinstance, 'isinstance')
register_builtin_transform(infer_issubclass, 'issubclass')
 
# Infer object.__new__ calls
MANAGER.register_transform(
Loading
Loading
Loading
Loading
@@ -116,6 +116,41 @@ def object_isinstance(node, class_or_seq, context=None):
return False
 
 
def object_issubclass(node, class_or_seq, context=None):
"""Check if a type is a subclass of any node in class_or_seq
:param node: A given node
:param class_or_seq: Union[Nodes.NodeNG], Sequence[nodes.NodeNG]]
:rtype: bool
:raises AstroidTypeError: if the given ``classes_or_seq`` are not types
:raises AstroidError: if the type of the given node cannot be inferred
or its type's mro doesn't work
"""
if not isinstance(class_or_seq, (tuple, list)):
class_seq = (class_or_seq,)
else:
class_seq = class_or_seq
if not isinstance(node, nodes.ClassDef):
raise TypeError("{node} needs to be a ClassDef node".format(node=node))
# Instances are not types
class_seq = [item if not isinstance(item, bases.Instance)
else util.Uninferable for item in class_seq]
# strict compatibility with issubclass
# issubclass(int, (int, 1)) evaluates to true
# issubclass(int, (1, int)) raises TypeError
for klass in class_seq:
if klass is util.Uninferable:
raise exceptions.AstroidTypeError(
"issubclass() arg 2 must be a type or tuple of types")
for obj_subclass in node.mro():
if obj_subclass == klass:
return True
return False
def safe_infer(node, context=None):
"""Return the inferred value for the given node.
 
Loading
Loading
Loading
Loading
@@ -1002,6 +1002,87 @@ class TestIsinstanceInference:
_get_result_node('isinstance(something, int)')
 
 
class TestIssubclassBrain:
"""Test issubclass() builtin inference"""
def test_type_type(self):
assert _get_result("issubclass(type, type)") == "True"
def test_object_type(self):
assert _get_result("issubclass(object, type)") == "False"
def test_type_object(self):
assert _get_result("issubclass(type, object)") == "True"
def test_issubclass_same_class(self):
assert _get_result("issubclass(int, int)") == "True"
def test_issubclass_not_the_same_class(self):
assert _get_result("issubclass(str, int)") == "False"
def test_issubclass_object_true(self):
assert _get_result("""
class Bar(object):
pass
issubclass(Bar, object)
""") == "True"
def test_issubclass_same_user_defined_class(self):
assert _get_result("""
class Bar(object):
pass
issubclass(Bar, Bar)
""") == "True"
def test_issubclass_different_user_defined_classes(self):
assert _get_result("""
class Foo(object):
pass
class Bar(object):
pass
issubclass(Bar, Foo)
""") == "False"
def test_issubclass_type_false(self):
assert _get_result("""
class Bar(object):
pass
issubclass(Bar, type)
""") == "False"
def test_isinstance_tuple_argument(self):
"""obj just has to be a subclass of ANY class/type on the right"""
assert _get_result("issubclass(int, (str, int))") == "True"
def test_isinstance_object_true2(self):
assert _get_result("""
class Bar(type):
pass
issubclass(Bar, object)
""") == "True"
def test_issubclass_short_circuit(self):
"""issubclasss allows bad type short-circuting"""
assert _get_result("issubclass(int, (int, 1))") == "True"
def test_uninferable_bad_type(self):
"""The second argument must be a class or a tuple of classes"""
# Should I subclass
with pytest.raises(astroid.InferenceError):
_get_result_node("issubclass(int, 1)")
def test_uninferable_keywords(self):
"""issubclass does not allow keywords"""
with pytest.raises(astroid.InferenceError):
_get_result_node("issubclass(int, class_or_tuple=int)")
def test_too_many_args(self):
"""issubclass must have two arguments"""
with pytest.raises(astroid.InferenceError):
_get_result_node("issubclass(int, int, str)")
def _get_result_node(code):
node = next(astroid.extract_node(code).infer())
return node
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment