From e3aa096710619f7b4647b9656649f90b29fe6eba Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Thu, 26 Nov 2015 15:35:03 +0200 Subject: Added a new warning, 'unsupported-assignment-operation' This is emitted when item assignment is tried on an object which doesn't have this ability. Closes issue #591. --- ChangeLog | 4 + pylint/checkers/typecheck.py | 41 ++++++++-- pylint/checkers/utils.py | 15 +++- .../functional/unsupported_assignment_operation.py | 94 ++++++++++++++++++++++ .../unsupported_assignment_operation.txt | 17 ++++ 5 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 pylint/test/functional/unsupported_assignment_operation.py create mode 100644 pylint/test/functional/unsupported_assignment_operation.txt diff --git a/ChangeLog b/ChangeLog index 2436dcc..f36f175 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,6 +2,10 @@ ChangeLog for Pylint -------------------- -- + * Added a new warning, 'unsupported-assignment-operation', which is + emitted when item assignment is tried on an object which doesn't + have this ability. Closes issue #591. + * Added multiple warnings related to imports. 'wrong-import-order' is emitted when PEP 8 recommendations regarding imports are not respected (that is, standard imports should be followed by third-party diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 43b0bde..75cb193 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -38,7 +38,8 @@ from pylint.checkers.utils import ( decorated_with, node_ignores_exception, is_iterable, is_mapping, supports_membership_test, is_comprehension, is_inside_abstract_class, - supports_subscript, + supports_getitem, + supports_setitem, safe_infer, has_known_bases) from pylint import utils @@ -161,6 +162,10 @@ MSGS = { 'unsubscriptable-object', "Emitted when a subscripted value doesn't support subscription" "(i.e. doesn't define __getitem__ method)"), + 'E1137': ("%r does not support item assignment", + 'unsupported-assignment-operation', + "Emitted when an object does not support item assignment " + "(i.e. doesn't define __setitem__ method)"), } # builtin sequence types in Python 2 and 3. @@ -825,18 +830,42 @@ accessed. Python regular expressions are accepted.'} if operator in ['in', 'not in']: self._check_membership_test(right) + @staticmethod + def _is_subscript_store_context(node): + statement = node.statement() + if isinstance(statement, astroid.Assign): + for target in statement.targets: + if target is node or target.parent_of(node): + return False + return True + @check_messages('unsubscriptable-object') def visit_subscript(self, node): if isinstance(node.value, (astroid.ListComp, astroid.DictComp)): return + + store_context = self._is_subscript_store_context(node) if isinstance(node.value, astroid.SetComp): - self.add_message('unsubscriptable-object', - args=node.value.as_string(), + if store_context: + msg = 'unsubscriptable-object' + else: + msg = 'unsupported-assignment-operation' + self.add_message(msg, args=node.value.as_string(), node=node.value) - infered = safe_infer(node.value) - if infered is None or infered is astroid.YES: return - if not supports_subscript(infered): + + inferred = safe_infer(node.value) + if inferred is None or inferred is astroid.YES: + return + + if not store_context: + if not supports_setitem(inferred): + self.add_message('unsupported-assignment-operation', + args=node.value.as_string(), + node=node.value) + return + + if not supports_getitem(inferred): self.add_message('unsubscriptable-object', args=node.value.as_string(), node=node.value) diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index e24a034..ba396b0 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -43,6 +43,7 @@ ABC_METHODS = set(('abc.abstractproperty', 'abc.abstractmethod', ITER_METHOD = '__iter__' NEXT_METHOD = 'next' if six.PY2 else '__next__' GETITEM_METHOD = '__getitem__' +SETITEM_METHOD = '__setitem__' CONTAINS_METHOD = '__contains__' KEYS_METHOD = 'keys' @@ -618,10 +619,14 @@ def _supports_iteration_protocol(value): return _hasattr(value, ITER_METHOD) or _hasattr(value, GETITEM_METHOD) -def _supports_subscript_protocol(value): +def _supports_getitem_protocol(value): return _hasattr(value, GETITEM_METHOD) +def _supports_setitem_protocol(value): + return _hasattr(value, SETITEM_METHOD) + + def _is_abstract_class_name(name): lname = name.lower() is_mixin = lname.endswith('mixin') @@ -672,8 +677,12 @@ def supports_membership_test(value): return supported or is_iterable(value) -def supports_subscript(value): - return _supports_protocol(value, _supports_subscript_protocol) +def supports_getitem(value): + return _supports_protocol(value, _supports_getitem_protocol) + + +def supports_setitem(value): + return _supports_protocol(value, _supports_setitem_protocol) # TODO(cpopa): deprecate these or leave them as aliases? diff --git a/pylint/test/functional/unsupported_assignment_operation.py b/pylint/test/functional/unsupported_assignment_operation.py new file mode 100644 index 0000000..a2e5f87 --- /dev/null +++ b/pylint/test/functional/unsupported_assignment_operation.py @@ -0,0 +1,94 @@ +""" +Checks that value used in a subscript support assignments +(i.e. defines __setitem__ method). +""" +# pylint: disable=missing-docstring,pointless-statement,expression-not-assigned,wrong-import-position +# pylint: disable=too-few-public-methods,import-error,invalid-name,wrong-import-order +import six + +# primitives +numbers = [1, 2, 3] +numbers[0] = 42 + + +bytearray(b"123")[0] = 42 +dict(a=1, b=2)['a'] = 42 +(1, 2, 3)[0] = 42 # [unsupported-assignment-operation] + +# list/dict comprehensions are fine +[x for x in range(10)][0] = 42 +{x: 10 - x for x in range(10)}[0] = 42 + + +# instances +class NonSubscriptable(object): + pass + +class Subscriptable(object): + def __setitem__(self, key, value): + return key + value + +NonSubscriptable()[0] = 24 # [unsupported-assignment-operation] +NonSubscriptable[0] = 24 # [unsupported-assignment-operation] +Subscriptable()[0] = 24 +Subscriptable[0] = 24 # [unsupported-assignment-operation] + +# generators are not subscriptable +def powers_of_two(): + k = 0 + while k < 10: + yield 2 ** k + k += 1 + +powers_of_two()[0] = 42 # [unsupported-assignment-operation] +powers_of_two[0] = 42 # [unsupported-assignment-operation] + + +# check that primitive non subscriptable types are catched +True[0] = 24 # [unsupported-assignment-operation] +None[0] = 42 # [unsupported-assignment-operation] +8.5[0] = 24 # [unsupported-assignment-operation] +10[0] = 24 # [unsupported-assignment-operation] + +# sets are not subscriptable +{x ** 2 for x in range(10)}[0] = 24 # [unsupported-assignment-operation] +set(numbers)[0] = 24 # [unsupported-assignment-operation] +frozenset(numbers)[0] = 42 # [unsupported-assignment-operation] + +# skip instances with unknown base classes +from some_missing_module import LibSubscriptable + +class MaybeSubscriptable(LibSubscriptable): + pass + +MaybeSubscriptable()[0] = 42 + +# subscriptable classes (through metaclasses) + +class MetaSubscriptable(type): + def __setitem__(cls, key, value): + return key + value + +class SubscriptableClass(six.with_metaclass(MetaSubscriptable, object)): + pass + +SubscriptableClass[0] = 24 +SubscriptableClass()[0] = 24 # [unsupported-assignment-operation] + +# functions are not subscriptable +def test(*args, **kwargs): + return args, kwargs + +test()[0] = 24 # [unsupported-assignment-operation] +test[0] = 24 # [unsupported-assignment-operation] + +# deque +from collections import deque +deq = deque(maxlen=10) +deq.append(42) +deq[0] = 42 + +# tuples assignment +values = [1, 2, 3, 4] +(values[0], values[1]) = 3, 4 +(values[0], SubscriptableClass()[0]) = 42, 42 # [unsupported-assignment-operation] diff --git a/pylint/test/functional/unsupported_assignment_operation.txt b/pylint/test/functional/unsupported_assignment_operation.txt new file mode 100644 index 0000000..9b8f547 --- /dev/null +++ b/pylint/test/functional/unsupported_assignment_operation.txt @@ -0,0 +1,17 @@ +unsupported-assignment-operation:16::'(1, 2, 3)' does not support item assignment +unsupported-assignment-operation:31::'NonSubscriptable()' does not support item assignment +unsupported-assignment-operation:32::'NonSubscriptable' does not support item assignment +unsupported-assignment-operation:34::'Subscriptable' does not support item assignment +unsupported-assignment-operation:43::'powers_of_two()' does not support item assignment +unsupported-assignment-operation:44::'powers_of_two' does not support item assignment +unsupported-assignment-operation:48::'True' does not support item assignment +unsupported-assignment-operation:49::'None' does not support item assignment +unsupported-assignment-operation:50::'8.5' does not support item assignment +unsupported-assignment-operation:51::'10' does not support item assignment +unsupported-assignment-operation:54::'{(x) ** (2) for x in range(10)}' does not support item assignment +unsupported-assignment-operation:55::'set(numbers)' does not support item assignment +unsupported-assignment-operation:56::'frozenset(numbers)' does not support item assignment +unsupported-assignment-operation:76::'SubscriptableClass()' does not support item assignment +unsupported-assignment-operation:82::'test()' does not support item assignment +unsupported-assignment-operation:83::'test' does not support item assignment +unsupported-assignment-operation:94::'SubscriptableClass()' does not support item assignment -- cgit v1.2.1