Skip to content
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

Merged
merged 12 commits into from
Mar 17, 2025

Conversation

ntBre
Copy link
Contributor

@ntBre ntBre commented Mar 5, 2025

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:

with (x as y): ...

The issue is parentheses around a with item with an optional_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 like

>>> with (
...     x,
...     y
... ) as foo:
...     pass
... 

even 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

Unverified

The email in this signature doesn’t match the committer email.
Summary
--

WIP currently I just added all of the valid cases from Alex's comment here
#16106 (comment)

Test Plan
--
Inline tests
@ntBre ntBre added parser Related to the parser preview Related to preview mode features labels Mar 5, 2025
Copy link
Contributor

github-actions bot commented Mar 5, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
@ntBre ntBre marked this pull request as ready for review March 5, 2025 18:45

Unverified

The email in this signature doesn’t match the committer email.
ntBre added 3 commits March 6, 2025 09:50

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
@ntBre
Copy link
Contributor Author

ntBre commented Mar 6, 2025

Thanks for the reviews! I

  • relocated the check to parse_with_items
  • added variant docs
  • expanded the test_ok cases with Micha's suggestion and some of the other cases I put in the docs

MichaReiser added a commit that referenced this pull request Mar 7, 2025
…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
Copy link
Member

@MichaReiser MichaReiser left a 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)
Copy link
Member

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()) {
Copy link
Member

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:

  1. 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.
  2. 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:

  1. 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
  2. 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.

Copy link
Member

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.

Copy link
Contributor Author

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__.

Copy link
Member

@dhruvmanila dhruvmanila left a 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()) {
Copy link
Member

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.

ntBre added 2 commits March 14, 2025 15:42
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__`.
@ntBre
Copy link
Contributor Author

ntBre commented Mar 14, 2025

I implemented option (1) as discussed above (always flagging cases that we parse as parenthesized context managers).

The only remaining ok case is with (foo, bar, baz) as tup: .... This could be a good candidate for a lint rule because we parse this accurately as a parenthesized expression within the with statement, making it actually accessible to ruff or red-knot as a tuple without having to modify the parser. This matches pyright too, which raises a type error about a tuple not implementing __enter__ or __exit__.

@ntBre ntBre merged commit 75a562d into main Mar 17, 2025
21 checks passed
@ntBre ntBre deleted the brent/syn-parenthesized-context-managers branch March 17, 2025 12:54
dcreager added a commit that referenced this pull request Mar 17, 2025
* 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)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
parser Related to the parser preview Related to preview mode features
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants