Skip to content

gh-103492: Clarify SyntaxWarning with literal comparison #103493

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Lib/test/test_codeop.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ def test_filename(self):
def test_warning(self):
# Test that the warning is only returned once.
with warnings_helper.check_warnings(
('"is" with a literal', SyntaxWarning),
('"is" with str literal', SyntaxWarning),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be clearer to have "is" with literal of type 'str'?

Note that types are often in `:

>>> 1+'d'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But consistently with only double quotes, or only single quotes?

Copy link
Contributor Author

@hauntsaninja hauntsaninja Apr 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I've added quotes to the types. I kept the phrase 'str' literal instead of literal of type 'str', since I found it a little easier to read and is similar to the way we just mention the types in the add example you give (instead of e.g. saying value of type 'int' and value of type 'str')

I'm happy with whatever quote type, let me know!

("invalid escape sequence", SyntaxWarning),
) as w:
compile_command(r"'\e' is 0")
Expand Down
23 changes: 16 additions & 7 deletions Lib/test/test_grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ def check(test, error=False):
check(f"{num}spam", error=True)

with warnings.catch_warnings():
warnings.filterwarnings('ignore', '"is" with a literal',
warnings.filterwarnings('ignore', '"is" with int literal',
SyntaxWarning)
with self.assertWarnsRegex(SyntaxWarning,
r'invalid \w+ literal'):
Expand Down Expand Up @@ -1467,21 +1467,30 @@ def test_comparison(self):
if 1 < 1 > 1 == 1 >= 1 <= 1 != 1 in 1 not in x is x is not x: pass

def test_comparison_is_literal(self):
def check(test, msg='"is" with a literal'):
def check(test, msg):
self.check_syntax_warning(test, msg)

check('x is 1')
check('x is "thing"')
check('1 is x')
check('x is y is 1')
check('x is not 1', '"is not" with a literal')
check('x is 1', '"is" with int literal')
check('x is "thing"', '"is" with str literal')
check('1 is x', '"is" with int literal')
check('x is y is 1', '"is" with int literal')
check('x is not 1', '"is not" with int literal')
check('x is not (1, 2)', '"is not" with tuple literal')
check('(1, 2) is not x', '"is not" with tuple literal')

check('None is 1', '"is" with int literal')
check('1 is None', '"is" with int literal')

with warnings.catch_warnings():
warnings.simplefilter('error', SyntaxWarning)
compile('x is None', '<testcase>', 'exec')
compile('x is False', '<testcase>', 'exec')
compile('x is True', '<testcase>', 'exec')
compile('x is ...', '<testcase>', 'exec')
compile('None is x', '<testcase>', 'exec')
compile('False is x', '<testcase>', 'exec')
compile('True is x', '<testcase>', 'exec')
compile('... is x', '<testcase>', 'exec')

def test_warn_missed_comma(self):
def check(test):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Clarify :exc:`SyntaxWarning` with literal ``is`` comparison by specifying which literal is problematic, since comparisons using ``is`` with e.g. None and bool literals are idiomatic.
12 changes: 8 additions & 4 deletions Python/compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -2280,13 +2280,17 @@ check_compare(struct compiler *c, expr_ty e)
n = asdl_seq_LEN(e->v.Compare.ops);
for (i = 0; i < n; i++) {
cmpop_ty op = (cmpop_ty)asdl_seq_GET(e->v.Compare.ops, i);
bool right = check_is_arg((expr_ty)asdl_seq_GET(e->v.Compare.comparators, i));
expr_ty right_expr = (expr_ty)asdl_seq_GET(e->v.Compare.comparators, i);
bool right = check_is_arg(right_expr);
if (op == Is || op == IsNot) {
if (!right || !left) {
const char *msg = (op == Is)
? "\"is\" with a literal. Did you mean \"==\"?"
: "\"is not\" with a literal. Did you mean \"!=\"?";
return compiler_warn(c, LOC(e), msg);
? "\"is\" with %.200s literal. Did you mean \"==\"?"
: "\"is not\" with %.200s literal. Did you mean \"!=\"?";
expr_ty literal = !left ? e->v.Compare.left : right_expr;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left gets updated in each iteration, but e->v.Compare.left is always the same thing. Is this correct?

Should we just check the first item before the loop and then here just look at right_expr?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing something, but bool left = check_is_arg(e->v.Compare.left); happens once outside the loop and it's only right that is updated in each iteration.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line 2296?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, of course! Thank you so much, this is buggy. I added the fix and a test case to catch the issue.

I think we can't do the first item check before the loop since all comparators may not be is, e.g. x == 3 is y, but let me know if I'm missing something!

return compiler_warn(
c, LOC(e), msg, Py_TYPE(literal->v.Constant.value)->tp_name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
c, LOC(e), msg, Py_TYPE(literal->v.Constant.value)->tp_name
c, LOC(e), msg, infer_type(literal)->tp_name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had this originally, but it needed a forward reference / we know it's a Constant_kind so we could skip the switch :-)

I'll add it back!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed, let me know if I should move the function instead

);
}
}
left = right;
Expand Down