[syntax-errors] Parenthesized keyword argument names after Python 3.8 (#16482)

Summary
--

Unlike the other syntax errors detected so far, parenthesized keyword
arguments are only allowed *before* 3.8. It sounds like they were only
accidentally allowed before that [^1].

As an aside, you get a pretty confusing error from Python for this, so
it's nice that we can catch it:

```pycon
>>> def f(**kwargs): ...
... f((a)=1)
...
  File "<python-input-0>", line 2
    f((a)=1)
       ^^^
SyntaxError: expression cannot contain assignment, perhaps you meant "=="?
>>>
```
Test Plan
--
Inline tests.

[^1]: https://github.com/python/cpython/issues/78822
This commit is contained in:
Brent Westbrook 2025-03-06 12:18:13 -05:00 committed by GitHub
parent 6c14225c66
commit b3c884f4f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 319 additions and 22 deletions

View file

@ -436,11 +436,6 @@ pub struct UnsupportedSyntaxError {
pub kind: UnsupportedSyntaxErrorKind,
pub range: TextRange,
/// The target [`PythonVersion`] for which this error was detected.
///
/// This is different from the version reported by the
/// [`minimum_version`](UnsupportedSyntaxErrorKind::minimum_version) method, which is the
/// earliest allowed version for this piece of syntax. The `target_version` is primarily used
/// for user-facing error messages.
pub target_version: PythonVersion,
}
@ -457,6 +452,26 @@ pub enum UnsupportedSyntaxErrorKind {
Walrus,
ExceptStar,
/// Represents the use of a parenthesized keyword argument name after Python 3.8.
///
/// ## Example
///
/// From [BPO 34641] it sounds like this was only accidentally supported and was removed when
/// noticed. Code like this used to be valid:
///
/// ```python
/// f((a)=1)
/// ```
///
/// After Python 3.8, you have to omit the parentheses around `a`:
///
/// ```python
/// f(a=1)
/// ```
///
/// [BPO 34641]: https://github.com/python/cpython/issues/78822
ParenthesizedKeywordArgumentName,
/// Represents the use of unparenthesized tuple unpacking in a `return` statement or `yield`
/// expression before Python 3.8.
///
@ -603,6 +618,9 @@ impl Display for UnsupportedSyntaxError {
UnsupportedSyntaxErrorKind::Match => "Cannot use `match` statement",
UnsupportedSyntaxErrorKind::Walrus => "Cannot use named assignment expression (`:=`)",
UnsupportedSyntaxErrorKind::ExceptStar => "Cannot use `except*`",
UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => {
"Cannot use parenthesized keyword argument name"
}
UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Return) => {
"Cannot use iterable unpacking in return statements"
}
@ -619,32 +637,67 @@ impl Display for UnsupportedSyntaxError {
"Cannot set default type for a type parameter"
}
};
write!(
f,
"{kind} on Python {} (syntax was added in Python {})",
"{kind} on Python {} (syntax was {changed})",
self.target_version,
self.kind.minimum_version(),
changed = self.kind.changed_version(),
)
}
}
impl UnsupportedSyntaxErrorKind {
/// The earliest allowed version for the syntax associated with this error.
pub const fn minimum_version(&self) -> PythonVersion {
/// Represents the kind of change in Python syntax between versions.
enum Change {
Added(PythonVersion),
Removed(PythonVersion),
}
impl Display for Change {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UnsupportedSyntaxErrorKind::Match => PythonVersion::PY310,
UnsupportedSyntaxErrorKind::Walrus => PythonVersion::PY38,
UnsupportedSyntaxErrorKind::ExceptStar => PythonVersion::PY311,
UnsupportedSyntaxErrorKind::StarTuple(_) => PythonVersion::PY38,
UnsupportedSyntaxErrorKind::RelaxedDecorator => PythonVersion::PY39,
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => PythonVersion::PY38,
UnsupportedSyntaxErrorKind::TypeParameterList => PythonVersion::PY312,
UnsupportedSyntaxErrorKind::TypeAliasStatement => PythonVersion::PY312,
UnsupportedSyntaxErrorKind::TypeParamDefault => PythonVersion::PY313,
Change::Added(version) => write!(f, "added in Python {version}"),
Change::Removed(version) => write!(f, "removed in Python {version}"),
}
}
}
impl UnsupportedSyntaxErrorKind {
/// Returns the Python version when the syntax associated with this error was changed, and the
/// type of [`Change`] (added or removed).
const fn changed_version(self) -> Change {
match self {
UnsupportedSyntaxErrorKind::Match => Change::Added(PythonVersion::PY310),
UnsupportedSyntaxErrorKind::Walrus => Change::Added(PythonVersion::PY38),
UnsupportedSyntaxErrorKind::ExceptStar => Change::Added(PythonVersion::PY311),
UnsupportedSyntaxErrorKind::StarTuple(_) => Change::Added(PythonVersion::PY38),
UnsupportedSyntaxErrorKind::RelaxedDecorator => Change::Added(PythonVersion::PY39),
UnsupportedSyntaxErrorKind::PositionalOnlyParameter => {
Change::Added(PythonVersion::PY38)
}
UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName => {
Change::Removed(PythonVersion::PY38)
}
UnsupportedSyntaxErrorKind::TypeParameterList => Change::Added(PythonVersion::PY312),
UnsupportedSyntaxErrorKind::TypeAliasStatement => Change::Added(PythonVersion::PY312),
UnsupportedSyntaxErrorKind::TypeParamDefault => Change::Added(PythonVersion::PY313),
}
}
/// Returns whether or not this kind of syntax is unsupported on `target_version`.
pub(crate) fn is_unsupported(self, target_version: PythonVersion) -> bool {
match self.changed_version() {
Change::Added(version) => target_version < version,
Change::Removed(version) => target_version >= version,
}
}
/// Returns `true` if this kind of syntax is supported on `target_version`.
pub(crate) fn is_supported(self, target_version: PythonVersion) -> bool {
!self.is_unsupported(target_version)
}
}
#[cfg(target_pointer_width = "64")]
mod sizes {
use crate::error::{LexicalError, LexicalErrorType};