Index: third_party/pylint/checkers/classes.py |
diff --git a/third_party/pylint/checkers/classes.py b/third_party/pylint/checkers/classes.py |
index 232e130b8b9d11e8e808cd6474a0f663dead3b89..eeaf689f440f29545512eabf2eec721090fc5237 100644 |
--- a/third_party/pylint/checkers/classes.py |
+++ b/third_party/pylint/checkers/classes.py |
@@ -18,16 +18,20 @@ |
from __future__ import generators |
import sys |
+from collections import defaultdict |
import astroid |
from astroid import YES, Instance, are_exclusive, AssAttr, Class |
-from astroid.bases import Generator |
+from astroid.bases import Generator, BUILTINS |
+from astroid.inference import InferenceContext |
from pylint.interfaces import IAstroidChecker |
from pylint.checkers import BaseChecker |
from pylint.checkers.utils import ( |
PYMETHODS, overrides_a_method, check_messages, is_attr_private, |
- is_attr_protected, node_frame_class, safe_infer) |
+ is_attr_protected, node_frame_class, safe_infer, is_builtin_object, |
+ decorated_with_property) |
+import six |
if sys.version_info >= (3, 0): |
NEXT_METHOD = '__next__' |
@@ -35,6 +39,32 @@ else: |
NEXT_METHOD = 'next' |
ITER_METHODS = ('__iter__', '__getitem__') |
+def _called_in_methods(func, klass, methods): |
+ """ Check if the func was called in any of the given methods, |
+ belonging to the *klass*. Returns True if so, False otherwise. |
+ """ |
+ if not isinstance(func, astroid.Function): |
+ return False |
+ for method in methods: |
+ try: |
+ infered = klass.getattr(method) |
+ except astroid.NotFoundError: |
+ continue |
+ for infer_method in infered: |
+ for callfunc in infer_method.nodes_of_class(astroid.CallFunc): |
+ try: |
+ bound = next(callfunc.func.infer()) |
+ except (astroid.InferenceError, StopIteration): |
+ continue |
+ if not isinstance(bound, astroid.BoundMethod): |
+ continue |
+ func_obj = bound._proxied |
+ if isinstance(func_obj, astroid.UnboundMethod): |
+ func_obj = func_obj._proxied |
+ if func_obj.name == func.name: |
+ return True |
+ return False |
+ |
def class_is_abstract(node): |
"""return true if the given class node should be considered as an abstract |
class |
@@ -45,11 +75,39 @@ def class_is_abstract(node): |
return True |
return False |
+def _is_attribute_property(name, klass): |
+ """ Check if the given attribute *name* is a property |
+ in the given *klass*. |
+ |
+ It will look for `property` calls or for functions |
+ with the given name, decorated by `property` or `property` |
+ subclasses. |
+ Returns ``True`` if the name is a property in the given klass, |
+ ``False`` otherwise. |
+ """ |
+ |
+ try: |
+ attributes = klass.getattr(name) |
+ except astroid.NotFoundError: |
+ return False |
+ property_name = "{0}.property".format(BUILTINS) |
+ for attr in attributes: |
+ try: |
+ infered = next(attr.infer()) |
+ except astroid.InferenceError: |
+ continue |
+ if (isinstance(infered, astroid.Function) and |
+ decorated_with_property(infered)): |
+ return True |
+ if infered.pytype() == property_name: |
+ return True |
+ return False |
+ |
MSGS = { |
'F0202': ('Unable to check methods signature (%s / %s)', |
'method-check-failed', |
- 'Used when PyLint has been unable to check methods signature \ |
+ 'Used when Pylint has been unable to check methods signature \ |
compatibility for an unexpected reason. Please report this kind \ |
if you don\'t make sense of it.'), |
@@ -136,7 +194,7 @@ MSGS = { |
), |
'F0220': ('failed to resolve interfaces implemented by %s (%s)', # W0224 |
'unresolved-interface', |
- 'Used when a PyLint as failed to find interfaces implemented by \ |
+ 'Used when a Pylint as failed to find interfaces implemented by \ |
a class'), |
@@ -172,7 +230,11 @@ MSGS = { |
'E0238': ('Invalid __slots__ object', |
'invalid-slots', |
'Used when an invalid __slots__ is found in class. ' |
- 'Only a string, an iterable or a sequence is permitted.') |
+ 'Only a string, an iterable or a sequence is permitted.'), |
+ 'E0239': ('Inheriting %r, which is not a class.', |
+ 'inherit-non-class', |
+ 'Used when a class inherits from something which is not a ' |
+ 'class.'), |
} |
@@ -234,7 +296,16 @@ a class method.'} |
'help' : 'List of valid names for the first argument in \ |
a metaclass class method.'} |
), |
- ) |
+ ('exclude-protected', |
+ { |
+ 'default': ( |
+ # namedtuple public API. |
+ '_asdict', '_fields', '_replace', '_source', '_make'), |
+ 'type': 'csv', |
+ 'metavar': '<protected access exclusions>', |
+ 'help': ('List of member names, which should be excluded ' |
+ 'from the protected access warning.')} |
+ )) |
def __init__(self, linter=None): |
BaseChecker.__init__(self, linter) |
@@ -245,7 +316,7 @@ a metaclass class method.'} |
def visit_class(self, node): |
"""init visit variable _accessed and check interfaces |
""" |
- self._accessed.append({}) |
+ self._accessed.append(defaultdict(list)) |
self._check_bases_classes(node) |
self._check_interfaces(node) |
# if not an interface, exception, metaclass |
@@ -255,8 +326,27 @@ a metaclass class method.'} |
except astroid.NotFoundError: |
self.add_message('no-init', args=node, node=node) |
self._check_slots(node) |
+ self._check_proper_bases(node) |
+ |
+ @check_messages('inherit-non-class') |
+ def _check_proper_bases(self, node): |
+ """ |
+ Detect that a class inherits something which is not |
+ a class or a type. |
+ """ |
+ for base in node.bases: |
+ ancestor = safe_infer(base) |
+ if ancestor in (YES, None): |
+ continue |
+ if (isinstance(ancestor, astroid.Instance) and |
+ ancestor.is_subtype_of('%s.type' % (BUILTINS,))): |
+ continue |
+ if not isinstance(ancestor, astroid.Class): |
+ self.add_message('inherit-non-class', |
+ args=base.as_string(), node=node) |
- @check_messages('access-member-before-definition', 'attribute-defined-outside-init') |
+ @check_messages('access-member-before-definition', |
+ 'attribute-defined-outside-init') |
def leave_class(self, cnode): |
"""close a class node: |
check that instance attributes are defined in __init__ and check |
@@ -271,7 +361,7 @@ a metaclass class method.'} |
return |
defining_methods = self.config.defining_attr_methods |
current_module = cnode.root() |
- for attr, nodes in cnode.instance_attrs.iteritems(): |
+ for attr, nodes in six.iteritems(cnode.instance_attrs): |
# skip nodes which are not in the current module and it may screw up |
# the output, while it's not worth it |
nodes = [n for n in nodes if not |
@@ -301,6 +391,12 @@ a metaclass class method.'} |
except astroid.NotFoundError: |
for node in nodes: |
if node.frame().name not in defining_methods: |
+ # If the attribute was set by a callfunc in any |
+ # of the defining methods, then don't emit |
+ # the warning. |
+ if _called_in_methods(node.frame(), cnode, |
+ defining_methods): |
+ continue |
self.add_message('attribute-defined-outside-init', |
args=attr, node=node) |
@@ -348,7 +444,7 @@ a metaclass class method.'} |
and overridden_frame.type == 'method'): |
overridden_frame = overridden_frame.parent.frame() |
if (isinstance(overridden_frame, Class) |
- and klass._is_subtype_of(overridden_frame.qname())): |
+ and klass.is_subtype_of(overridden_frame.qname())): |
args = (overridden.root().name, overridden.fromlineno) |
self.add_message('method-hidden', args=args, node=node) |
except astroid.NotFoundError: |
@@ -466,7 +562,7 @@ a metaclass class method.'} |
attrname = node.attrname |
# Check self |
if self.is_first_attr(node): |
- self._accessed[-1].setdefault(attrname, []).append(node) |
+ self._accessed[-1][attrname].append(node) |
return |
if not self.linter.is_message_enabled('protected-access'): |
return |
@@ -475,7 +571,7 @@ a metaclass class method.'} |
def visit_assattr(self, node): |
if isinstance(node.ass_type(), astroid.AugAssign) and self.is_first_attr(node): |
- self._accessed[-1].setdefault(node.attrname, []).append(node) |
+ self._accessed[-1][node.attrname].append(node) |
self._check_in_slots(node) |
def _check_in_slots(self, node): |
@@ -500,6 +596,10 @@ a metaclass class method.'} |
# If we have a '__dict__' in slots, then |
# assigning any name is valid. |
if not any(slot.value == '__dict__' for slot in slots): |
+ if _is_attribute_property(node.attrname, klass): |
+ # Properties circumvent the slots mechanism, |
+ # so we should not emit a warning for them. |
+ return |
self.add_message('assigning-non-slot', |
args=(node.attrname, ), node=node) |
@@ -526,7 +626,8 @@ a metaclass class method.'} |
''' |
attrname = node.attrname |
- if is_attr_protected(attrname): |
+ if (is_attr_protected(attrname) and |
+ attrname not in self.config.exclude_protected): |
klass = node_frame_class(node) |
@@ -549,6 +650,23 @@ a metaclass class method.'} |
# We are in a class, one remaining valid cases, Klass._attr inside |
# Klass |
if not (callee == klass.name or callee in klass.basenames): |
+ # Detect property assignments in the body of the class. |
+ # This is acceptable: |
+ # |
+ # class A: |
+ # b = property(lambda: self._b) |
+ |
+ stmt = node.parent.statement() |
+ try: |
+ if (isinstance(stmt, astroid.Assign) and |
+ (stmt in klass.body or klass.parent_of(stmt)) and |
+ isinstance(stmt.value, astroid.CallFunc) and |
+ isinstance(stmt.value.func, astroid.Name) and |
+ stmt.value.func.name == 'property' and |
+ is_builtin_object(next(stmt.value.func.infer(), None))): |
+ return |
+ except astroid.InferenceError: |
+ pass |
self.add_message('protected-access', node=node, args=attrname) |
def visit_name(self, node): |
@@ -562,7 +680,7 @@ a metaclass class method.'} |
def _check_accessed_members(self, node, accessed): |
"""check that accessed members are defined""" |
# XXX refactor, probably much simpler now that E0201 is in type checker |
- for attr, nodes in accessed.iteritems(): |
+ for attr, nodes in six.iteritems(accessed): |
# deactivate "except doesn't do anything", that's expected |
# pylint: disable=W0704 |
try: |
@@ -574,7 +692,7 @@ a metaclass class method.'} |
pass |
# is it an instance attribute of a parent class ? |
try: |
- node.instance_attr_ancestors(attr).next() |
+ next(node.instance_attr_ancestors(attr)) |
# yes, stop here |
continue |
except StopIteration: |
@@ -606,7 +724,8 @@ a metaclass class method.'} |
lno = defstmt.fromlineno |
for _node in nodes: |
if _node.frame() is frame and _node.fromlineno < lno \ |
- and not are_exclusive(_node.statement(), defstmt, ('AttributeError', 'Exception', 'BaseException')): |
+ and not are_exclusive(_node.statement(), defstmt, |
+ ('AttributeError', 'Exception', 'BaseException')): |
self.add_message('access-member-before-definition', |
node=_node, args=(attr, lno)) |
@@ -649,7 +768,8 @@ a metaclass class method.'} |
else: |
self._check_first_arg_config( |
first, |
- self.config.valid_classmethod_first_arg, node, 'bad-mcs-method-argument', |
+ self.config.valid_classmethod_first_arg, node, |
+ 'bad-mcs-method-argument', |
node.name) |
# regular class |
else: |
@@ -657,7 +777,8 @@ a metaclass class method.'} |
if node.type == 'classmethod': |
self._check_first_arg_config( |
first, |
- self.config.valid_classmethod_first_arg, node, 'bad-classmethod-argument', |
+ self.config.valid_classmethod_first_arg, node, |
+ 'bad-classmethod-argument', |
node.name) |
# regular method without self as argument |
elif first != 'self': |
@@ -719,7 +840,8 @@ a metaclass class method.'} |
try: |
method = node_method(node, name) |
except astroid.NotFoundError: |
- self.add_message('missing-interface-method', args=(name, iface.name), |
+ self.add_message('missing-interface-method', |
+ args=(name, iface.name), |
node=node) |
continue |
# ignore inherited methods |
@@ -762,17 +884,29 @@ a metaclass class method.'} |
expr.expr.func.name == 'super': |
return |
try: |
- klass = expr.expr.infer().next() |
+ klass = next(expr.expr.infer()) |
if klass is YES: |
continue |
+ # The infered klass can be super(), which was |
+ # assigned to a variable and the `__init__` was called later. |
+ # |
+ # base = super() |
+ # base.__init__(...) |
+ |
+ if (isinstance(klass, astroid.Instance) and |
+ isinstance(klass._proxied, astroid.Class) and |
+ is_builtin_object(klass._proxied) and |
+ klass._proxied.name == 'super'): |
+ return |
try: |
del not_called_yet[klass] |
except KeyError: |
if klass not in to_call: |
- self.add_message('non-parent-init-called', node=expr, args=klass.name) |
+ self.add_message('non-parent-init-called', |
+ node=expr, args=klass.name) |
except astroid.InferenceError: |
continue |
- for klass, method in not_called_yet.iteritems(): |
+ for klass, method in six.iteritems(not_called_yet): |
if klass.name == 'object' or method.parent.name == 'object': |
continue |
self.add_message('super-init-not-called', args=klass.name, node=node) |
@@ -784,7 +918,8 @@ a metaclass class method.'} |
""" |
if not (isinstance(method1, astroid.Function) |
and isinstance(refmethod, astroid.Function)): |
- self.add_message('method-check-failed', args=(method1, refmethod), node=method1) |
+ self.add_message('method-check-failed', |
+ args=(method1, refmethod), node=method1) |
return |
# don't care about functions with unknown argument (builtins) |
if method1.args.args is None or refmethod.args.args is None: |
@@ -813,7 +948,7 @@ def _ancestors_to_call(klass_node, method='__init__'): |
to_call = {} |
for base_node in klass_node.ancestors(recurs=False): |
try: |
- to_call[base_node] = base_node.igetattr(method).next() |
+ to_call[base_node] = next(base_node.igetattr(method)) |
except astroid.InferenceError: |
continue |
return to_call |