From 0af480e3351ae40b4ae7f3ce7272a46fd4265dbd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 17 Feb 2020 11:43:54 -0800 Subject: Warn about is comparison to tuple (#484) --- pyflakes/checker.py | 57 +++++++++++++++++++++++++++++++++++----- pyflakes/messages.py | 2 +- pyflakes/test/test_is_literal.py | 22 ++++++++++++++++ 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/pyflakes/checker.py b/pyflakes/checker.py index c89d6fb..351ab12 100644 --- a/pyflakes/checker.py +++ b/pyflakes/checker.py @@ -78,6 +78,51 @@ else: LOOP_TYPES = (ast.While, ast.For) FUNCTION_TYPES = (ast.FunctionDef,) + +if PY38_PLUS: + def _is_singleton(node): # type: (ast.AST) -> bool + return ( + isinstance(node, ast.Constant) and + isinstance(node.value, (bool, type(Ellipsis), type(None))) + ) +elif not PY2: + def _is_singleton(node): # type: (ast.AST) -> bool + return isinstance(node, (ast.NameConstant, ast.Ellipsis)) +else: + def _is_singleton(node): # type: (ast.AST) -> bool + return ( + isinstance(node, ast.Name) and + node.id in {'True', 'False', 'Ellipsis', 'None'} + ) + + +def _is_tuple_constant(node): # type: (ast.AST) -> bool + return ( + isinstance(node, ast.Tuple) and + all(_is_constant(elt) for elt in node.elts) + ) + + +if PY38_PLUS: + def _is_constant(node): + return isinstance(node, ast.Constant) or _is_tuple_constant(node) +else: + _const_tps = (ast.Str, ast.Num) + if not PY2: + _const_tps += (ast.Bytes,) + + def _is_constant(node): + return ( + isinstance(node, _const_tps) or + _is_singleton(node) or + _is_tuple_constant(node) + ) + + +def _is_const_non_singleton(node): # type: (ast.AST) -> bool + return _is_constant(node) and not _is_singleton(node) + + # https://github.com/python/typed_ast/blob/1.4.0/ast27/Parser/tokenizer.c#L102-L104 TYPE_COMMENT_RE = re.compile(r'^#\s*type:\s*') # https://github.com/python/typed_ast/blob/1.4.0/ast27/Parser/tokenizer.c#L1408-L1413 @@ -2124,14 +2169,14 @@ class Checker(object): self.handleNode(node.value, node) def COMPARE(self, node): - literals = (ast.Str, ast.Num) - if not PY2: - literals += (ast.Bytes,) - left = node.left for op, right in zip(node.ops, node.comparators): - if (isinstance(op, (ast.Is, ast.IsNot)) and - (isinstance(left, literals) or isinstance(right, literals))): + if ( + isinstance(op, (ast.Is, ast.IsNot)) and ( + _is_const_non_singleton(left) or + _is_const_non_singleton(right) + ) + ): self.report(messages.IsLiteral, node) left = right diff --git a/pyflakes/messages.py b/pyflakes/messages.py index cf67cf8..e93a493 100644 --- a/pyflakes/messages.py +++ b/pyflakes/messages.py @@ -265,7 +265,7 @@ class InvalidPrintSyntax(Message): class IsLiteral(Message): - message = 'use ==/!= to compare str, bytes, and int literals' + message = 'use ==/!= to compare constant literals (str, bytes, int, float, tuple)' class FStringMissingPlaceholders(Message): diff --git a/pyflakes/test/test_is_literal.py b/pyflakes/test/test_is_literal.py index 9280cda..fbbb205 100644 --- a/pyflakes/test/test_is_literal.py +++ b/pyflakes/test/test_is_literal.py @@ -198,3 +198,25 @@ class Test(TestCase): if 4 < x is 'foo': pass """, IsLiteral) + + def test_is_tuple_constant(self): + self.flakes('''\ + x = 5 + if x is (): + pass + ''', IsLiteral) + + def test_is_tuple_constant_containing_constants(self): + self.flakes('''\ + x = 5 + if x is (1, '2', True, (1.5, ())): + pass + ''', IsLiteral) + + def test_is_tuple_containing_variables_ok(self): + # a bit nonsensical, but does not trigger a SyntaxWarning + self.flakes('''\ + x = 5 + if x is (x,): + pass + ''') -- cgit v1.2.1