From 0c7dc2248c66da8d8884985ec5d96f2d243b1849 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Thu, 26 Nov 2015 17:18:16 +0200 Subject: Added a new warning, 'unsupported-delete-operation' It is emitted when item deletion is tried on an object which doesn't have this ability. Closes issue #592. --- ChangeLog | 4 + pylint/checkers/typecheck.py | 51 +++++++----- pylint/checkers/utils.py | 9 +++ .../functional/unsupported_delete_operation.py | 94 ++++++++++++++++++++++ .../functional/unsupported_delete_operation.txt | 17 ++++ 5 files changed, 154 insertions(+), 21 deletions(-) create mode 100644 pylint/test/functional/unsupported_delete_operation.py create mode 100644 pylint/test/functional/unsupported_delete_operation.txt diff --git a/ChangeLog b/ChangeLog index f36f175..e53e79c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,10 @@ ChangeLog for Pylint emitted when item assignment is tried on an object which doesn't have this ability. Closes issue #591. + * Added a new warning, 'unsupported-delete-operation', which is + emitted when item deletion is tried on an object which doesn't + have this ability. Closes issue #592. + * 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 75cb193..7ec0b55 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -40,6 +40,7 @@ from pylint.checkers.utils import ( is_comprehension, is_inside_abstract_class, supports_getitem, supports_setitem, + supports_delitem, safe_infer, has_known_bases) from pylint import utils @@ -50,6 +51,7 @@ _ZOPE_DEPRECATED = ( ) BUILTINS = six.moves.builtins.__name__ STR_FORMAT = "%s.str.format" % BUILTINS +STORE_CONTEXT, LOAD_CONTEXT, DELETE_CONTEXT = range(3) def _unflatten(iterable): @@ -166,6 +168,10 @@ MSGS = { 'unsupported-assignment-operation', "Emitted when an object does not support item assignment " "(i.e. doesn't define __setitem__ method)"), + 'E1138': ("%r does not support item deletion", + 'unsupported-delete-operation', + "Emitted when an object does not support item deletion " + "(i.e. doesn't define __delitem__ method)"), } # builtin sequence types in Python 2 and 3. @@ -831,25 +837,31 @@ accessed. Python regular expressions are accepted.'} self._check_membership_test(right) @staticmethod - def _is_subscript_store_context(node): + def _subscript_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 + return STORE_CONTEXT + elif isinstance(statement, astroid.Delete): + return DELETE_CONTEXT + return LOAD_CONTEXT - @check_messages('unsubscriptable-object') + @check_messages('unsubscriptable-object', 'unsupported-assignment-operation', + 'unsupported-delete-operation') def visit_subscript(self, node): if isinstance(node.value, (astroid.ListComp, astroid.DictComp)): return - store_context = self._is_subscript_store_context(node) + context = self._subscript_context(node) + if context == LOAD_CONTEXT: + msg = 'unsubscriptable-object' + elif context == STORE_CONTEXT: + msg = 'unsupported-assignment-operation' + elif context == DELETE_CONTEXT: + msg = 'unsupported-delete-operation' + if isinstance(node.value, astroid.SetComp): - if store_context: - msg = 'unsubscriptable-object' - else: - msg = 'unsupported-assignment-operation' self.add_message(msg, args=node.value.as_string(), node=node.value) return @@ -858,18 +870,15 @@ accessed. Python regular expressions are accepted.'} 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) - + supported_protocol = None + if context == STORE_CONTEXT: + supported_protocol = supports_setitem + elif context == LOAD_CONTEXT: + supported_protocol = supports_getitem + elif context == DELETE_CONTEXT: + supported_protocol = supports_delitem + if not supported_protocol(inferred): + self.add_message(msg, args=node.value.as_string(), node=node.value) class IterableChecker(BaseChecker): diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index ba396b0..0d6a488 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -44,6 +44,7 @@ ITER_METHOD = '__iter__' NEXT_METHOD = 'next' if six.PY2 else '__next__' GETITEM_METHOD = '__getitem__' SETITEM_METHOD = '__setitem__' +DELITEM_METHOD = '__delitem__' CONTAINS_METHOD = '__contains__' KEYS_METHOD = 'keys' @@ -627,6 +628,10 @@ def _supports_setitem_protocol(value): return _hasattr(value, SETITEM_METHOD) +def _supports_delitem_protocol(value): + return _hasattr(value, DELITEM_METHOD) + + def _is_abstract_class_name(name): lname = name.lower() is_mixin = lname.endswith('mixin') @@ -685,6 +690,10 @@ def supports_setitem(value): return _supports_protocol(value, _supports_setitem_protocol) +def supports_delitem(value): + return _supports_protocol(value, _supports_delitem_protocol) + + # TODO(cpopa): deprecate these or leave them as aliases? def safe_infer(node, context=None): """Return the inferred value for the given node. diff --git a/pylint/test/functional/unsupported_delete_operation.py b/pylint/test/functional/unsupported_delete_operation.py new file mode 100644 index 0000000..297cc15 --- /dev/null +++ b/pylint/test/functional/unsupported_delete_operation.py @@ -0,0 +1,94 @@ +""" +Checks that value used in a subscript support deletion +(i.e. defines __delitem__ 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] +del numbers[0] + + +del bytearray(b"123")[0] +del dict(a=1, b=2)['a'] +del (1, 2, 3)[0] # [unsupported-delete-operation] + +# list/dict comprehensions are fine +del [x for x in range(10)][0] +del {x: 10 - x for x in range(10)}[0] + + +# instances +class NonSubscriptable(object): + pass + +class Subscriptable(object): + def __delitem__(self, key): + pass + +del NonSubscriptable()[0] # [unsupported-delete-operation] +del NonSubscriptable[0] # [unsupported-delete-operation] +del Subscriptable()[0] +del Subscriptable[0] # [unsupported-delete-operation] + +# generators are not subscriptable +def powers_of_two(): + k = 0 + while k < 10: + yield 2 ** k + k += 1 + +del powers_of_two()[0] # [unsupported-delete-operation] +del powers_of_two[0] # [unsupported-delete-operation] + + +# check that primitive non subscriptable types are catched +del True[0] # [unsupported-delete-operation] +del None[0] # [unsupported-delete-operation] +del 8.5[0] # [unsupported-delete-operation] +del 10[0] # [unsupported-delete-operation] + +# sets are not subscriptable +del {x ** 2 for x in range(10)}[0] # [unsupported-delete-operation] +del set(numbers)[0] # [unsupported-delete-operation] +del frozenset(numbers)[0] # [unsupported-delete-operation] + +# skip instances with unknown base classes +from some_missing_module import LibSubscriptable + +class MaybeSubscriptable(LibSubscriptable): + pass + +del MaybeSubscriptable()[0] + +# subscriptable classes (through metaclasses) + +class MetaSubscriptable(type): + def __delitem__(cls, key): + pass + +class SubscriptableClass(six.with_metaclass(MetaSubscriptable, object)): + pass + +del SubscriptableClass[0] +del SubscriptableClass()[0] # [unsupported-delete-operation] + +# functions are not subscriptable +def test(*args, **kwargs): + return args, kwargs + +del test()[0] # [unsupported-delete-operation] +del test[0] # [unsupported-delete-operation] + +# deque +from collections import deque +deq = deque(maxlen=10) +deq.append(42) +del deq[0] + +# tuples assignment +values = [1, 2, 3, 4] +del (values[0], values[1]) +del (values[0], SubscriptableClass()[0]) # [unsupported-delete-operation] diff --git a/pylint/test/functional/unsupported_delete_operation.txt b/pylint/test/functional/unsupported_delete_operation.txt new file mode 100644 index 0000000..2eead9b --- /dev/null +++ b/pylint/test/functional/unsupported_delete_operation.txt @@ -0,0 +1,17 @@ +unsupported-delete-operation:16::'(1, 2, 3)' does not support item deletion +unsupported-delete-operation:31::'NonSubscriptable()' does not support item deletion +unsupported-delete-operation:32::'NonSubscriptable' does not support item deletion +unsupported-delete-operation:34::'Subscriptable' does not support item deletion +unsupported-delete-operation:43::'powers_of_two()' does not support item deletion +unsupported-delete-operation:44::'powers_of_two' does not support item deletion +unsupported-delete-operation:48::'True' does not support item deletion +unsupported-delete-operation:49::'None' does not support item deletion +unsupported-delete-operation:50::'8.5' does not support item deletion +unsupported-delete-operation:51::'10' does not support item deletion +unsupported-delete-operation:54::'{(x) ** (2) for x in range(10)}' does not support item deletion +unsupported-delete-operation:55::'set(numbers)' does not support item deletion +unsupported-delete-operation:56::'frozenset(numbers)' does not support item deletion +unsupported-delete-operation:76::'SubscriptableClass()' does not support item deletion +unsupported-delete-operation:82::'test()' does not support item deletion +unsupported-delete-operation:83::'test' does not support item deletion +unsupported-delete-operation:94::'SubscriptableClass()' does not support item deletion -- cgit v1.2.1