-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
[syntax-errors] Parenthesized context managers before Python 3.9 #16523
Conversation
Summary -- WIP currently I just added all of the valid cases from Alex's comment here #16106 (comment) Test Plan -- Inline tests
|
Thanks for the reviews! I
|
…ax errors (#16549) ## Summary This should give us better coverage for the unsupported syntax error features and increases our confidence that the formatter doesn't accidentially introduce new unsupported syntax errors. A feature like this would have been very useful when working on f-string formatting where it took a lot of iteration to find all Python 3.11 or older incompatibilities. ## Test Plan I applied my changes on top of #16523 and removed the target version check in the with-statement formatting code. As expected, the integration tests now failed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, this is a bit more subtle. I wrote a longer inline comment about the problem and listed a few options
| | ||
1 | # parse_options: {"target-version": "3.8"} | ||
2 | with (foo as x, bar as y): ... | ||
| ^ Syntax Error: Cannot use parentheses within a `with` statement on Python 3.8 (syntax was added in Python 3.9) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BurntSushi's new diagnostic system will allow us to mark both parentheses.
if self.at(TokenKind::Lpar) { | ||
if let Some(items) = self.try_parse_parenthesized_with_items() { | ||
if items.iter().any(|item| item.optional_vars.is_some()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not directly related to your PR, but it is somewhat related that the parser now supports a target_version
option.
If given:
with (CtxManager1(), CtxManager2()):
...
- Pre 3.9: The context manager expression is a tuple which is always a runtime error (I think)
- 3.9 or newer: This parses as a multiple-item context manager.
There are two concerns here:
- Always parsing the above as a context manager with multiple with items affects downstream tools. E.g., a type checker will error when parsed as a tuple because the tuple doesn't implement the context manager protocol. However, the code might be valid when parsed as a with statement with multiple context expressions.
- We'll miss errors that are technically not syntax errors, but they are most certainly not what the user wanted.
I think we have two options here:
- We make the backwards compatibility stricter than it has to be and flag any parenthesized context manager with more than one expression or with a trailing comma as an error because it is parsed as a tuple on Python 3.8 or older
- We leave it as is and consider this a concern of a lint rule to catch tuples in context manager positions.
I'm not sure if we should change the parser to correctly parse this as a tuple instead of a statement with multiple context managers. I think it's fine to leave it as is for 1, but we should definitely change the parser if we go for 2.
I just tested how pyright handles this and pyright always flags:
with (CtxManager1(), CtxManager2()):
...
While this is not a 100% correct, I think it's accurate enough and could be a simplified fix for 1 and 2, but I'm interested in more opinions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, that's a good point.
- Pre 3.9: The context manager expression is a tuple which is always a runtime error (I think)
Yes, it's a runtime error. I quickly tested it out:
Context managers as tuple test code
from contextlib import contextmanager
@contextmanager
def context1():
yield 1
@contextmanager
def context2():
yield 2
with (context1(), context2()):
...
My initial thought was to change the parser to emit the correct AST as per the target version but that is something that we only need to question for this specific change as it seems to be the only change which not only changes the grammar but that in turn changes the parsed AST depending on the target version. But, I'm not sure if that'd actually provide any advantages.
I think my preference would be to go for (1) given that (a) it's already done by Pyright and I don't see any issues that users have brought up, (b) mypy doesn't flag this case on 3.8 either (mypy playground), and (c) Python 3.8 is has reached EOL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh that's a very good point, thanks for catching that.
Option (1) sounds like the easier of the two, but I see some appeal in having it as a lint rule too. I guess it's a bit weird to flag this as a SyntaxError
, but like you said, I don't think there's any way for this not to cause an error at runtime. It also seems like a pain to change the parser for such an old Python version.
pyright is actually a little touchy here, I think. It flags the case with multiple elements as a parenthesized context manager, but for a single-element tuple, you get a type error about the tuple not implementing __enter__
and __exit__
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(A bit embarrassing but I just found out that my comment was still pending, submitting it now)
if self.at(TokenKind::Lpar) { | ||
if let Some(items) = self.try_parse_parenthesized_with_items() { | ||
if items.iter().any(|item| item.optional_vars.is_some()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, that's a good point.
- Pre 3.9: The context manager expression is a tuple which is always a runtime error (I think)
Yes, it's a runtime error. I quickly tested it out:
Context managers as tuple test code
from contextlib import contextmanager
@contextmanager
def context1():
yield 1
@contextmanager
def context2():
yield 2
with (context1(), context2()):
...
My initial thought was to change the parser to emit the correct AST as per the target version but that is something that we only need to question for this specific change as it seems to be the only change which not only changes the grammar but that in turn changes the parsed AST depending on the target version. But, I'm not sure if that'd actually provide any advantages.
I think my preference would be to go for (1) given that (a) it's already done by Pyright and I don't see any issues that users have brought up, (b) mypy doesn't flag this case on 3.8 either (mypy playground), and (c) Python 3.8 is has reached EOL.
The only remaining `ok` case is `with (foo, bar, baz) as tup: ...` and could be a good candidate for a lint rule because we parse this accurately as a parenthesized expression within the `with` statement. This matches pyright too, which raises a type error about a tuple not implementing `__enter__` or `__exit__`.
I implemented option (1) as discussed above (always flagging cases that we parse as parenthesized context managers). The only remaining |
* main: (25 commits) [syntax-errors] Parenthesized context managers before Python 3.9 (#16523) [ci]: Disable wheel testing on `ppc64le` (#16793) [red-knot] Stabilize `negation_reverses_subtype_order` property test (#16801) [red-knot] Emit error if int/float/complex/bytes/boolean literals appear in type expressions outside `typing.Literal[]` (#16765) [ci] Use `git diff` instead of `changed-files` GH action (#16796) [syntax-errors] Improve error message and range for pre-PEP-614 decorator syntax errors (#16581) [`flake8-bandit`] Allow raw strings in `suspicious-mark-safe-usage` (`S308`) #16702 (#16770) [`refurb`] Avoid panicking `unwrap` in `verbose-decimal-constructor` (`FURB157`) (#16777) [red-knot] Add `--color` CLI option (#16758) [internal]: Upgrade salsa (#16794) Pin dependencies (#16791) [internal]: Update indirect dependencies (#16792) [ci]: Fixup codspeed upgrade (#16790) Update Rust crate compact_str to 0.9.0 (#16785) Update Rust crate clap to v4.5.32 (#16778) Update Rust crate codspeed-criterion-compat to v2.9.1 (#16784) Update Rust crate quote to v1.0.40 (#16782) Update Rust crate ordermap to v0.5.6 (#16781) Update cloudflare/wrangler-action action to v3.14.1 (#16783) Update Rust crate env_logger to v0.11.7 (#16779) ...
Summary
I thought this was very complicated based on the comment here: #16106 (comment) and on some of the discussion in the CPython issue here: python/cpython#56991. However, after a little bit of experimentation, I think it boils down to this example:
The issue is parentheses around a
with
item with anoptional_var
, as we (and Python) call the trailing variable name (y
in this case). It's not actually about line breaks after all, except that line breaks are allowed in parenthesized expressions, which explains the validity of cases likeeven on Python 3.8.
I followed pyright's example again here on the diagnostic range (just the opening paren) and the wording of the error.
Test Plan
Inline tests