summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaudiu Popa <pcmanticore@gmail.com>2015-11-26 17:18:16 +0200
committerClaudiu Popa <pcmanticore@gmail.com>2015-11-26 17:18:16 +0200
commit0c7dc2248c66da8d8884985ec5d96f2d243b1849 (patch)
treec147f8d0cc1690bfd777fd4044d946173bbc07a2
parente3aa096710619f7b4647b9656649f90b29fe6eba (diff)
downloadpylint-0c7dc2248c66da8d8884985ec5d96f2d243b1849.tar.gz
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.
-rw-r--r--ChangeLog4
-rw-r--r--pylint/checkers/typecheck.py51
-rw-r--r--pylint/checkers/utils.py9
-rw-r--r--pylint/test/functional/unsupported_delete_operation.py94
-rw-r--r--pylint/test/functional/unsupported_delete_operation.txt17
5 files changed, 154 insertions, 21 deletions
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