diff --git a/packages/griffelib/src/griffe/_internal/expressions.py b/packages/griffelib/src/griffe/_internal/expressions.py index 73082b2f..03f67708 100644 --- a/packages/griffelib/src/griffe/_internal/expressions.py +++ b/packages/griffelib/src/griffe/_internal/expressions.py @@ -111,6 +111,7 @@ def _join( joint: str | Expr, *, flat: bool = True, + outer_precedence: _OperatorPrecedence = _OperatorPrecedence.NONE, ) -> Iterator[str | Expr]: """Apply a separator between elements. @@ -119,14 +120,14 @@ def _join( """ it = iter(elements) try: - # Since we are in a sequence, don't parenthesize items. + # Since we are in a sequence, don't parenthesize items (unless told otherwise). # Avoids [a + b, c + d] being serialized as [(a + b), (c + d)] - yield from _yield(next(it), flat=flat, outer_precedence=_OperatorPrecedence.NONE) + yield from _yield(next(it), flat=flat, outer_precedence=outer_precedence) except StopIteration: return for element in it: - yield from _yield(joint, flat=flat, outer_precedence=_OperatorPrecedence.NONE) - yield from _yield(element, flat=flat, outer_precedence=_OperatorPrecedence.NONE) + yield from _yield(joint, flat=flat, outer_precedence=outer_precedence) + yield from _yield(element, flat=flat, outer_precedence=outer_precedence) def _field_as_dict( @@ -277,7 +278,12 @@ class ExprAttribute(Expr): def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: precedence = _get_precedence(self) - yield from _yield(self.values[0], flat=flat, outer_precedence=precedence, is_left=True) + first = self.values[0] + if isinstance(first, str) and first.isdigit(): + # Integer literals need parentheses: `1.bit_length()` is a syntax error. + yield f"({first})" + else: + yield from _yield(first, flat=flat, outer_precedence=precedence, is_left=True) for value in self.values[1:]: yield "." yield from _yield(value, flat=flat, outer_precedence=precedence) @@ -417,12 +423,14 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: if self.is_async: yield "async " yield "for " - yield from _yield(self.target, flat=flat) + yield from _yield(self.target, flat=flat, outer_precedence=_OperatorPrecedence.NONE) yield " in " - yield from _yield(self.iterable, flat=flat) - if self.conditions: + # The iterable and conditions are disjunctions in the grammar: + # lambdas, conditionals and walrus assignments need parentheses there. + yield from _yield(self.iterable, flat=flat, outer_precedence=_OperatorPrecedence.OR, is_left=True) + for condition in self.conditions: yield " if " - yield from _join(self.conditions, " if ", flat=flat) + yield from _yield(condition, flat=flat, outer_precedence=_OperatorPrecedence.OR, is_left=True) # TODO: `ExprConstant` is never instantiated, @@ -451,6 +459,8 @@ class ExprDict(Expr): def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "{" + # Walrus assignments and yields need parentheses in keys and values, + # e.g. `{1: (x := 2)}`. yield from _join( ( ("**", value) if key is None else (key, ": ", value) @@ -458,6 +468,7 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: ), ", ", flat=flat, + outer_precedence=_OperatorPrecedence.STARRED, ) yield "}" @@ -476,12 +487,12 @@ class ExprDictComp(Expr): def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "{" if self.value: - yield from _yield(self.key, flat=flat) + yield from _yield(self.key, flat=flat, outer_precedence=_OperatorPrecedence.NONE) yield ": " - yield from _yield(self.value, flat=flat) + yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.NONE) else: yield "**" - yield from _yield(self.key, flat=flat) + yield from _yield(self.key, flat=flat, outer_precedence=_OperatorPrecedence.NONE) yield " " yield from _join(self.generators, " ", flat=flat) yield "}" @@ -498,33 +509,70 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield from _join(self.dims, ", ", flat=flat) +def _iterate_format_parts( + value: str | Expr, + conversion: str | None, + format_spec: Sequence[str | Expr] | None, + *, + flat: bool = True, +) -> Iterator[str | Expr]: + # Shared by `ExprFormatted` and `ExprInterpolation`. + yield "{" + if str(value).startswith("{"): + # Separate braces, `{{` would be read as an escaped brace, e.g. `f"{ {1: 2}}"`. + yield " " + # Lambdas and walrus assignments need parentheses: + # their colon would otherwise start the format specifier, e.g. `f"{(x := 1)}"`. + yield from _yield(value, flat=flat, outer_precedence=_OperatorPrecedence.IF_ELSE) + if conversion: + yield f"!{conversion}" + if format_spec: + yield ":" + for part in format_spec: + if isinstance(part, str): + # Literal braces must be doubled, as in literal f-string parts. + yield part.replace("{", "{{").replace("}", "}}") + else: + yield from _yield(part, flat=flat, outer_precedence=_OperatorPrecedence.NONE) + yield "}" + + @dataclass(eq=True, slots=True) class ExprFormatted(Expr): """Formatted string like `{1 + 1}`.""" value: str | Expr """Formatted value.""" + conversion: str | None = None + """Conversion flag (`s`, `r` or `a`), without the leading exclamation mark.""" + format_spec: Sequence[str | Expr] | None = None + """Format specifier parts: literal strings and interpolated expressions.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: - yield "{" - # Prevent parentheses from being added, avoiding `{(1 + 1)}` - yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.NONE) - yield "}" + yield from _iterate_format_parts(self.value, self.conversion, self.format_spec, flat=flat) @dataclass(eq=True, slots=True) class ExprGeneratorExp(Expr): - """Generator expressions like `a for b in c for d in e`.""" + """Generator expressions like `(a for b in c for d in e)`.""" element: str | Expr """Yielded element.""" generators: Sequence[Expr] """Generators iterated on.""" + implicit: bool = False + """Whether the generator's parentheses are implicit (as the sole argument of a call).""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: - yield from _yield(self.element, flat=flat) + # Generator expressions require parentheses everywhere + # except as the sole argument of a call, e.g. `f(a for a in b)`. + if not self.implicit: + yield "(" + yield from _yield(self.element, flat=flat, outer_precedence=_OperatorPrecedence.NONE) yield " " yield from _join(self.generators, " ", flat=flat) + if not self.implicit: + yield ")" @dataclass(eq=True, slots=True) @@ -562,12 +610,13 @@ class ExprInterpolation(Expr): value: str | Expr """Interpolated value.""" + conversion: str | None = None + """Conversion flag (`s`, `r` or `a`), without the leading exclamation mark.""" + format_spec: Sequence[str | Expr] | None = None + """Format specifier parts: literal strings and interpolated expressions.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: - yield "{" - # Prevent parentheses from being added, avoiding `{(1 + 1)}` - yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.NONE) - yield "}" + yield from _iterate_format_parts(self.value, self.conversion, self.format_spec, flat=flat) _FSTRING_ALL_QUOTES = ("'", '"', "'''", '"""') @@ -662,7 +711,8 @@ def canonical_path(self) -> str: def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield self.name yield "=" - yield from _yield(self.value, flat=flat) + # Walrus assignments and yields need parentheses, e.g. `f(a=(b := c))`. + yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.STARRED) @dataclass(eq=True, slots=True) @@ -674,7 +724,7 @@ class ExprVarPositional(Expr): def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "*" - yield from _yield(self.value, flat=flat) + yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.BIT_OR, is_left=True) @dataclass(eq=True, slots=True) @@ -686,7 +736,7 @@ class ExprVarKeyword(Expr): def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "**" - yield from _yield(self.value, flat=flat) + yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.BIT_OR, is_left=True) @dataclass(eq=True, slots=True) @@ -700,33 +750,35 @@ class ExprLambda(Expr): def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: pos_only = False - pos_or_kw = False - kw_only = False - length = len(self.parameters) + star = False yield "lambda" - if length: + if self.parameters: yield " " - for index, parameter in enumerate(self.parameters, 1): + for index, parameter in enumerate(self.parameters): + if index: + yield ", " if parameter.kind is ParameterKind.positional_only: pos_only = True - elif parameter.kind is ParameterKind.var_positional: + elif pos_only: + # End of the positional-only section. + pos_only = False + yield "/, " + if parameter.kind is ParameterKind.var_positional: + star = True yield "*" elif parameter.kind is ParameterKind.var_keyword: yield "**" - elif parameter.kind is ParameterKind.positional_or_keyword and not pos_or_kw: - pos_or_kw = True - elif parameter.kind is ParameterKind.keyword_only and not kw_only: - kw_only = True + elif parameter.kind is ParameterKind.keyword_only and not star: + # `*args` also starts the keyword-only section. + star = True yield "*, " - if parameter.kind is not ParameterKind.positional_only and pos_only: - pos_only = False - yield "/, " yield parameter.name if parameter.default and parameter.kind not in (ParameterKind.var_positional, ParameterKind.var_keyword): yield "=" - yield from _yield(parameter.default, flat=flat) - if index < length: - yield ", " + yield from _yield(parameter.default, flat=flat, outer_precedence=_OperatorPrecedence.STARRED) + if pos_only: + # All parameters are positional-only. + yield ", /" yield ": " # Body of lambda should not have parentheses, avoiding `lambda: a.b` yield from _yield(self.body, flat=flat, outer_precedence=_OperatorPrecedence.NONE) @@ -756,7 +808,7 @@ class ExprListComp(Expr): def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "[" - yield from _yield(self.element, flat=flat) + yield from _yield(self.element, flat=flat, outer_precedence=_OperatorPrecedence.NONE) yield " " yield from _join(self.generators, " ", flat=flat) yield "]" @@ -866,9 +918,10 @@ class ExprNamedExpr(Expr): """Value.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: - yield from _yield(self.target, flat=flat) + yield from _yield(self.target, flat=flat, outer_precedence=_OperatorPrecedence.NONE) yield " := " - yield from _yield(self.value, flat=flat) + # Nested walrus assignments and yields need parentheses, e.g. `a := (b := c)`. + yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.STARRED) @dataclass(eq=True, slots=True) @@ -909,7 +962,7 @@ class ExprSetComp(Expr): def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "{" - yield from _yield(self.element, flat=flat) + yield from _yield(self.element, flat=flat, outer_precedence=_OperatorPrecedence.NONE) yield " " yield from _join(self.generators, " ", flat=flat) yield "}" @@ -927,14 +980,15 @@ class ExprSlice(Expr): """Iteration step.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: + # Walrus assignments need parentheses in slice bounds, e.g. `a[(b := c):]`. if self.lower is not None: - yield from _yield(self.lower, flat=flat) + yield from _yield(self.lower, flat=flat, outer_precedence=_OperatorPrecedence.STARRED) yield ":" if self.upper is not None: - yield from _yield(self.upper, flat=flat) + yield from _yield(self.upper, flat=flat, outer_precedence=_OperatorPrecedence.STARRED) if self.step is not None: yield ":" - yield from _yield(self.step, flat=flat) + yield from _yield(self.step, flat=flat, outer_precedence=_OperatorPrecedence.STARRED) @dataclass(eq=True, slots=True) @@ -1055,7 +1109,7 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "yield" if self.value is not None: yield " " - yield from _yield(self.value, flat=flat) + yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.STARRED) @dataclass(eq=True, slots=True) @@ -1067,7 +1121,7 @@ class ExprYieldFrom(Expr): def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "yield from " - yield from _yield(self.value, flat=flat) + yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.STARRED) _unary_op_map = { @@ -1118,6 +1172,8 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: ExprConstant: lambda _: _OperatorPrecedence.ATOMIC, ExprJoinedStr: lambda _: _OperatorPrecedence.ATOMIC, ExprFormatted: lambda _: _OperatorPrecedence.ATOMIC, + ExprTemplateStr: lambda _: _OperatorPrecedence.ATOMIC, + ExprInterpolation: lambda _: _OperatorPrecedence.ATOMIC, # Container displays. ExprList: lambda _: _OperatorPrecedence.ATOMIC, ExprTuple: lambda _: _OperatorPrecedence.ATOMIC, @@ -1151,8 +1207,7 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: ExprIfExp: lambda _: _OperatorPrecedence.IF_ELSE, ExprNamedExpr: lambda _: _OperatorPrecedence.ASSIGN, ExprLambda: lambda _: _OperatorPrecedence.LAMBDA, - # NOTE: Ruff categorizes as atomic, but `(a for a in b).c` implies its less than `CALL_ATTRIBUTE`. - ExprGeneratorExp: lambda _: _OperatorPrecedence.LAMBDA, + ExprGeneratorExp: lambda _: _OperatorPrecedence.ATOMIC, ExprVarPositional: lambda _: _OperatorPrecedence.STARRED, ExprVarKeyword: lambda _: _OperatorPrecedence.STARRED, ExprYield: lambda _: _OperatorPrecedence.YIELD, @@ -1200,7 +1255,11 @@ def _build_boolop(node: ast.BoolOp, parent: Module | Class, **kwargs: Any) -> Ex def _build_call(node: ast.Call, parent: Module | Class, **kwargs: Any) -> Expr: function = _build(node.func, parent, **kwargs) - positional_args = [_build(arg, parent, **kwargs) for arg in node.args] + if len(node.args) == 1 and not node.keywords and isinstance(node.args[0], ast.GeneratorExp): + # A sole generator argument reuses the call's parentheses, e.g. `f(a for a in b)`. + positional_args = [_build(node.args[0], parent, implicit_parens=True, **kwargs)] + else: + positional_args = [_build(arg, parent, **kwargs) for arg in node.args] keyword_args = [_build(kwarg, parent, function=function, **kwargs) for kwarg in node.keywords] return ExprCall(function, [*positional_args, *keyword_args]) @@ -1276,20 +1335,43 @@ def _build_dictcomp(node: ast.DictComp, parent: Module | Class, **kwargs: Any) - ) +_conversion_map = {115: "s", 114: "r", 97: "a"} + + +def _build_format_spec(node: ast.expr | None, parent: Module | Class, **kwargs: Any) -> list[str | Expr] | None: + if node is None: + return None + # Format specifiers are joined-string parts without the `f` prefix and quotes: + # literal parts stay raw, interpolated parts are built recursively. + return [_build(value, parent, in_joined_str=True, **kwargs) for value in node.values] # ty:ignore[unresolved-attribute] + + def _build_formatted( node: ast.FormattedValue, parent: Module | Class, *, in_formatted_str: bool = False, # noqa: ARG001 + in_joined_str: bool = False, # noqa: ARG001 **kwargs: Any, ) -> Expr: - return ExprFormatted(_build(node.value, parent, in_formatted_str=True, **kwargs)) + return ExprFormatted( + _build(node.value, parent, in_formatted_str=True, in_joined_str=True, **kwargs), + conversion=_conversion_map.get(node.conversion), + format_spec=_build_format_spec(node.format_spec, parent, **kwargs), + ) -def _build_generatorexp(node: ast.GeneratorExp, parent: Module | Class, **kwargs: Any) -> Expr: +def _build_generatorexp( + node: ast.GeneratorExp, + parent: Module | Class, + *, + implicit_parens: bool = False, + **kwargs: Any, +) -> Expr: return ExprGeneratorExp( _build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators], + implicit=implicit_parens, ) @@ -1358,7 +1440,15 @@ def _build_setcomp(node: ast.SetComp, parent: Module | Class, **kwargs: Any) -> return ExprSetComp(_build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators]) -def _build_slice(node: ast.Slice, parent: Module | Class, **kwargs: Any) -> Expr: +def _build_slice( + node: ast.Slice, + parent: Module | Class, + *, + subscript_slice: bool = False, # noqa: ARG001 + **kwargs: Any, +) -> Expr: + # Note: `subscript_slice` is intentionally consumed here so that it doesn't propagate + # to the slice bounds, where tuples require their parentheses, e.g. `o[(1, 2):]`. return ExprSlice( None if node.lower is None else _build(node.lower, parent, **kwargs), None if node.upper is None else _build(node.upper, parent, **kwargs), @@ -1464,8 +1554,19 @@ def __call__(self, node: Any, parent: Module | Class, **kwargs: Any) -> Expr: .. if sys.version_info >= (3, 14): - def _build_interpolation(node: ast.Interpolation, parent: Module | Class, **kwargs: Any) -> Expr: - return ExprInterpolation(_build(node.value, parent, **kwargs)) + def _build_interpolation( + node: ast.Interpolation, + parent: Module | Class, + *, + in_formatted_str: bool = False, # noqa: ARG001 + in_joined_str: bool = False, # noqa: ARG001 + **kwargs: Any, + ) -> Expr: + return ExprInterpolation( + _build(node.value, parent, in_formatted_str=True, in_joined_str=True, **kwargs), + conversion=_conversion_map.get(node.conversion), + format_spec=_build_format_spec(node.format_spec, parent, **kwargs), + ) def _build_templatestr( node: ast.TemplateStr, diff --git a/packages/griffelib/tests/test_expressions.py b/packages/griffelib/tests/test_expressions.py index c18cf52b..cf08d155 100644 --- a/packages/griffelib/tests/test_expressions.py +++ b/packages/griffelib/tests/test_expressions.py @@ -88,6 +88,85 @@ def test_expressions(code: str) -> None: assert str(expression) == code +# Expressions from each precedence level, and contexts embedding them. +# Not all combinations are valid Python: invalid ones are skipped. +_expression_shapes = [ + "x", + "a.b", + "'s'", + "x + 1", + "a | b", + "-x", + "not x", + "a or b", + "a < b", + "x ** 2", + "a if b else c", + "lambda: 1", + "(w := 1)", + "(g for g in y)", + "[g for g in y]", + "{g: g for g in y}", + "f(1)", + "o[1]", + "(1, 2)", + "[1, 2]", + "{1: 2}", + "{1, 2}", + "f'{x!r}'", + "f'{x:>{width}}'", +] +_expression_contexts = [ + "f(%s)", + "f(%s, 1)", + "f(a=%s)", + "f(*%s)", + "f(**%s)", + "[%s, 1]", + "{%s, 1}", + "(%s, 1)", + "{%s: 1}", + "{1: %s}", + "o[%s]", + "o[%s:1]", + "o[1:%s]", + "o[1:1:%s]", + "[%s for u in y]", + "[u for u in %s]", + "[u for u in y if %s]", + "(%s for u in y)", + "lambda p=%s: p", + "%s + 1", + "-%s", + "%s.m", + "%s()", + "%s if c else d", + "f'{%s}'", + "f'{v:{%s}}'", + "[(nv := %s)]", +] + + +@pytest.mark.parametrize("shape", _expression_shapes) +@pytest.mark.parametrize("context", _expression_contexts) +def test_expressions_stay_valid_and_equivalent(shape: str, context: str) -> None: + """Stringified expressions must re-parse as valid, semantically-equivalent Python. + + Parameters: + shape: An expression to embed in each code context. + context: A code template embedding the expression. + """ + code = context % shape + try: + original = ast.parse(code, mode="eval") + except SyntaxError: + pytest.skip("shape is invalid in this context") + expression = get_expression(original.body, parent=Module("module"), parse_strings=False) + rendered = str(expression) + reparsed = ast.parse(rendered, mode="eval") # Output must be valid Python. + assert ast.dump(reparsed) == ast.dump(original), f"{code!r} rendered as {rendered!r}" + + def test_length_one_tuple_as_string() -> None: """Length-1 tuples must have a trailing comma.""" code = "x = ('a',)" @@ -265,6 +344,47 @@ def test_fstring_quote_selection(code: str, expected: str) -> None: assert str(expression) == expected +def test_fstring_conversions_and_format_specs() -> None: + """Conversion flags and format specifiers must be preserved. + + Regression test: they used to be dropped entirely (`f"{x!r:>10}"` rendered as `f'{x}'`). + """ + with temporary_visited_module( + """ + a = f"{x!r}" + b = f"{x:>10}" + c = f"{x:{width}.{precision}}" + d = f"{x!a:>{width}}" + e = f"{x:%Y-%m-%d}" + + def func(param=f"{x!r:>3}"): ... + """, + ) as module: + assert str(module["a"].value) == "f'{x!r}'" + assert str(module["b"].value) == "f'{x:>10}'" + assert str(module["c"].value) == "f'{x:{width}.{precision}}'" + assert str(module["d"].value) == "f'{x!a:>{width}}'" + assert str(module["e"].value) == "f'{x:%Y-%m-%d}'" + assert str(module["func"].parameters["param"].default) == "f'{x!r:>3}'" + + +@pytest.mark.skipif(sys.version_info < (3, 14), reason="t-strings require Python 3.14+") +def test_tstring_conversions_and_format_specs() -> None: + """Same as f-strings: t-string interpolations must keep conversions, specs and quotes.""" + with temporary_visited_module( + """ + a = t"{x!r}" + b = t"{x:>10}" + c = t"{x!s:>{width}}" + d = t"{'quoted'}" + """, + ) as module: + assert str(module["a"].value) == "t'{x!r}'" + assert str(module["b"].value) == "t'{x:>10}'" + assert str(module["c"].value) == "t'{x!s:>{width}}'" + assert str(module["d"].value) == "t\"{'quoted'}\"" + + @pytest.mark.skipif(sys.version_info < (3, 14), reason="t-strings require Python 3.14+") @pytest.mark.parametrize( ("code", "expected"), diff --git a/packages/griffelib/tests/test_nodes.py b/packages/griffelib/tests/test_nodes.py index 46d8d7b9..7a0e605d 100644 --- a/packages/griffelib/tests/test_nodes.py +++ b/packages/griffelib/tests/test_nodes.py @@ -66,33 +66,76 @@ *(["t'say \"hello\" to {x}'"] if sys.version_info >= (3, 14) else []), # YORE: EOL 3.13: Regex-replace `\*\(\[(.+)\].+\),` with `\1,` within line. *(["t'''it's \"complicated\" {x}'''"] if sys.version_info >= (3, 14) else []), + # Formatted values: conversions and format specifiers. + "f'{x!r}'", + "f'{x!s}'", + "f'{x!a}'", + "f'{x:>10}'", + "f'{x:{width}}'", + "f'{x!r:>{width}}'", + "f'a {x:%Y-%m-%d} b'", + "f'{(x := 1)}'", + "f'{ {1: 2}}'", + # YORE: EOL 3.13: Regex-replace `\*\(\[(.+)\].+\),` with `\1,` within line. + *(["t'{x!r}'"] if sys.version_info >= (3, 14) else []), + # YORE: EOL 3.13: Regex-replace `\*\(\[(.+)\].+\),` with `\1,` within line. + *(["t'{x!s:>{width}}'"] if sys.version_info >= (3, 14) else []), # Slices. "o[x]", "o[x, y]", "o[x:y]", "o[x:y, z]", "o[x, y(z)]", + "o[(a, b):y]", + "o[a + b:]", # Walrus operator. "a if (a := b) else c", + "{(a := 1): 2}", + "{1: (a := 2)}", + "[a for a in b if (c := a)]", # Starred. "a(*b, **c)", + "a(*b | c)", # Structs. "(a, b, c)", "{a, b, c}", "{a: b, c: d}", + "{a + b: c + d for a, b in e}", "[a, b, c]", + "[a + b for a in c]", + # Generator expressions. + "(a + b for a in c)", + "a(b for b in c)", + "sum((a for a in b), c)", + "[(a for a in b), c]", + "{1: (a for a in b)}", + "(a for a in b if (c := a))", # Yields. "yield", "yield a", + "yield a + b", "yield from a", # Lambdas. "lambda a: a", "lambda a, b: a", "lambda *a, **b: a", "lambda a, b=0: a", + "lambda a=b + c: a", "lambda a, /, b, c: a", "lambda a, *, b, c: a", "lambda a, /, b, *, c: a", + "lambda a, /: a", + "lambda a, b, /: a", + "lambda a, /, *args: args", + "lambda a, /, **kwargs: kwargs", + "lambda a, /, *, b: b", + "lambda a, /, b=0, *args, c=1, **kwargs: a", + "lambda *args, b: b", + "lambda *args, b=0: b", + # Calls with expression-level arguments. + "call(a=b + c)", + # Attribute access on integer literals. + "(1).bit_length()", ]