Skip to content

[syntax-errors] Allow yield in base classes and annotations #17206

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 2 commits into from
Apr 4, 2025

Conversation

ntBre
Copy link
Contributor

@ntBre ntBre commented Apr 4, 2025

Summary

This PR fixes the issue pointed out by @JelleZijlstra in #17101 (comment). Namely, I conflated two very different errors from CPython:

>>> def m[T](x: (yield from 1)): ...
  File "<python-input-310>", line 1
    def m[T](x: (yield from 1)): ...
                 ^^^^^^^^^^^^
SyntaxError: yield expression cannot be used within the definition of a generic
>>> def m(x: (yield from 1)): ...
  File "<python-input-311>", line 1
    def m(x: (yield from 1)): ...
              ^^^^^^^^^^^^
SyntaxError: 'yield from' outside function
>>> def outer():
...     def m(x: (yield from 1)): ...
...
>>>

I thought the second error was the same as the first, but yield (and yield from) is actually valid in this position when inside a function scope. The same is true for base classes, as pointed out in the original comment.

We don't currently raise an error for yield outside of a function, but that should be handled separately.

On the upside, this had the benefit of removing the InvalidExpressionPosition::BaseClass variant, the InvalidExpressionPosition::TypeAnnotation variant, and the allow_named_expr field from the visitor because they were all no longer used.

Test Plan

Updated inline tests.

Summary
--

This PR fixes the issue pointed out by @JelleZijlstra in
#17101 (comment). Namely, I
conflated two very different errors from CPython:

```pycon
>>> def m[T](x: (yield from 1)): ...
  File "<python-input-310>", line 1
    def m[T](x: (yield from 1)): ...
                 ^^^^^^^^^^^^
SyntaxError: yield expression cannot be used within the definition of a generic
>>> def m(x: (yield from 1)): ...
  File "<python-input-311>", line 1
    def m(x: (yield from 1)): ...
              ^^^^^^^^^^^^
SyntaxError: 'yield from' outside function
>>> def outer():
...     def m(x: (yield from 1)): ...
...
>>>
```

I thought the second error was the same as the first, but `yield` (and `yield
from`) is actually valid in this position when inside a function scope. The same
is true for base classes, as pointed out in the original comment.

We don't currently raise an error for `yield` outside of a function, but that
should be handled separately.

On the upside, this had the benefit of removing the
`InvalidExpressionPosition::BaseClass` variant, the
`InvalidExpressionPosition::TypeAnnotation` variant, and the `allow_named_expr`
field from the visitor because they were all no longer used.

Test Plan
--

Updated inline tests.
@ntBre ntBre added the bug Something isn't working label Apr 4, 2025
@@ -1,13 +1,9 @@
def f[T]() -> (y := 3): ...
def g[T](arg: (x := 1)): ...
def h[T](x: (yield 1)): ...
def i(x: (yield 1)): ...
Copy link
Contributor

Choose a reason for hiding this comment

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

These ones should perhaps stay as errors. They are a syntax error in Python 3.14 (and under from __future__ import annotations). They're legal otherwise in older versions, but yielding in an annotation is a very questionable idea and it will be a SyntaxError in the future, so it would make sense for Ruff to error about it.

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, interesting. We can check the version before emitting these errors, but I don't think we have any others that depend on from __future__ import annotations yet. It does seem easiest just to make this an error on any version in that case. Thanks!

And it looks like my error message was very similar to CPython in that case too:

>>> from __future__ import annotations
>>> def i(x: (yield 1)): ...
  File "<python-input-1>", line 1
    def i(x: (yield 1)): ...
              ^^^^^^^
SyntaxError: yield expression cannot be used within an annotation

Copy link
Contributor

Choose a reason for hiding this comment

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

For reference: https://peps.python.org/pep-0649/#changes-to-allowable-annotations-syntax

Up to you how to handle this in Ruff. You could raise a SyntaxError on all versions for simplicity (pro: helps prepare people for the error, simpler to implement; con: not strictly accurate on 3.13 and earlier, might have false positives in the unlikely case people are actually using yield etc. in annotations). You could also add a new error code specifically for this case, so people can ignore it if they want. Or you can just leave it as in this PR for now, and make it a SyntaxError for >3.14 only once you add 3.14 support.

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 is this the same for named expressions too? I had already skipped those in the initial PR because they were allowed in 3.13:

>>> def i(x: (x:=1)): ...
...
>>> from __future__ import annotations
>>> def i(x: (x:=1)): ...
  File "<python-input-2>", line 1
    def i(x: (x:=1)): ...
              ^^^^
SyntaxError: named expression cannot be used within an annotation
>>>

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, and await. (The comment above was written before I saw your reply.)

Copy link
Contributor

github-actions bot commented Apr 4, 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.

@ntBre
Copy link
Contributor Author

ntBre commented Apr 4, 2025

I added a SemanticSyntaxContext::future_annotations_or_stub method for checking future annotations and restored the function annotations check in that case (or on Python 3.14). I also added await handling in these positions to #11934 since I left them out of the initial PR here. For now I just want to fix the existing false positive before the hotfix release.

if let Some(arguments) = arguments {
visitor.position = InvalidExpressionPosition::GenericDefinition;
Copy link
Member

Choose a reason for hiding this comment

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

Do we need the same future check here to determine the position or are functions different?

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 think base classes still allow this, at least with future annotations:

>>> from __future__ import annotations
>>> class F(y := list): ...
... def f[T]():
...     class G((yield 1)): ...
...     class H((yield from 1)): ...
...

Although I'm now realizing that this applies to other annotations not considered in this PR:

>>> def f():
...     x: (yield 1)
...
>>> from __future__ import annotations
>>> def f():
...     x: (yield 1)
...
  File "<python-input-2>", line 2
    x: (yield 1)
        ^^^^^^^
SyntaxError: yield expression cannot be used within an annotation

Another addition to #11934...

@ntBre ntBre merged commit acc5662 into main Apr 4, 2025
22 checks passed
@ntBre ntBre deleted the brent/fix-annotations branch April 4, 2025 17:48
@ntBre ntBre added the preview Related to preview mode features label Apr 4, 2025
dcreager added a commit that referenced this pull request Apr 4, 2025
* main:
  [red-knot] Empty tuple is always-falsy (#17213)
  Run fuzzer with `--preview` (#17210)
  Bump 0.11.4 (#17212)
  [syntax-errors] Allow `yield` in base classes and annotations (#17206)
  Don't skip visiting non-tuple slice in `typing.Annotated` subscripts (#17201)
  [red-knot] mypy_primer: do not specify Python version (#17200)
  [red-knot] Add `Type.definition` method (#17153)
  Implement `Invalid rule provided` as rule RUF102 with `--fix` (#17138)
  [red-knot] Add basic on-hover to playground and LSP (#17057)
  [red-knot] don't remove negations when simplifying constrained typevars (#17189)
  [minor] Fix extra semicolon for clippy (#17188)
  [syntax-errors] Invalid syntax in annotations (#17101)
  [syntax-errors] Duplicate attributes in match class pattern (#17186)
  [syntax-errors] Fix multiple assignment for class keyword argument (#17184)
  use astral-sh/cargo-dist instead (#17187)
  Enable overindented docs lint (#17182)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working preview Related to preview mode features
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants