mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-29 03:04:38 +08:00
* builtins: name the other operand with tp_name in str and Template __add__
`str.__add__` and `Template.__add__` built their TypeError with
`PyType::name()`, which drops the module, where CPython formats `tp_name`:
>>> t"a" + "b"
TypeError: can only concatenate Template (not 'str') to Template
CPython 3.14 reports the qualified name and quotes the operand with double
quotes, as `Objects/unicodeobject.c` and `Objects/templateobject.c` do:
TypeError: can only concatenate string.templatelib.Template
(not "str") to string.templatelib.Template
`Template.__add__` also spelled its own name literally rather than taking it
from `PyClassDef::TP_NAME`, so the two halves of the message could drift apart.
Known gap: `slot_name()` still does not qualify a class defined in a module, so
`"a" + collections.OrderedDict()` names `OrderedDict` where CPython names
`collections.OrderedDict`. This affects both methods.
Unblocks test_template_concatenation in test_tstring.
Assisted-by: Claude Code:claude-opus-5
* interpolated strings: align f-string and t-string diagnostics with CPython
RustPython re-derives CPython's syntax diagnostics by scanning the source rather
than by translating ruff's parse errors, and the scanner shared by f-strings and
t-strings misread several replacement field states:
- A field that ran off the end of the literal (`f'{'`) was reported as an
expression that failed to start, where CPython asks for the closing brace.
- A field whose expression cannot start (`f'{;'`) and one whose expression runs
into a stray character (`f'{a;'`) both fell through to the missing brace
message instead of CPython's two distinct ones.
- A conversion or format spec following `=` was never validated, and a format
spec following a valid conversion was skipped entirely.
- Format specs were scanned with the `{{` escape rule that only applies to
literal text, so nested replacement fields inside them went unchecked.
- Brackets opened inside a field were never matched, so `f'{a[4)}'` and
`f'{3)+(4}'` fell back to a bare "invalid syntax".
- Comments inside a field were not recognised, so a `#` that swallows the
closing brace was reported as an unterminated literal, and a comment in a
multi-line field hid the expression behind it.
- Unterminated literals were named "string" whatever their prefix, and an
interpolated literal left with a replacement field open reported the missing
quote rather than the brace.
A field is only "never closed" while the tokenizer is still reading its
expression. Past the field's own `:` it is emitting literal text again, so
`f'{a:>5` runs out of input as an ordinary unterminated literal while `f'{a`
reports the brace. The field's own `:` is also not the one in a slice, a display
or a lambda, and an unclosed bracket inside the expression is what CPython names
rather than the field around it, so this walk tracks the whole delimiter stack.
Two messages were worded from the wrong branch of CPython's tokenizer. The hint
"perhaps you escaped the end quote?" is raised only where lexer.c handles a
literal without an interpolation prefix; its `%c-string` branch has just the
triple-quoted and plain forms, so pairing the hint with a prefix produced
`unterminated f-string literal (...); perhaps you escaped the end quote?`, which
CPython never emits. And a bracket mismatch inside a field dropped the
` on line %d` clause that lexer.c adds whenever `parenlinenostack[level]` differs
from the current `lineno`; the general bracket scanner in this file already
computed that suffix, so `f"""{a[\n4)}"""` now names line 1 as CPython does.
The rules these now follow are the ones CPython spells out in
Grammar/python.gram (`invalid_fstring_replacement_field` and its t-string twin)
and in Parser/lexer/lexer.c, which likewise parameterises the literal's prefix
rather than duplicating the messages.
Only a field's expression is code. A format spec is text, so a `(` there opens
nothing and a `#` selects the alternate form instead of starting a comment;
inside the expression a `#` does start one. Scanning the whole field for either
therefore reported diagnostics against valid literals, and because these
scanners only run once the source has already failed to parse, that let a good
literal take the blame for an error further down the file. Expression-level
checks now stop at the field's top-level separator and skip comments, and the
literal is walked field by field rather than byte by byte so that a `{` inside a
comment no longer opens one. A pre-existing instance of the same bug in the
line-continuation check is fixed along with them.
Ruff reports a mixed literal concatenation only as a bytes/non-bytes mix, so
`t"x" b"y"` arrived as a bytes error where CPython names the t-string. CPython's
`invalid_string_tstring_concat` is an `invalid_` rule, reached only on the error
pass, and `strings` tries `(fstring|string)+` ahead of it: that alternative
consumes the concatenation's leading run of non-t-string literals, so a mix among
those raises from `_PyPegen_concatenate_strings` on the first pass and sets
`error_indicator`, which suppresses the t-string rule. The t-string message
therefore wins for `t"x" b"y"` and the bytes message keeps precedence for
`"a" b"b" t"c"`. Both are now settled here rather than left to whichever scanner
runs next, which had been answering the second case with an unrelated
"Is this intended to be part of the string?".
A leading doubled `=` or `!` was read as a marker with an empty expression before it,
so `f"{==a}"` reported `valid expression required before '='` where CPython has no
expression to report at all. The same lookahead now guards that branch, and a
doubled `=` or `!` counts as an expression that cannot start.
A top-level `=` or `!` was read as a debug or conversion marker without checking
whether it belonged to a longer operator, so `f"{a==b}"` and `f"{a!=b}"` were also
read as fields ending early. CPython tokenises `==`, `!=`, `<=` and the rest as
single tokens before it ever considers the debug marker, so the separator scan now
skips a `=` that follows one of `= ! < > + - * / % & | ^ @ :` or precedes another
`=`, and a `!` that precedes one.
An expression cannot end on an operator that still wants an operand, and CPython
points at that operator rather than at the brace. `f"{a==}"`, `f"{a and}"` and
`f"{a.b.}"` fell through to a plain `invalid syntax`; they now carry the same
message and column CPython gives, reusing the scan that already handled `;` and
`$` for exactly this shape. `is not` and `not in` are single operators, so the
first word is what gets pointed at, and `...` is consumed in whole triples so
that `f"{....}"` blames the fourth dot rather than the first. The region has to
be walked forwards: a comment's terminating newline is whitespace, so trimming
backwards from the end would step into the comment body and read a triple-quoted
field whose comment ends in `+` as an expression ending in `+`.
That check runs only once the field is known to close, which is where a stray
character and a dangling operator part ways. A stray character is a finished
token, so the parser rejects it on the lookahead in
`annotated_rhs !('='|'!'|':'|'}')` and `f"{a;"` gets the separator message even
with no closing brace. A dangling operator instead makes the parser ask for one
more token, and producing it runs into the literal's closing quote, where
lexer.c answers from its `INSIDE_FSTRING(tok)` branch with
`%c-string: expecting '}'` before any `invalid_` rule is reached. So `f"{a and"`
is a missing brace while `f"{a and}"` names the operator.
A leading `.` was read as a character that cannot start an expression, so the
Ellipsis literal `f"{...}"`, the float `f"{.5}"` and its signed form `f"{-.5}"`
were reported as broken whenever the file failed to parse somewhere else.
`pegen` joins the cpython spelling dictionary, for the `_PyPegen_*` names these
comments cite.
Known gaps, none covered by either test file. Needing the longest valid expression
prefix, which a character scanner cannot compute: a starred tuple (`f"{*a,}"`) and
a dict-unpacking display are read as unable to start an expression, and
`f"{a===b}"`, `f"{a b}"` and `f"{a,,}"` fall through to a bare `invalid syntax`.
The caret still differs from CPython's on `expecting a valid expression after '{'`,
`expecting '}'`, `expecting '}', or format specs` and `unterminated ... literal`,
and on the messages CPython points at a whole token for, such as
`f"{lambda x: x}"` and `f"{x! r}"`. A quote inside a format spec is still treated
as a string delimiter.
Two more are reachable only through a t-string concatenation that ruff does not
report as a bytes mix, so `mixed_tstring_literal_error` never runs: three or more
literals with no bytes literal among them (`t"a" t"b" "c"`, `"a" "b" t"c"`) answer
with `invalid syntax. Is this intended to be part of the string?`, and so does a
tokenizer-level nesting overflow where CPython has
`too many nested f-strings or t-strings`. Two-literal mixes are correct because
ruff's own error carries the wording.
Two pre-existing bugs outside this change are worth naming, since it touches
their neighbourhood. `unterminated triple-quoted ... literal` reports
`detected at line 2` for a one-line source where CPython reports line 1, for every
prefix including none, so it is in the shared line arithmetic rather than the
interpolated path. And `str.__add__`'s message is unreachable for an operand that
defines `__radd__`: `"a" + 1` falls through to the generic
`unsupported operand type(s)` from the binop dispatch, where CPython has
`can only concatenate str (not "int") to str`.
Unblocks test_syntax_errors and test_literal_concatenation in test_tstring, and
test_comments,
test_conversions, test_invalid_syntax_error_message, test_mismatched_braces,
test_mismatched_parens, test_parens_in_expressions and
test_syntax_error_after_debug in test_fstring.
Assisted-by: Claude Code:claude-opus-5
* diagnostics: give the source scanners a diagnostic type
Every scanner in `cpython_parse_diagnostic_override` returned a bare
`Option<(String, usize, usize)>` — 69 signatures and 114 construction sites of
message, start offset, end offset — which the consumer then reassembled into a
message and a range one call later. They return a `CpythonDiagnostic` now, and
the two things that consume one, `NormalizedParseDiagnostic::other` and
`CompileError::from_source_error`, take it whole.
The type earns its name. These scanners run *after* ruff's parse has failed, so
what they produce is not a parse error but a reconstruction of what CPython
would have said about the same source; nothing here ever hands one back to ruff.
Reusing `ruff_python_parser::ParseError` would have fit the shape — it is
`{ ParseErrorType, TextRange }` — but every one of the 114 sites would have
wrapped its message in the single `OtherError` variant of a ninety-variant enum,
widened `other` and `from_source_error` to accept variants no caller
constructs, and, being a foreign type, ruled out the constructor that now
carries the `u32` cast and its justification. The `OtherError` wrapping happens
once per consumer instead, at the boundary where this does cross into ruff's
vocabulary.
Two nameless tuples that carried more than a diagnostic get names, following
`CallArgFrame` and `AssignmentContext` in this file:
- `bracket_syntax_error` returned the message alongside a bare `bool` for
whether the bracket was left open, which the caller needs apart from the
message because ruff reports that case as an EOF error. It returns a
`BracketError` now.
- `unclosed_replacement_field_error` walked its delimiter stack as
`Vec<(usize, u8, bool)>`, where the third element decided whether the field
had reached its format spec — `.2 = true` and `Some((_, b'{', false))` at the
use sites. `OpenDelimiter` names all three.
The `source_error!` macro is untouched at the 34 call sites it already had and
drops to five lines. Two copies of its body had been written out by hand
(`invalid_number_literal_error`, `unterminated_string_error`) and fold into it,
bringing it to 36.
One thing does change beyond the packaging. `TextRange::new` asserts that its
start does not exceed its end, and it asserts unconditionally. Building the
range at the scanner rather than at the consumer therefore puts that assert in
front of the hundred-odd scanners that reach `NormalizedParseDiagnostic::other`,
which never built a range before — they handed their two offsets straight to
`source_locations`. A scanner that emitted a reversed span used to produce a
garbled location; now it aborts. Every site was read for this and each clamp is
anchored to a bound at or after its start, and neither two hundred thousand
generated sources nor eleven thousand mutations of stdlib files reached it, so
this is a latent invariant made loud rather than a new failure — but it is not
packaging, so it is written down here.
Otherwise a pure refactor: no diagnostic changes message, column or line.
Verified by building the parent commit and diffing its output against this one
over a malformed-source corpus and over every `Lib/**/*.py` that parses clean
(1728 files, each recompiled with a trailing `$` so a scanner that misreads
valid code shows up as a moved line) — byte-identical, and the count of files
where the reported line moves is 1149 on both. test_fstring, test_tstring,
test_string_literals, test_syntax, test_exceptions, test_grammar and
test_compile all pass.
Addresses the review on #8601.
Assisted-by: Claude Code:claude-opus-5
* diagnostics: name the operator-character set behind the debug marker
`is_replacement_field_marker` carried its list of operator characters inline, a
sixteen-line `matches!` wrapped around three lines of logic, and it looked very
much like `is_dangling_operator_byte` a few functions down — enough that a
reviewer asked whether they were the same set.
They are not, and neither contains the other. Written in the same order the
difference is three characters:
precedes_equals_in_one_operator ! % & * + - / : < = > @ ^ |
is_dangling_operator_byte ! % & * + - . / < = > @ ^ | ~
The two are derived from different things. One asks which bytes pair with a
following `=` to make a single token, so that the `=` is part of that operator
rather than the start of a debug specifier — read off Python's token table, and
`.=` and `~=` are not on it, while `:=` is. The other asks which bytes make up
an operator that still wants an operand, which is why `~a` and `a.b.` put `.`
and `~` there and why the separator `:` stays out.
So the list moves next to the one it resembles, under a name, both sorted the
same way, and each says what the other has that it does not. Neither is defined
in terms of the other: the thirteen characters they share are two answers that
happen to coincide, not a set with a meaning of its own, and a common list would
tie each to the other's reasons to change.
No behavior change: the extracted list is character for character what was
inline. Verified against a build of the parent over a malformed-source corpus
and every `Lib/**/*.py` that parses clean — byte-identical — and with
test_fstring, test_tstring, test_string_literals, test_syntax, test_exceptions,
test_grammar and test_compile.
Addresses the review on #8601.
Assisted-by: Claude Code:claude-opus-5
* diagnostics: give the unterminated-literal scan one exit
A single-quoted literal that meets a newline and one that runs off the end of
the source are the same failure, and CPython's lexer says so in one condition:
if (c == EOF || (quote_size == 1 && c == '\n')) {
Everything after that — the check for a replacement field left open, and the
choice of message — is written once there. This scan had grown a second exit
for the newline case with its own copy of both, so the guard added for
interpolated literals had to be added twice and the message assembled twice.
The newline case now breaks out of the scan and falls into the exit that was
already handling end of source. `line` cannot have moved from `start_line` on
that path, and `quote_size` is 1, so the surviving exit computes the same
`detected_line` and the same `triple` the deleted one did.
No behavior change: verified against a build of the parent over a
malformed-source corpus and every `Lib/**/*.py` that parses clean —
byte-identical — and with test_fstring, test_tstring, test_string_literals,
test_syntax, test_exceptions, test_grammar and test_compile.
Assisted-by: Claude Code:claude-opus-5
* diagnostics: classify an unterminated triple-quoted t-string as incomplete input
`analyze_compile_error` pairs the f-string and t-string error variants
everywhere it names a message, but the match that decides whether a failure is
a `SyntaxError` or an `IncompleteInputError` listed only the f-string one. So a
`t'''` typed at the prompt would not have asked for another line where an
`f'''` would.
Would not have, rather than does not: the arm is unreachable today. The
compiler's `unterminated_string_error` scanner claims these first and hands
back an `OtherError`, so `f'''` and a bare `'''` get a `SyntaxError` at the
prompt too, on `main` as much as here — which is why
`test_codeop.test_incomplete` carries an `expectedFailure`. Whoever untangles
that precedence should not then find that f-strings work and t-strings
silently do not.
CPython does not separate the two either: `lexer.c` emits one message
parameterised with `%c` for the prefix and sets `E_EOFS` the same way for both,
and `_is_end_of_source` (`Parser/pegen.c`) looks only at that code, never at
which kind of literal produced it.
No behavior change, and none is testable while the arm cannot be reached.
Assisted-by: Claude Code:claude-opus-5
1879 lines
72 KiB
Python
Vendored
1879 lines
72 KiB
Python
Vendored
# -*- coding: utf-8 -*-
|
||
# There are tests here with unicode string literals and
|
||
# identifiers. There's a code in ast.c that was added because of a
|
||
# failure with a non-ascii-only expression. So, I have tests for
|
||
# that. There are workarounds that would let me run tests for that
|
||
# code without unicode identifiers and strings, but just using them
|
||
# directly seems like the easiest and therefore safest thing to do.
|
||
# Unicode identifiers in tests is allowed by PEP 3131.
|
||
|
||
import ast
|
||
import datetime
|
||
import dis
|
||
import os
|
||
import re
|
||
import types
|
||
import decimal
|
||
import unittest
|
||
import warnings
|
||
from test import support
|
||
from test.support.os_helper import temp_cwd
|
||
from test.support.script_helper import assert_python_failure, assert_python_ok
|
||
|
||
a_global = 'global variable'
|
||
|
||
# You could argue that I'm too strict in looking for specific error
|
||
# values with assertRaisesRegex, but without it it's way too easy to
|
||
# make a syntax error in the test strings. Especially with all of the
|
||
# triple quotes, raw strings, backslashes, etc. I think it's a
|
||
# worthwhile tradeoff. When I switched to this method, I found many
|
||
# examples where I wasn't testing what I thought I was.
|
||
|
||
class TestCase(unittest.TestCase):
|
||
def assertAllRaise(self, exception_type, regex, error_strings):
|
||
for str in error_strings:
|
||
with self.subTest(str=str):
|
||
with self.assertRaisesRegex(exception_type, regex):
|
||
eval(str)
|
||
|
||
def test__format__lookup(self):
|
||
# Make sure __format__ is looked up on the type, not the instance.
|
||
class X:
|
||
def __format__(self, spec):
|
||
return 'class'
|
||
|
||
x = X()
|
||
|
||
# Add a bound __format__ method to the 'y' instance, but not
|
||
# the 'x' instance.
|
||
y = X()
|
||
y.__format__ = types.MethodType(lambda self, spec: 'instance', y)
|
||
|
||
self.assertEqual(f'{y}', format(y))
|
||
self.assertEqual(f'{y}', 'class')
|
||
self.assertEqual(format(x), format(y))
|
||
|
||
# __format__ is not called this way, but still make sure it
|
||
# returns what we expect (so we can make sure we're bypassing
|
||
# it).
|
||
self.assertEqual(x.__format__(''), 'class')
|
||
self.assertEqual(y.__format__(''), 'instance')
|
||
|
||
# This is how __format__ is actually called.
|
||
self.assertEqual(type(x).__format__(x, ''), 'class')
|
||
self.assertEqual(type(y).__format__(y, ''), 'class')
|
||
|
||
def test_ast(self):
|
||
# Inspired by http://bugs.python.org/issue24975
|
||
class X:
|
||
def __init__(self):
|
||
self.called = False
|
||
def __call__(self):
|
||
self.called = True
|
||
return 4
|
||
x = X()
|
||
expr = """
|
||
a = 10
|
||
f'{a * x()}'"""
|
||
t = ast.parse(expr)
|
||
c = compile(t, '', 'exec')
|
||
|
||
# Make sure x was not called.
|
||
self.assertFalse(x.called)
|
||
|
||
# Actually run the code.
|
||
exec(c)
|
||
|
||
# Make sure x was called.
|
||
self.assertTrue(x.called)
|
||
|
||
def test_ast_line_numbers(self):
|
||
expr = """
|
||
a = 10
|
||
f'{a * x()}'"""
|
||
t = ast.parse(expr)
|
||
self.assertEqual(type(t), ast.Module)
|
||
self.assertEqual(len(t.body), 2)
|
||
# check `a = 10`
|
||
self.assertEqual(type(t.body[0]), ast.Assign)
|
||
self.assertEqual(t.body[0].lineno, 2)
|
||
# check `f'...'`
|
||
self.assertEqual(type(t.body[1]), ast.Expr)
|
||
self.assertEqual(type(t.body[1].value), ast.JoinedStr)
|
||
self.assertEqual(len(t.body[1].value.values), 1)
|
||
self.assertEqual(type(t.body[1].value.values[0]), ast.FormattedValue)
|
||
self.assertEqual(t.body[1].lineno, 3)
|
||
self.assertEqual(t.body[1].value.lineno, 3)
|
||
self.assertEqual(t.body[1].value.values[0].lineno, 3)
|
||
# check the binop location
|
||
binop = t.body[1].value.values[0].value
|
||
self.assertEqual(type(binop), ast.BinOp)
|
||
self.assertEqual(type(binop.left), ast.Name)
|
||
self.assertEqual(type(binop.op), ast.Mult)
|
||
self.assertEqual(type(binop.right), ast.Call)
|
||
self.assertEqual(binop.lineno, 3)
|
||
self.assertEqual(binop.left.lineno, 3)
|
||
self.assertEqual(binop.right.lineno, 3)
|
||
self.assertEqual(binop.col_offset, 3)
|
||
self.assertEqual(binop.left.col_offset, 3)
|
||
self.assertEqual(binop.right.col_offset, 7)
|
||
|
||
def test_ast_line_numbers_multiple_formattedvalues(self):
|
||
expr = """
|
||
f'no formatted values'
|
||
f'eggs {a * x()} spam {b + y()}'"""
|
||
t = ast.parse(expr)
|
||
self.assertEqual(type(t), ast.Module)
|
||
self.assertEqual(len(t.body), 2)
|
||
# check `f'no formatted value'`
|
||
self.assertEqual(type(t.body[0]), ast.Expr)
|
||
self.assertEqual(type(t.body[0].value), ast.JoinedStr)
|
||
self.assertEqual(t.body[0].lineno, 2)
|
||
# check `f'...'`
|
||
self.assertEqual(type(t.body[1]), ast.Expr)
|
||
self.assertEqual(type(t.body[1].value), ast.JoinedStr)
|
||
self.assertEqual(len(t.body[1].value.values), 4)
|
||
self.assertEqual(type(t.body[1].value.values[0]), ast.Constant)
|
||
self.assertEqual(type(t.body[1].value.values[0].value), str)
|
||
self.assertEqual(type(t.body[1].value.values[1]), ast.FormattedValue)
|
||
self.assertEqual(type(t.body[1].value.values[2]), ast.Constant)
|
||
self.assertEqual(type(t.body[1].value.values[2].value), str)
|
||
self.assertEqual(type(t.body[1].value.values[3]), ast.FormattedValue)
|
||
self.assertEqual(t.body[1].lineno, 3)
|
||
self.assertEqual(t.body[1].value.lineno, 3)
|
||
self.assertEqual(t.body[1].value.values[0].lineno, 3)
|
||
self.assertEqual(t.body[1].value.values[1].lineno, 3)
|
||
self.assertEqual(t.body[1].value.values[2].lineno, 3)
|
||
self.assertEqual(t.body[1].value.values[3].lineno, 3)
|
||
# check the first binop location
|
||
binop1 = t.body[1].value.values[1].value
|
||
self.assertEqual(type(binop1), ast.BinOp)
|
||
self.assertEqual(type(binop1.left), ast.Name)
|
||
self.assertEqual(type(binop1.op), ast.Mult)
|
||
self.assertEqual(type(binop1.right), ast.Call)
|
||
self.assertEqual(binop1.lineno, 3)
|
||
self.assertEqual(binop1.left.lineno, 3)
|
||
self.assertEqual(binop1.right.lineno, 3)
|
||
self.assertEqual(binop1.col_offset, 8)
|
||
self.assertEqual(binop1.left.col_offset, 8)
|
||
self.assertEqual(binop1.right.col_offset, 12)
|
||
# check the second binop location
|
||
binop2 = t.body[1].value.values[3].value
|
||
self.assertEqual(type(binop2), ast.BinOp)
|
||
self.assertEqual(type(binop2.left), ast.Name)
|
||
self.assertEqual(type(binop2.op), ast.Add)
|
||
self.assertEqual(type(binop2.right), ast.Call)
|
||
self.assertEqual(binop2.lineno, 3)
|
||
self.assertEqual(binop2.left.lineno, 3)
|
||
self.assertEqual(binop2.right.lineno, 3)
|
||
self.assertEqual(binop2.col_offset, 23)
|
||
self.assertEqual(binop2.left.col_offset, 23)
|
||
self.assertEqual(binop2.right.col_offset, 27)
|
||
|
||
def test_ast_line_numbers_nested(self):
|
||
expr = """
|
||
a = 10
|
||
f'{a * f"-{x()}-"}'"""
|
||
t = ast.parse(expr)
|
||
self.assertEqual(type(t), ast.Module)
|
||
self.assertEqual(len(t.body), 2)
|
||
# check `a = 10`
|
||
self.assertEqual(type(t.body[0]), ast.Assign)
|
||
self.assertEqual(t.body[0].lineno, 2)
|
||
# check `f'...'`
|
||
self.assertEqual(type(t.body[1]), ast.Expr)
|
||
self.assertEqual(type(t.body[1].value), ast.JoinedStr)
|
||
self.assertEqual(len(t.body[1].value.values), 1)
|
||
self.assertEqual(type(t.body[1].value.values[0]), ast.FormattedValue)
|
||
self.assertEqual(t.body[1].lineno, 3)
|
||
self.assertEqual(t.body[1].value.lineno, 3)
|
||
self.assertEqual(t.body[1].value.values[0].lineno, 3)
|
||
# check the binop location
|
||
binop = t.body[1].value.values[0].value
|
||
self.assertEqual(type(binop), ast.BinOp)
|
||
self.assertEqual(type(binop.left), ast.Name)
|
||
self.assertEqual(type(binop.op), ast.Mult)
|
||
self.assertEqual(type(binop.right), ast.JoinedStr)
|
||
self.assertEqual(binop.lineno, 3)
|
||
self.assertEqual(binop.left.lineno, 3)
|
||
self.assertEqual(binop.right.lineno, 3)
|
||
self.assertEqual(binop.col_offset, 3)
|
||
self.assertEqual(binop.left.col_offset, 3)
|
||
self.assertEqual(binop.right.col_offset, 7)
|
||
# check the nested call location
|
||
self.assertEqual(len(binop.right.values), 3)
|
||
self.assertEqual(type(binop.right.values[0]), ast.Constant)
|
||
self.assertEqual(type(binop.right.values[0].value), str)
|
||
self.assertEqual(type(binop.right.values[1]), ast.FormattedValue)
|
||
self.assertEqual(type(binop.right.values[2]), ast.Constant)
|
||
self.assertEqual(type(binop.right.values[2].value), str)
|
||
self.assertEqual(binop.right.values[0].lineno, 3)
|
||
self.assertEqual(binop.right.values[1].lineno, 3)
|
||
self.assertEqual(binop.right.values[2].lineno, 3)
|
||
call = binop.right.values[1].value
|
||
self.assertEqual(type(call), ast.Call)
|
||
self.assertEqual(call.lineno, 3)
|
||
self.assertEqual(call.col_offset, 11)
|
||
|
||
def test_ast_line_numbers_duplicate_expression(self):
|
||
expr = """
|
||
a = 10
|
||
f'{a * x()} {a * x()} {a * x()}'
|
||
"""
|
||
t = ast.parse(expr)
|
||
self.assertEqual(type(t), ast.Module)
|
||
self.assertEqual(len(t.body), 2)
|
||
# check `a = 10`
|
||
self.assertEqual(type(t.body[0]), ast.Assign)
|
||
self.assertEqual(t.body[0].lineno, 2)
|
||
# check `f'...'`
|
||
self.assertEqual(type(t.body[1]), ast.Expr)
|
||
self.assertEqual(type(t.body[1].value), ast.JoinedStr)
|
||
self.assertEqual(len(t.body[1].value.values), 5)
|
||
self.assertEqual(type(t.body[1].value.values[0]), ast.FormattedValue)
|
||
self.assertEqual(type(t.body[1].value.values[1]), ast.Constant)
|
||
self.assertEqual(type(t.body[1].value.values[1].value), str)
|
||
self.assertEqual(type(t.body[1].value.values[2]), ast.FormattedValue)
|
||
self.assertEqual(type(t.body[1].value.values[3]), ast.Constant)
|
||
self.assertEqual(type(t.body[1].value.values[3].value), str)
|
||
self.assertEqual(type(t.body[1].value.values[4]), ast.FormattedValue)
|
||
self.assertEqual(t.body[1].lineno, 3)
|
||
self.assertEqual(t.body[1].value.lineno, 3)
|
||
self.assertEqual(t.body[1].value.values[0].lineno, 3)
|
||
self.assertEqual(t.body[1].value.values[1].lineno, 3)
|
||
self.assertEqual(t.body[1].value.values[2].lineno, 3)
|
||
self.assertEqual(t.body[1].value.values[3].lineno, 3)
|
||
self.assertEqual(t.body[1].value.values[4].lineno, 3)
|
||
# check the first binop location
|
||
binop = t.body[1].value.values[0].value
|
||
self.assertEqual(type(binop), ast.BinOp)
|
||
self.assertEqual(type(binop.left), ast.Name)
|
||
self.assertEqual(type(binop.op), ast.Mult)
|
||
self.assertEqual(type(binop.right), ast.Call)
|
||
self.assertEqual(binop.lineno, 3)
|
||
self.assertEqual(binop.left.lineno, 3)
|
||
self.assertEqual(binop.right.lineno, 3)
|
||
self.assertEqual(binop.col_offset, 3)
|
||
self.assertEqual(binop.left.col_offset, 3)
|
||
self.assertEqual(binop.right.col_offset, 7)
|
||
# check the second binop location
|
||
binop = t.body[1].value.values[2].value
|
||
self.assertEqual(type(binop), ast.BinOp)
|
||
self.assertEqual(type(binop.left), ast.Name)
|
||
self.assertEqual(type(binop.op), ast.Mult)
|
||
self.assertEqual(type(binop.right), ast.Call)
|
||
self.assertEqual(binop.lineno, 3)
|
||
self.assertEqual(binop.left.lineno, 3)
|
||
self.assertEqual(binop.right.lineno, 3)
|
||
self.assertEqual(binop.col_offset, 13)
|
||
self.assertEqual(binop.left.col_offset, 13)
|
||
self.assertEqual(binop.right.col_offset, 17)
|
||
# check the third binop location
|
||
binop = t.body[1].value.values[4].value
|
||
self.assertEqual(type(binop), ast.BinOp)
|
||
self.assertEqual(type(binop.left), ast.Name)
|
||
self.assertEqual(type(binop.op), ast.Mult)
|
||
self.assertEqual(type(binop.right), ast.Call)
|
||
self.assertEqual(binop.lineno, 3)
|
||
self.assertEqual(binop.left.lineno, 3)
|
||
self.assertEqual(binop.right.lineno, 3)
|
||
self.assertEqual(binop.col_offset, 23)
|
||
self.assertEqual(binop.left.col_offset, 23)
|
||
self.assertEqual(binop.right.col_offset, 27)
|
||
|
||
def test_ast_numbers_fstring_with_formatting(self):
|
||
|
||
t = ast.parse('f"Here is that pesky {xxx:.3f} again"')
|
||
self.assertEqual(len(t.body), 1)
|
||
self.assertEqual(t.body[0].lineno, 1)
|
||
|
||
self.assertEqual(type(t.body[0]), ast.Expr)
|
||
self.assertEqual(type(t.body[0].value), ast.JoinedStr)
|
||
self.assertEqual(len(t.body[0].value.values), 3)
|
||
|
||
self.assertEqual(type(t.body[0].value.values[0]), ast.Constant)
|
||
self.assertEqual(type(t.body[0].value.values[1]), ast.FormattedValue)
|
||
self.assertEqual(type(t.body[0].value.values[2]), ast.Constant)
|
||
|
||
_, expr, _ = t.body[0].value.values
|
||
|
||
name = expr.value
|
||
self.assertEqual(type(name), ast.Name)
|
||
self.assertEqual(name.lineno, 1)
|
||
self.assertEqual(name.end_lineno, 1)
|
||
self.assertEqual(name.col_offset, 22)
|
||
self.assertEqual(name.end_col_offset, 25)
|
||
|
||
def test_ast_line_numbers_multiline_fstring(self):
|
||
# See bpo-30465 for details.
|
||
expr = """
|
||
a = 10
|
||
f'''
|
||
{a
|
||
*
|
||
x()}
|
||
non-important content
|
||
'''
|
||
"""
|
||
t = ast.parse(expr)
|
||
self.assertEqual(type(t), ast.Module)
|
||
self.assertEqual(len(t.body), 2)
|
||
# check `a = 10`
|
||
self.assertEqual(type(t.body[0]), ast.Assign)
|
||
self.assertEqual(t.body[0].lineno, 2)
|
||
# check `f'...'`
|
||
self.assertEqual(type(t.body[1]), ast.Expr)
|
||
self.assertEqual(type(t.body[1].value), ast.JoinedStr)
|
||
self.assertEqual(len(t.body[1].value.values), 3)
|
||
self.assertEqual(type(t.body[1].value.values[0]), ast.Constant)
|
||
self.assertEqual(type(t.body[1].value.values[0].value), str)
|
||
self.assertEqual(type(t.body[1].value.values[1]), ast.FormattedValue)
|
||
self.assertEqual(type(t.body[1].value.values[2]), ast.Constant)
|
||
self.assertEqual(type(t.body[1].value.values[2].value), str)
|
||
self.assertEqual(t.body[1].lineno, 3)
|
||
self.assertEqual(t.body[1].value.lineno, 3)
|
||
self.assertEqual(t.body[1].value.values[0].lineno, 3)
|
||
self.assertEqual(t.body[1].value.values[1].lineno, 4)
|
||
self.assertEqual(t.body[1].value.values[2].lineno, 6)
|
||
self.assertEqual(t.body[1].col_offset, 0)
|
||
self.assertEqual(t.body[1].value.col_offset, 0)
|
||
self.assertEqual(t.body[1].value.values[0].col_offset, 4)
|
||
self.assertEqual(t.body[1].value.values[1].col_offset, 2)
|
||
self.assertEqual(t.body[1].value.values[2].col_offset, 11)
|
||
# NOTE: the following lineno information and col_offset is correct for
|
||
# expressions within FormattedValues.
|
||
binop = t.body[1].value.values[1].value
|
||
self.assertEqual(type(binop), ast.BinOp)
|
||
self.assertEqual(type(binop.left), ast.Name)
|
||
self.assertEqual(type(binop.op), ast.Mult)
|
||
self.assertEqual(type(binop.right), ast.Call)
|
||
self.assertEqual(binop.lineno, 4)
|
||
self.assertEqual(binop.left.lineno, 4)
|
||
self.assertEqual(binop.right.lineno, 6)
|
||
self.assertEqual(binop.col_offset, 3)
|
||
self.assertEqual(binop.left.col_offset, 3)
|
||
self.assertEqual(binop.right.col_offset, 7)
|
||
|
||
expr = """
|
||
a = f'''
|
||
{blech}
|
||
'''
|
||
"""
|
||
t = ast.parse(expr)
|
||
self.assertEqual(type(t), ast.Module)
|
||
self.assertEqual(len(t.body), 1)
|
||
# Check f'...'
|
||
self.assertEqual(type(t.body[0]), ast.Assign)
|
||
self.assertEqual(type(t.body[0].value), ast.JoinedStr)
|
||
self.assertEqual(len(t.body[0].value.values), 3)
|
||
self.assertEqual(type(t.body[0].value.values[1]), ast.FormattedValue)
|
||
self.assertEqual(t.body[0].lineno, 2)
|
||
self.assertEqual(t.body[0].value.lineno, 2)
|
||
self.assertEqual(t.body[0].value.values[0].lineno, 2)
|
||
self.assertEqual(t.body[0].value.values[1].lineno, 3)
|
||
self.assertEqual(t.body[0].value.values[2].lineno, 3)
|
||
self.assertEqual(t.body[0].col_offset, 0)
|
||
self.assertEqual(t.body[0].value.col_offset, 4)
|
||
self.assertEqual(t.body[0].value.values[0].col_offset, 8)
|
||
self.assertEqual(t.body[0].value.values[1].col_offset, 10)
|
||
self.assertEqual(t.body[0].value.values[2].col_offset, 17)
|
||
# Check {blech}
|
||
self.assertEqual(t.body[0].value.values[1].value.lineno, 3)
|
||
self.assertEqual(t.body[0].value.values[1].value.end_lineno, 3)
|
||
self.assertEqual(t.body[0].value.values[1].value.col_offset, 11)
|
||
self.assertEqual(t.body[0].value.values[1].value.end_col_offset, 16)
|
||
|
||
def test_ast_line_numbers_with_parentheses(self):
|
||
expr = """
|
||
x = (
|
||
f" {test(t)}"
|
||
)"""
|
||
t = ast.parse(expr)
|
||
self.assertEqual(type(t), ast.Module)
|
||
self.assertEqual(len(t.body), 1)
|
||
# check the joinedstr location
|
||
joinedstr = t.body[0].value
|
||
self.assertEqual(type(joinedstr), ast.JoinedStr)
|
||
self.assertEqual(joinedstr.lineno, 3)
|
||
self.assertEqual(joinedstr.end_lineno, 3)
|
||
self.assertEqual(joinedstr.col_offset, 4)
|
||
self.assertEqual(joinedstr.end_col_offset, 17)
|
||
# check the formatted value location
|
||
fv = t.body[0].value.values[1]
|
||
self.assertEqual(type(fv), ast.FormattedValue)
|
||
self.assertEqual(fv.lineno, 3)
|
||
self.assertEqual(fv.end_lineno, 3)
|
||
self.assertEqual(fv.col_offset, 7)
|
||
self.assertEqual(fv.end_col_offset, 16)
|
||
# check the test(t) location
|
||
call = t.body[0].value.values[1].value
|
||
self.assertEqual(type(call), ast.Call)
|
||
self.assertEqual(call.lineno, 3)
|
||
self.assertEqual(call.end_lineno, 3)
|
||
self.assertEqual(call.col_offset, 8)
|
||
self.assertEqual(call.end_col_offset, 15)
|
||
|
||
expr = """
|
||
x = (
|
||
u'wat',
|
||
u"wat",
|
||
b'wat',
|
||
b"wat",
|
||
f'wat',
|
||
f"wat",
|
||
)
|
||
|
||
y = (
|
||
u'''wat''',
|
||
u\"\"\"wat\"\"\",
|
||
b'''wat''',
|
||
b\"\"\"wat\"\"\",
|
||
f'''wat''',
|
||
f\"\"\"wat\"\"\",
|
||
)
|
||
"""
|
||
t = ast.parse(expr)
|
||
self.assertEqual(type(t), ast.Module)
|
||
self.assertEqual(len(t.body), 2)
|
||
x, y = t.body
|
||
|
||
# Check the single quoted string offsets first.
|
||
offsets = [
|
||
(elt.col_offset, elt.end_col_offset)
|
||
for elt in x.value.elts
|
||
]
|
||
self.assertTrue(all(
|
||
offset == (4, 10)
|
||
for offset in offsets
|
||
))
|
||
|
||
# Check the triple quoted string offsets.
|
||
offsets = [
|
||
(elt.col_offset, elt.end_col_offset)
|
||
for elt in y.value.elts
|
||
]
|
||
self.assertTrue(all(
|
||
offset == (4, 14)
|
||
for offset in offsets
|
||
))
|
||
|
||
expr = """
|
||
x = (
|
||
'PERL_MM_OPT', (
|
||
f'wat'
|
||
f'some_string={f(x)} '
|
||
f'wat'
|
||
),
|
||
)
|
||
"""
|
||
t = ast.parse(expr)
|
||
self.assertEqual(type(t), ast.Module)
|
||
self.assertEqual(len(t.body), 1)
|
||
# check the fstring
|
||
fstring = t.body[0].value.elts[1]
|
||
self.assertEqual(type(fstring), ast.JoinedStr)
|
||
self.assertEqual(len(fstring.values), 3)
|
||
wat1, middle, wat2 = fstring.values
|
||
# check the first wat
|
||
self.assertEqual(type(wat1), ast.Constant)
|
||
self.assertEqual(wat1.lineno, 4)
|
||
self.assertEqual(wat1.end_lineno, 5)
|
||
self.assertEqual(wat1.col_offset, 14)
|
||
self.assertEqual(wat1.end_col_offset, 26)
|
||
# check the call
|
||
call = middle.value
|
||
self.assertEqual(type(call), ast.Call)
|
||
self.assertEqual(call.lineno, 5)
|
||
self.assertEqual(call.end_lineno, 5)
|
||
self.assertEqual(call.col_offset, 27)
|
||
self.assertEqual(call.end_col_offset, 31)
|
||
# check the second wat
|
||
self.assertEqual(type(wat2), ast.Constant)
|
||
self.assertEqual(wat2.lineno, 5)
|
||
self.assertEqual(wat2.end_lineno, 6)
|
||
self.assertEqual(wat2.col_offset, 32)
|
||
# wat ends at the offset 17, but the whole f-string
|
||
# ends at the offset 18 (since the quote is part of the
|
||
# f-string but not the wat string)
|
||
self.assertEqual(wat2.end_col_offset, 17)
|
||
self.assertEqual(fstring.end_col_offset, 18)
|
||
|
||
def test_ast_fstring_empty_format_spec(self):
|
||
expr = "f'{expr:}'"
|
||
|
||
mod = ast.parse(expr)
|
||
self.assertEqual(type(mod), ast.Module)
|
||
self.assertEqual(len(mod.body), 1)
|
||
|
||
fstring = mod.body[0].value
|
||
self.assertEqual(type(fstring), ast.JoinedStr)
|
||
self.assertEqual(len(fstring.values), 1)
|
||
|
||
fv = fstring.values[0]
|
||
self.assertEqual(type(fv), ast.FormattedValue)
|
||
|
||
format_spec = fv.format_spec
|
||
self.assertEqual(type(format_spec), ast.JoinedStr)
|
||
self.assertEqual(len(format_spec.values), 0)
|
||
|
||
def test_ast_fstring_format_spec(self):
|
||
expr = "f'{1:{name}}'"
|
||
|
||
mod = ast.parse(expr)
|
||
self.assertEqual(type(mod), ast.Module)
|
||
self.assertEqual(len(mod.body), 1)
|
||
|
||
fstring = mod.body[0].value
|
||
self.assertEqual(type(fstring), ast.JoinedStr)
|
||
self.assertEqual(len(fstring.values), 1)
|
||
|
||
fv = fstring.values[0]
|
||
self.assertEqual(type(fv), ast.FormattedValue)
|
||
|
||
format_spec = fv.format_spec
|
||
self.assertEqual(type(format_spec), ast.JoinedStr)
|
||
self.assertEqual(len(format_spec.values), 1)
|
||
|
||
format_spec_value = format_spec.values[0]
|
||
self.assertEqual(type(format_spec_value), ast.FormattedValue)
|
||
self.assertEqual(format_spec_value.value.id, 'name')
|
||
|
||
expr = "f'{1:{name1}{name2}}'"
|
||
|
||
mod = ast.parse(expr)
|
||
self.assertEqual(type(mod), ast.Module)
|
||
self.assertEqual(len(mod.body), 1)
|
||
|
||
fstring = mod.body[0].value
|
||
self.assertEqual(type(fstring), ast.JoinedStr)
|
||
self.assertEqual(len(fstring.values), 1)
|
||
|
||
fv = fstring.values[0]
|
||
self.assertEqual(type(fv), ast.FormattedValue)
|
||
|
||
format_spec = fv.format_spec
|
||
self.assertEqual(type(format_spec), ast.JoinedStr)
|
||
self.assertEqual(len(format_spec.values), 2)
|
||
|
||
format_spec_value = format_spec.values[0]
|
||
self.assertEqual(type(format_spec_value), ast.FormattedValue)
|
||
self.assertEqual(format_spec_value.value.id, 'name1')
|
||
|
||
format_spec_value = format_spec.values[1]
|
||
self.assertEqual(type(format_spec_value), ast.FormattedValue)
|
||
self.assertEqual(format_spec_value.value.id, 'name2')
|
||
|
||
|
||
def test_docstring(self):
|
||
def f():
|
||
f'''Not a docstring'''
|
||
self.assertIsNone(f.__doc__)
|
||
def g():
|
||
'''Not a docstring''' \
|
||
f''
|
||
self.assertIsNone(g.__doc__)
|
||
|
||
def test_literal_eval(self):
|
||
with self.assertRaisesRegex(ValueError, 'malformed node or string'):
|
||
ast.literal_eval("f'x'")
|
||
|
||
def test_ast_compile_time_concat(self):
|
||
x = ['']
|
||
|
||
expr = """x[0] = 'foo' f'{3}'"""
|
||
t = ast.parse(expr)
|
||
c = compile(t, '', 'exec')
|
||
exec(c)
|
||
self.assertEqual(x[0], 'foo3')
|
||
|
||
def test_compile_time_concat_errors(self):
|
||
self.assertAllRaise(SyntaxError,
|
||
'cannot mix bytes and nonbytes literals',
|
||
[r"""f'' b''""",
|
||
r"""b'' f''""",
|
||
])
|
||
|
||
def test_literal(self):
|
||
self.assertEqual(f'', '')
|
||
self.assertEqual(f'a', 'a')
|
||
self.assertEqual(f' ', ' ')
|
||
|
||
def test_unterminated_string(self):
|
||
self.assertAllRaise(SyntaxError, 'unterminated string',
|
||
[r"""f'{"x'""",
|
||
r"""f'{"x}'""",
|
||
r"""f'{("x'""",
|
||
r"""f'{("x}'""",
|
||
])
|
||
|
||
@unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
|
||
def test_mismatched_parens(self):
|
||
self.assertAllRaise(SyntaxError, r"closing parenthesis '\}' "
|
||
r"does not match opening parenthesis '\('",
|
||
["f'{((}'",
|
||
])
|
||
self.assertAllRaise(SyntaxError, r"closing parenthesis '\)' "
|
||
r"does not match opening parenthesis '\['",
|
||
["f'{a[4)}'",
|
||
])
|
||
self.assertAllRaise(SyntaxError, r"closing parenthesis '\]' "
|
||
r"does not match opening parenthesis '\('",
|
||
["f'{a(4]}'",
|
||
])
|
||
self.assertAllRaise(SyntaxError, r"closing parenthesis '\}' "
|
||
r"does not match opening parenthesis '\['",
|
||
["f'{a[4}'",
|
||
])
|
||
self.assertAllRaise(SyntaxError, r"closing parenthesis '\}' "
|
||
r"does not match opening parenthesis '\('",
|
||
["f'{a(4}'",
|
||
])
|
||
self.assertRaises(SyntaxError, eval, "f'{" + "("*20 + "}'")
|
||
|
||
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: No exception raised
|
||
@unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
|
||
def test_fstring_nested_too_deeply(self):
|
||
def raises_syntax_or_memory_error(txt):
|
||
try:
|
||
eval(txt)
|
||
except SyntaxError:
|
||
pass
|
||
except MemoryError:
|
||
pass
|
||
except Exception as ex:
|
||
self.fail(f"Should raise SyntaxError or MemoryError, not {type(ex)}")
|
||
else:
|
||
self.fail("No exception raised")
|
||
|
||
raises_syntax_or_memory_error('f"{1+2:{1+2:{1+1:{1}}}}"')
|
||
|
||
def create_nested_fstring(n):
|
||
if n == 0:
|
||
return "1+1"
|
||
prev = create_nested_fstring(n-1)
|
||
return f'f"{{{prev}}}"'
|
||
|
||
raises_syntax_or_memory_error(create_nested_fstring(160))
|
||
raises_syntax_or_memory_error("f'{" + "("*100 + "}'")
|
||
raises_syntax_or_memory_error("f'{" + "("*1000 + "}'")
|
||
raises_syntax_or_memory_error("f'{" + "("*10_000 + "}'")
|
||
|
||
def test_syntax_error_in_nested_fstring(self):
|
||
# See gh-104016 for more information on this crash
|
||
self.assertAllRaise(SyntaxError,
|
||
"invalid syntax",
|
||
['f"{1 1:' + ('{f"1:' * 199)])
|
||
|
||
def test_double_braces(self):
|
||
self.assertEqual(f'{{', '{')
|
||
self.assertEqual(f'a{{', 'a{')
|
||
self.assertEqual(f'{{b', '{b')
|
||
self.assertEqual(f'a{{b', 'a{b')
|
||
self.assertEqual(f'}}', '}')
|
||
self.assertEqual(f'a}}', 'a}')
|
||
self.assertEqual(f'}}b', '}b')
|
||
self.assertEqual(f'a}}b', 'a}b')
|
||
self.assertEqual(f'{{}}', '{}')
|
||
self.assertEqual(f'a{{}}', 'a{}')
|
||
self.assertEqual(f'{{b}}', '{b}')
|
||
self.assertEqual(f'{{}}c', '{}c')
|
||
self.assertEqual(f'a{{b}}', 'a{b}')
|
||
self.assertEqual(f'a{{}}c', 'a{}c')
|
||
self.assertEqual(f'{{b}}c', '{b}c')
|
||
self.assertEqual(f'a{{b}}c', 'a{b}c')
|
||
|
||
self.assertEqual(f'{{{10}', '{10')
|
||
self.assertEqual(f'}}{10}', '}10')
|
||
self.assertEqual(f'}}{{{10}', '}{10')
|
||
self.assertEqual(f'}}a{{{10}', '}a{10')
|
||
|
||
self.assertEqual(f'{10}{{', '10{')
|
||
self.assertEqual(f'{10}}}', '10}')
|
||
self.assertEqual(f'{10}}}{{', '10}{')
|
||
self.assertEqual(f'{10}}}a{{' '}', '10}a{}')
|
||
|
||
# Inside of strings, don't interpret doubled brackets.
|
||
self.assertEqual(f'{"{{}}"}', '{{}}')
|
||
|
||
self.assertAllRaise(TypeError, 'unhashable type',
|
||
["f'{ {{}} }'", # dict in a set
|
||
])
|
||
|
||
def test_compile_time_concat(self):
|
||
x = 'def'
|
||
self.assertEqual('abc' f'## {x}ghi', 'abc## defghi')
|
||
self.assertEqual('abc' f'{x}' 'ghi', 'abcdefghi')
|
||
self.assertEqual('abc' f'{x}' 'gh' f'i{x:4}', 'abcdefghidef ')
|
||
self.assertEqual('{x}' f'{x}', '{x}def')
|
||
self.assertEqual('{x' f'{x}', '{xdef')
|
||
self.assertEqual('{x}' f'{x}', '{x}def')
|
||
self.assertEqual('{{x}}' f'{x}', '{{x}}def')
|
||
self.assertEqual('{{x' f'{x}', '{{xdef')
|
||
self.assertEqual('x}}' f'{x}', 'x}}def')
|
||
self.assertEqual(f'{x}' 'x}}', 'defx}}')
|
||
self.assertEqual(f'{x}' '', 'def')
|
||
self.assertEqual('' f'{x}' '', 'def')
|
||
self.assertEqual('' f'{x}', 'def')
|
||
self.assertEqual(f'{x}' '2', 'def2')
|
||
self.assertEqual('1' f'{x}' '2', '1def2')
|
||
self.assertEqual('1' f'{x}', '1def')
|
||
self.assertEqual(f'{x}' f'-{x}', 'def-def')
|
||
self.assertEqual('' f'', '')
|
||
self.assertEqual('' f'' '', '')
|
||
self.assertEqual('' f'' '' f'', '')
|
||
self.assertEqual(f'', '')
|
||
self.assertEqual(f'' '', '')
|
||
self.assertEqual(f'' '' f'', '')
|
||
self.assertEqual(f'' '' f'' '', '')
|
||
|
||
# This is not really [f'{'] + [f'}'] since we treat the inside
|
||
# of braces as a purely new context, so it is actually f'{ and
|
||
# then eval(' f') (a valid expression) and then }' which would
|
||
# constitute a valid f-string.
|
||
self.assertEqual(f'{' f'}', ' f')
|
||
|
||
self.assertAllRaise(SyntaxError, "expecting '}'",
|
||
['''f'{3' f"}"''', # can't concat to get a valid f-string
|
||
])
|
||
|
||
def test_comments(self):
|
||
# These aren't comments, since they're in strings.
|
||
d = {'#': 'hash'}
|
||
self.assertEqual(f'{"#"}', '#')
|
||
self.assertEqual(f'{d["#"]}', 'hash')
|
||
|
||
self.assertAllRaise(SyntaxError, "'{' was never closed",
|
||
["f'{1#}'", # error because everything after '#' is a comment
|
||
"f'{#}'",
|
||
"f'one: {1#}'",
|
||
"f'{1# one} {2 this is a comment still#}'",
|
||
])
|
||
self.assertAllRaise(SyntaxError, r"f-string: unmatched '\)'",
|
||
["f'{)#}'", # When wrapped in parens, this becomes
|
||
# '()#)'. Make sure that doesn't compile.
|
||
])
|
||
self.assertEqual(f'''A complex trick: {
|
||
2 # two
|
||
}''', 'A complex trick: 2')
|
||
self.assertEqual(f'''
|
||
{
|
||
40 # forty
|
||
+ # plus
|
||
2 # two
|
||
}''', '\n42')
|
||
self.assertEqual(f'''
|
||
{
|
||
40 # forty
|
||
+ # plus
|
||
2 # two
|
||
}''', '\n42')
|
||
|
||
self.assertEqual(f'''
|
||
# this is not a comment
|
||
{ # the following operation it's
|
||
3 # this is a number
|
||
* 2}''', '\n# this is not a comment\n6')
|
||
self.assertEqual(f'''
|
||
{# f'a {comment}'
|
||
86 # constant
|
||
# nothing more
|
||
}''', '\n86')
|
||
|
||
self.assertAllRaise(SyntaxError, r"f-string: valid expression required before '}'",
|
||
["""f'''
|
||
{
|
||
# only a comment
|
||
}'''
|
||
""", # this is equivalent to f'{}'
|
||
])
|
||
|
||
def test_many_expressions(self):
|
||
# Create a string with many expressions in it. Note that
|
||
# because we have a space in here as a literal, we're actually
|
||
# going to use twice as many ast nodes: one for each literal
|
||
# plus one for each expression.
|
||
def build_fstr(n, extra=''):
|
||
return "f'" + ('{x} ' * n) + extra + "'"
|
||
|
||
x = 'X'
|
||
width = 1
|
||
|
||
# Test around 256.
|
||
for i in range(250, 260):
|
||
self.assertEqual(eval(build_fstr(i)), (x+' ')*i)
|
||
|
||
# Test concatenating 2 largs fstrings.
|
||
self.assertEqual(eval(build_fstr(255)*256), (x+' ')*(255*256))
|
||
|
||
s = build_fstr(253, '{x:{width}} ')
|
||
self.assertEqual(eval(s), (x+' ')*254)
|
||
|
||
# Test lots of expressions and constants, concatenated.
|
||
s = "f'{1}' 'x' 'y'" * 1024
|
||
self.assertEqual(eval(s), '1xy' * 1024)
|
||
|
||
def test_format_specifier_expressions(self):
|
||
width = 10
|
||
precision = 4
|
||
value = decimal.Decimal('12.34567')
|
||
self.assertEqual(f'result: {value:{width}.{precision}}', 'result: 12.35')
|
||
self.assertEqual(f'result: {value:{width!r}.{precision}}', 'result: 12.35')
|
||
self.assertEqual(f'result: {value:{width:0}.{precision:1}}', 'result: 12.35')
|
||
self.assertEqual(f'result: {value:{1}{0:0}.{precision:1}}', 'result: 12.35')
|
||
self.assertEqual(f'result: {value:{ 1}{ 0:0}.{ precision:1}}', 'result: 12.35')
|
||
self.assertEqual(f'{10:#{1}0x}', ' 0xa')
|
||
self.assertEqual(f'{10:{"#"}1{0}{"x"}}', ' 0xa')
|
||
self.assertEqual(f'{-10:-{"#"}1{0}x}', ' -0xa')
|
||
self.assertEqual(f'{-10:{"-"}#{1}0{"x"}}', ' -0xa')
|
||
self.assertEqual(f'{10:#{3 != {4:5} and width}x}', ' 0xa')
|
||
self.assertEqual(f'result: {value:{width:{0}}.{precision:1}}', 'result: 12.35')
|
||
|
||
self.assertAllRaise(SyntaxError, "f-string: expecting ':' or '}'",
|
||
["""f'{"s"!r{":10"}}'""",
|
||
# This looks like a nested format spec.
|
||
])
|
||
|
||
self.assertAllRaise(SyntaxError,
|
||
"f-string: expecting a valid expression after '{'",
|
||
[# Invalid syntax inside a nested spec.
|
||
"f'{4:{/5}}'",
|
||
])
|
||
|
||
self.assertAllRaise(SyntaxError, 'f-string: invalid conversion character',
|
||
[# No expansion inside conversion or for
|
||
# the : or ! itself.
|
||
"""f'{"s"!{"r"}}'""",
|
||
])
|
||
|
||
def test_custom_format_specifier(self):
|
||
class CustomFormat:
|
||
def __format__(self, format_spec):
|
||
return format_spec
|
||
|
||
self.assertEqual(f'{CustomFormat():\n}', '\n')
|
||
self.assertEqual(f'{CustomFormat():\u2603}', '☃')
|
||
with self.assertWarns(SyntaxWarning):
|
||
exec(r'f"{F():¯\_(ツ)_/¯}"', {'F': CustomFormat})
|
||
|
||
def test_side_effect_order(self):
|
||
class X:
|
||
def __init__(self):
|
||
self.i = 0
|
||
def __format__(self, spec):
|
||
self.i += 1
|
||
return str(self.i)
|
||
|
||
x = X()
|
||
self.assertEqual(f'{x} {x}', '1 2')
|
||
|
||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||
def test_missing_expression(self):
|
||
self.assertAllRaise(SyntaxError,
|
||
"f-string: valid expression required before '}'",
|
||
["f'{}'",
|
||
"f'{ }'"
|
||
"f' {} '",
|
||
"f'{10:{ }}'",
|
||
"f' { } '",
|
||
|
||
# The Python parser ignores also the following
|
||
# whitespace characters in additional to a space.
|
||
"f'''{\t\f\r\n}'''",
|
||
])
|
||
|
||
self.assertAllRaise(SyntaxError,
|
||
"f-string: valid expression required before '!'",
|
||
["f'{!r}'",
|
||
"f'{ !r}'",
|
||
"f'{!}'",
|
||
"f'''{\t\f\r\n!a}'''",
|
||
|
||
# Catch empty expression before the
|
||
# missing closing brace.
|
||
"f'{!'",
|
||
"f'{!s:'",
|
||
|
||
# Catch empty expression before the
|
||
# invalid conversion.
|
||
"f'{!x}'",
|
||
"f'{ !xr}'",
|
||
"f'{!x:}'",
|
||
"f'{!x:a}'",
|
||
"f'{ !xr:}'",
|
||
"f'{ !xr:a}'",
|
||
])
|
||
|
||
self.assertAllRaise(SyntaxError,
|
||
"f-string: valid expression required before ':'",
|
||
["f'{:}'",
|
||
"f'{ :!}'",
|
||
"f'{:2}'",
|
||
"f'''{\t\f\r\n:a}'''",
|
||
"f'{:'",
|
||
"F'{[F'{:'}[F'{:'}]]]",
|
||
])
|
||
|
||
self.assertAllRaise(SyntaxError,
|
||
"f-string: valid expression required before '='",
|
||
["f'{=}'",
|
||
"f'{ =}'",
|
||
"f'{ =:}'",
|
||
"f'{ =!}'",
|
||
"f'''{\t\f\r\n=}'''",
|
||
"f'{='",
|
||
])
|
||
|
||
# Different error message is raised for other whitespace characters.
|
||
self.assertAllRaise(SyntaxError, r"invalid non-printable character U\+00A0",
|
||
["f'''{\xa0}'''",
|
||
"\xa0",
|
||
])
|
||
|
||
def test_parens_in_expressions(self):
|
||
self.assertEqual(f'{3,}', '(3,)')
|
||
|
||
self.assertAllRaise(SyntaxError,
|
||
"f-string: expecting a valid expression after '{'",
|
||
["f'{,}'",
|
||
])
|
||
|
||
self.assertAllRaise(SyntaxError, r"f-string: unmatched '\)'",
|
||
["f'{3)+(4}'",
|
||
])
|
||
|
||
def test_newlines_before_syntax_error(self):
|
||
self.assertAllRaise(SyntaxError,
|
||
"f-string: expecting a valid expression after '{'",
|
||
["f'{.}'", "\nf'{.}'", "\n\nf'{.}'"])
|
||
|
||
def test_backslashes_in_string_part(self):
|
||
self.assertEqual(f'\t', '\t')
|
||
self.assertEqual(r'\t', '\\t')
|
||
self.assertEqual(rf'\t', '\\t')
|
||
self.assertEqual(f'{2}\t', '2\t')
|
||
self.assertEqual(f'{2}\t{3}', '2\t3')
|
||
self.assertEqual(f'\t{3}', '\t3')
|
||
|
||
self.assertEqual(f'\u0394', '\u0394')
|
||
self.assertEqual(r'\u0394', '\\u0394')
|
||
self.assertEqual(rf'\u0394', '\\u0394')
|
||
self.assertEqual(f'{2}\u0394', '2\u0394')
|
||
self.assertEqual(f'{2}\u0394{3}', '2\u03943')
|
||
self.assertEqual(f'\u0394{3}', '\u03943')
|
||
|
||
self.assertEqual(f'\U00000394', '\u0394')
|
||
self.assertEqual(r'\U00000394', '\\U00000394')
|
||
self.assertEqual(rf'\U00000394', '\\U00000394')
|
||
self.assertEqual(f'{2}\U00000394', '2\u0394')
|
||
self.assertEqual(f'{2}\U00000394{3}', '2\u03943')
|
||
self.assertEqual(f'\U00000394{3}', '\u03943')
|
||
|
||
self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}', '\u0394')
|
||
self.assertEqual(f'{2}\N{GREEK CAPITAL LETTER DELTA}', '2\u0394')
|
||
self.assertEqual(f'{2}\N{GREEK CAPITAL LETTER DELTA}{3}', '2\u03943')
|
||
self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}{3}', '\u03943')
|
||
self.assertEqual(f'2\N{GREEK CAPITAL LETTER DELTA}', '2\u0394')
|
||
self.assertEqual(f'2\N{GREEK CAPITAL LETTER DELTA}3', '2\u03943')
|
||
self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}3', '\u03943')
|
||
|
||
self.assertEqual(f'\x20', ' ')
|
||
self.assertEqual(r'\x20', '\\x20')
|
||
self.assertEqual(rf'\x20', '\\x20')
|
||
self.assertEqual(f'{2}\x20', '2 ')
|
||
self.assertEqual(f'{2}\x20{3}', '2 3')
|
||
self.assertEqual(f'\x20{3}', ' 3')
|
||
|
||
self.assertEqual(f'2\x20', '2 ')
|
||
self.assertEqual(f'2\x203', '2 3')
|
||
self.assertEqual(f'\x203', ' 3')
|
||
|
||
with self.assertWarns(SyntaxWarning): # invalid escape sequence
|
||
value = eval(r"f'\{6*7}'")
|
||
self.assertEqual(value, '\\42')
|
||
with self.assertWarns(SyntaxWarning): # invalid escape sequence
|
||
value = eval(r"f'\g'")
|
||
self.assertEqual(value, '\\g')
|
||
self.assertEqual(f'\\{6*7}', '\\42')
|
||
self.assertEqual(fr'\{6*7}', '\\42')
|
||
|
||
AMPERSAND = 'spam'
|
||
# Get the right unicode character (&), or pick up local variable
|
||
# depending on the number of backslashes.
|
||
self.assertEqual(f'\N{AMPERSAND}', '&')
|
||
self.assertEqual(f'\\N{AMPERSAND}', '\\Nspam')
|
||
self.assertEqual(fr'\N{AMPERSAND}', '\\Nspam')
|
||
self.assertEqual(f'\\\N{AMPERSAND}', '\\&')
|
||
|
||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||
def test_misformed_unicode_character_name(self):
|
||
# These test are needed because unicode names are parsed
|
||
# differently inside f-strings.
|
||
self.assertAllRaise(SyntaxError, r"\(unicode error\) 'unicodeescape' codec can't decode bytes in position .*: malformed \\N character escape",
|
||
[r"f'\N'",
|
||
r"f'\N '",
|
||
r"f'\N '", # See bpo-46503.
|
||
r"f'\N{'",
|
||
r"f'\N{GREEK CAPITAL LETTER DELTA'",
|
||
|
||
# Here are the non-f-string versions,
|
||
# which should give the same errors.
|
||
r"'\N'",
|
||
r"'\N '",
|
||
r"'\N '",
|
||
r"'\N{'",
|
||
r"'\N{GREEK CAPITAL LETTER DELTA'",
|
||
])
|
||
|
||
def test_backslashes_in_expression_part(self):
|
||
self.assertEqual(f"{(
|
||
1 +
|
||
2
|
||
)}", "3")
|
||
|
||
self.assertEqual("\N{LEFT CURLY BRACKET}", '{')
|
||
self.assertEqual(f'{"\N{LEFT CURLY BRACKET}"}', '{')
|
||
self.assertEqual(rf'{"\N{LEFT CURLY BRACKET}"}', '{')
|
||
|
||
self.assertAllRaise(SyntaxError,
|
||
"f-string: valid expression required before '}'",
|
||
["f'{\n}'",
|
||
])
|
||
|
||
def test_invalid_backslashes_inside_fstring_context(self):
|
||
# All of these variations are invalid python syntax,
|
||
# so they are also invalid in f-strings as well.
|
||
cases = [
|
||
formatting.format(expr=expr)
|
||
for formatting in [
|
||
"{expr}",
|
||
"f'{{{expr}}}'",
|
||
"rf'{{{expr}}}'",
|
||
]
|
||
for expr in [
|
||
r"\'a\'",
|
||
r"\t3",
|
||
r"\\"[0],
|
||
]
|
||
]
|
||
self.assertAllRaise(SyntaxError, 'unexpected character after line continuation',
|
||
cases)
|
||
|
||
def test_no_escapes_for_braces(self):
|
||
"""
|
||
Only literal curly braces begin an expression.
|
||
"""
|
||
# \x7b is '{'.
|
||
self.assertEqual(f'\x7b1+1}}', '{1+1}')
|
||
self.assertEqual(f'\x7b1+1', '{1+1')
|
||
self.assertEqual(f'\u007b1+1', '{1+1')
|
||
self.assertEqual(f'\N{LEFT CURLY BRACKET}1+1\N{RIGHT CURLY BRACKET}', '{1+1}')
|
||
|
||
def test_newlines_in_expressions(self):
|
||
self.assertEqual(f'{0}', '0')
|
||
self.assertEqual(rf'''{3+
|
||
4}''', '7')
|
||
|
||
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "f-string: expecting a valid expression after '{'" does not match "invalid syntax (<string>, line 1)"
|
||
def test_lambda(self):
|
||
x = 5
|
||
self.assertEqual(f'{(lambda y:x*y)("8")!r}', "'88888'")
|
||
self.assertEqual(f'{(lambda y:x*y)("8")!r:10}', "'88888' ")
|
||
self.assertEqual(f'{(lambda y:x*y)("8"):10}', "88888 ")
|
||
|
||
# lambda doesn't work without parens, because the colon
|
||
# makes the parser think it's a format_spec
|
||
# emit warning if we can match a format_spec
|
||
self.assertAllRaise(SyntaxError,
|
||
"f-string: lambda expressions are not allowed "
|
||
"without parentheses",
|
||
["f'{lambda x:x}'",
|
||
"f'{lambda :x}'",
|
||
"f'{lambda *arg, :x}'",
|
||
"f'{1, lambda:x}'",
|
||
"f'{lambda x:}'",
|
||
"f'{lambda :}'",
|
||
])
|
||
# Ensure the detection of invalid lambdas doesn't trigger detection
|
||
# for valid lambdas in the second error pass
|
||
with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
|
||
compile("lambda name_3=f'{name_4}': {name_3}\n1 $ 1", "<string>", "exec")
|
||
|
||
# but don't emit the paren warning in general cases
|
||
with self.assertRaisesRegex(SyntaxError, "f-string: expecting a valid expression after '{'"):
|
||
eval("f'{+ lambda:None}'")
|
||
|
||
def test_valid_prefixes(self):
|
||
self.assertEqual(F'{1}', "1")
|
||
self.assertEqual(FR'{2}', "2")
|
||
self.assertEqual(fR'{3}', "3")
|
||
|
||
def test_roundtrip_raw_quotes(self):
|
||
self.assertEqual(fr"\'", "\\'")
|
||
self.assertEqual(fr'\"', '\\"')
|
||
self.assertEqual(fr'\"\'', '\\"\\\'')
|
||
self.assertEqual(fr'\'\"', '\\\'\\"')
|
||
self.assertEqual(fr'\"\'\"', '\\"\\\'\\"')
|
||
self.assertEqual(fr'\'\"\'', '\\\'\\"\\\'')
|
||
self.assertEqual(fr'\"\'\"\'', '\\"\\\'\\"\\\'')
|
||
|
||
def test_fstring_backslash_before_double_bracket(self):
|
||
deprecated_cases = [
|
||
(r"f'\{{\}}'", '\\{\\}'),
|
||
(r"f'\{{'", '\\{'),
|
||
(r"f'\{{{1+1}'", '\\{2'),
|
||
(r"f'\}}{1+1}'", '\\}2'),
|
||
(r"f'{1+1}\}}'", '2\\}')
|
||
]
|
||
for case, expected_result in deprecated_cases:
|
||
with self.subTest(case=case, expected_result=expected_result):
|
||
with self.assertWarns(SyntaxWarning):
|
||
result = eval(case)
|
||
self.assertEqual(result, expected_result)
|
||
self.assertEqual(fr'\{{\}}', '\\{\\}')
|
||
self.assertEqual(fr'\{{', '\\{')
|
||
self.assertEqual(fr'\{{{1+1}', '\\{2')
|
||
self.assertEqual(fr'\}}{1+1}', '\\}2')
|
||
self.assertEqual(fr'{1+1}\}}', '2\\}')
|
||
|
||
def test_fstring_backslash_before_double_bracket_warns_once(self):
|
||
with self.assertWarns(SyntaxWarning) as w:
|
||
eval(r"f'\{{'")
|
||
self.assertEqual(len(w.warnings), 1)
|
||
self.assertEqual(w.warnings[0].category, SyntaxWarning)
|
||
|
||
def test_fstring_backslash_prefix_raw(self):
|
||
self.assertEqual(f'\\', '\\')
|
||
self.assertEqual(f'\\\\', '\\\\')
|
||
self.assertEqual(fr'\\', r'\\')
|
||
self.assertEqual(fr'\\\\', r'\\\\')
|
||
self.assertEqual(rf'\\', r'\\')
|
||
self.assertEqual(rf'\\\\', r'\\\\')
|
||
self.assertEqual(Rf'\\', R'\\')
|
||
self.assertEqual(Rf'\\\\', R'\\\\')
|
||
self.assertEqual(fR'\\', R'\\')
|
||
self.assertEqual(fR'\\\\', R'\\\\')
|
||
self.assertEqual(FR'\\', R'\\')
|
||
self.assertEqual(FR'\\\\', R'\\\\')
|
||
|
||
def test_fstring_format_spec_greedy_matching(self):
|
||
self.assertEqual(f"{1:}}}", "1}")
|
||
self.assertEqual(f"{1:>3{5}}}}", " 1}")
|
||
|
||
def test_yield(self):
|
||
# Not terribly useful, but make sure the yield turns
|
||
# a function into a generator
|
||
def fn(y):
|
||
f'y:{yield y*2}'
|
||
f'{yield}'
|
||
|
||
g = fn(4)
|
||
self.assertEqual(next(g), 8)
|
||
self.assertEqual(next(g), None)
|
||
|
||
def test_yield_send(self):
|
||
def fn(x):
|
||
yield f'x:{yield (lambda i: x * i)}'
|
||
|
||
g = fn(10)
|
||
the_lambda = next(g)
|
||
self.assertEqual(the_lambda(4), 40)
|
||
self.assertEqual(g.send('string'), 'x:string')
|
||
|
||
def test_expressions_with_triple_quoted_strings(self):
|
||
self.assertEqual(f"{'''x'''}", 'x')
|
||
self.assertEqual(f"{'''eric's'''}", "eric's")
|
||
|
||
# Test concatenation within an expression
|
||
self.assertEqual(f'{"x" """eric"s""" "y"}', 'xeric"sy')
|
||
self.assertEqual(f'{"x" """eric"s"""}', 'xeric"s')
|
||
self.assertEqual(f'{"""eric"s""" "y"}', 'eric"sy')
|
||
self.assertEqual(f'{"""x""" """eric"s""" "y"}', 'xeric"sy')
|
||
self.assertEqual(f'{"""x""" """eric"s""" """y"""}', 'xeric"sy')
|
||
self.assertEqual(f'{r"""x""" """eric"s""" """y"""}', 'xeric"sy')
|
||
|
||
def test_multiple_vars(self):
|
||
x = 98
|
||
y = 'abc'
|
||
self.assertEqual(f'{x}{y}', '98abc')
|
||
|
||
self.assertEqual(f'X{x}{y}', 'X98abc')
|
||
self.assertEqual(f'{x}X{y}', '98Xabc')
|
||
self.assertEqual(f'{x}{y}X', '98abcX')
|
||
|
||
self.assertEqual(f'X{x}Y{y}', 'X98Yabc')
|
||
self.assertEqual(f'X{x}{y}Y', 'X98abcY')
|
||
self.assertEqual(f'{x}X{y}Y', '98XabcY')
|
||
|
||
self.assertEqual(f'X{x}Y{y}Z', 'X98YabcZ')
|
||
|
||
def test_closure(self):
|
||
def outer(x):
|
||
def inner():
|
||
return f'x:{x}'
|
||
return inner
|
||
|
||
self.assertEqual(outer('987')(), 'x:987')
|
||
self.assertEqual(outer(7)(), 'x:7')
|
||
|
||
def test_arguments(self):
|
||
y = 2
|
||
def f(x, width):
|
||
return f'x={x*y:{width}}'
|
||
|
||
self.assertEqual(f('foo', 10), 'x=foofoo ')
|
||
x = 'bar'
|
||
self.assertEqual(f(10, 10), 'x= 20')
|
||
|
||
def test_locals(self):
|
||
value = 123
|
||
self.assertEqual(f'v:{value}', 'v:123')
|
||
|
||
def test_missing_variable(self):
|
||
with self.assertRaises(NameError):
|
||
f'v:{value}'
|
||
|
||
def test_missing_format_spec(self):
|
||
class O:
|
||
def __format__(self, spec):
|
||
if not spec:
|
||
return '*'
|
||
return spec
|
||
|
||
self.assertEqual(f'{O():x}', 'x')
|
||
self.assertEqual(f'{O()}', '*')
|
||
self.assertEqual(f'{O():}', '*')
|
||
|
||
self.assertEqual(f'{3:}', '3')
|
||
self.assertEqual(f'{3!s:}', '3')
|
||
|
||
def test_global(self):
|
||
self.assertEqual(f'g:{a_global}', 'g:global variable')
|
||
self.assertEqual(f'g:{a_global!r}', "g:'global variable'")
|
||
|
||
a_local = 'local variable'
|
||
self.assertEqual(f'g:{a_global} l:{a_local}',
|
||
'g:global variable l:local variable')
|
||
self.assertEqual(f'g:{a_global!r}',
|
||
"g:'global variable'")
|
||
self.assertEqual(f'g:{a_global} l:{a_local!r}',
|
||
"g:global variable l:'local variable'")
|
||
|
||
self.assertIn("module 'unittest' from", f'{unittest}')
|
||
|
||
def test_shadowed_global(self):
|
||
a_global = 'really a local'
|
||
self.assertEqual(f'g:{a_global}', 'g:really a local')
|
||
self.assertEqual(f'g:{a_global!r}', "g:'really a local'")
|
||
|
||
a_local = 'local variable'
|
||
self.assertEqual(f'g:{a_global} l:{a_local}',
|
||
'g:really a local l:local variable')
|
||
self.assertEqual(f'g:{a_global!r}',
|
||
"g:'really a local'")
|
||
self.assertEqual(f'g:{a_global} l:{a_local!r}',
|
||
"g:really a local l:'local variable'")
|
||
|
||
def test_call(self):
|
||
def foo(x):
|
||
return 'x=' + str(x)
|
||
|
||
self.assertEqual(f'{foo(10)}', 'x=10')
|
||
|
||
def test_nested_fstrings(self):
|
||
y = 5
|
||
self.assertEqual(f'{f"{0}"*3}', '000')
|
||
self.assertEqual(f'{f"{y}"*3}', '555')
|
||
|
||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||
def test_invalid_string_prefixes(self):
|
||
single_quote_cases = ["fu''",
|
||
"uf''",
|
||
"Fu''",
|
||
"fU''",
|
||
"Uf''",
|
||
"uF''",
|
||
"ufr''",
|
||
"urf''",
|
||
"fur''",
|
||
"fru''",
|
||
"rfu''",
|
||
"ruf''",
|
||
"FUR''",
|
||
"Fur''",
|
||
"fb''",
|
||
"fB''",
|
||
"Fb''",
|
||
"FB''",
|
||
"bf''",
|
||
"bF''",
|
||
"Bf''",
|
||
"BF''",]
|
||
double_quote_cases = [case.replace("'", '"') for case in single_quote_cases]
|
||
self.assertAllRaise(SyntaxError, 'prefixes are incompatible',
|
||
single_quote_cases + double_quote_cases)
|
||
|
||
def test_leading_trailing_spaces(self):
|
||
self.assertEqual(f'{ 3}', '3')
|
||
self.assertEqual(f'{ 3}', '3')
|
||
self.assertEqual(f'{3 }', '3')
|
||
self.assertEqual(f'{3 }', '3')
|
||
|
||
self.assertEqual(f'expr={ {x: y for x, y in [(1, 2), ]}}',
|
||
'expr={1: 2}')
|
||
self.assertEqual(f'expr={ {x: y for x, y in [(1, 2), ]} }',
|
||
'expr={1: 2}')
|
||
|
||
def test_not_equal(self):
|
||
# There's a special test for this because there's a special
|
||
# case in the f-string parser to look for != as not ending an
|
||
# expression. Normally it would, while looking for !s or !r.
|
||
|
||
self.assertEqual(f'{3!=4}', 'True')
|
||
self.assertEqual(f'{3!=4:}', 'True')
|
||
self.assertEqual(f'{3!=4!s}', 'True')
|
||
self.assertEqual(f'{3!=4!s:.3}', 'Tru')
|
||
|
||
def test_equal_equal(self):
|
||
# Because an expression ending in = has special meaning,
|
||
# there's a special test for ==. Make sure it works.
|
||
|
||
self.assertEqual(f'{0==1}', 'False')
|
||
|
||
def test_conversions(self):
|
||
self.assertEqual(f'{3.14:10.10}', ' 3.14')
|
||
self.assertEqual(f'{1.25!s:10.10}', '1.25 ')
|
||
self.assertEqual(f'{1.25!r:10.10}', '1.25 ')
|
||
self.assertEqual(f'{1.25!a:10.10}', '1.25 ')
|
||
|
||
self.assertEqual(f'{"a"}', 'a')
|
||
self.assertEqual(f'{"a"!r}', "'a'")
|
||
self.assertEqual(f'{"a"!a}', "'a'")
|
||
|
||
# Conversions can have trailing whitespace after them since it
|
||
# does not provide any significance
|
||
self.assertEqual(f"{3!s }", "3")
|
||
self.assertEqual(f'{1.25!s :10.10}', '1.25 ')
|
||
|
||
# Not a conversion.
|
||
self.assertEqual(f'{"a!r"}', "a!r")
|
||
|
||
# Not a conversion, but show that ! is allowed in a format spec.
|
||
self.assertEqual(f'{3.14:!<10.10}', '3.14!!!!!!')
|
||
|
||
self.assertAllRaise(SyntaxError, "f-string: expecting '}'",
|
||
["f'{3!'",
|
||
"f'{3!s'",
|
||
])
|
||
|
||
self.assertAllRaise(SyntaxError, 'f-string: missing conversion character',
|
||
["f'{3!}'",
|
||
"f'{3!:'",
|
||
"f'{3!:}'",
|
||
])
|
||
|
||
for conv_identifier in 'g', 'A', 'G', 'ä', 'ɐ':
|
||
self.assertAllRaise(SyntaxError,
|
||
"f-string: invalid conversion character %r: "
|
||
"expected 's', 'r', or 'a'" % conv_identifier,
|
||
["f'{3!" + conv_identifier + "}'"])
|
||
|
||
for conv_non_identifier in '3', '!':
|
||
self.assertAllRaise(SyntaxError,
|
||
"f-string: invalid conversion character",
|
||
["f'{3!" + conv_non_identifier + "}'"])
|
||
|
||
for conv in ' s', ' s ':
|
||
self.assertAllRaise(SyntaxError,
|
||
"f-string: conversion type must come right after the"
|
||
" exclamation mark",
|
||
["f'{3!" + conv + "}'"])
|
||
|
||
self.assertAllRaise(SyntaxError,
|
||
"f-string: invalid conversion character 'ss': "
|
||
"expected 's', 'r', or 'a'",
|
||
["f'{3!ss}'",
|
||
"f'{3!ss:}'",
|
||
"f'{3!ss:s}'",
|
||
])
|
||
|
||
def test_assignment(self):
|
||
self.assertAllRaise(SyntaxError, r'invalid syntax',
|
||
["f'' = 3",
|
||
"f'{0}' = x",
|
||
"f'{x}' = x",
|
||
])
|
||
|
||
def test_del(self):
|
||
self.assertAllRaise(SyntaxError, 'invalid syntax',
|
||
["del f''",
|
||
"del '' f''",
|
||
])
|
||
|
||
def test_mismatched_braces(self):
|
||
self.assertAllRaise(SyntaxError, "f-string: single '}' is not allowed",
|
||
["f'{{}'",
|
||
"f'{{}}}'",
|
||
"f'}'",
|
||
"f'x}'",
|
||
"f'x}x'",
|
||
r"f'\u007b}'",
|
||
|
||
# Can't have { or } in a format spec.
|
||
"f'{3:}>10}'",
|
||
"f'{3:}}>10}'",
|
||
])
|
||
|
||
self.assertAllRaise(SyntaxError, "f-string: expecting '}'",
|
||
["f'{3'",
|
||
"f'{3!'",
|
||
"f'{3:'",
|
||
"f'{3!s'",
|
||
"f'{3!s:'",
|
||
"f'{3!s:3'",
|
||
"f'x{'",
|
||
"f'x{x'",
|
||
"f'{x'",
|
||
"f'{3:s'",
|
||
"f'{{{'",
|
||
"f'{{}}{'",
|
||
"f'{'",
|
||
"f'{i='", # See gh-93418.
|
||
])
|
||
|
||
self.assertAllRaise(SyntaxError,
|
||
"f-string: expecting a valid expression after '{'",
|
||
["f'{3:{{>10}'",
|
||
])
|
||
|
||
# But these are just normal strings.
|
||
self.assertEqual(f'{"{"}', '{')
|
||
self.assertEqual(f'{"}"}', '}')
|
||
self.assertEqual(f'{3:{"}"}>10}', '}}}}}}}}}3')
|
||
self.assertEqual(f'{2:{"{"}>10}', '{{{{{{{{{2')
|
||
|
||
def test_if_conditional(self):
|
||
# There's special logic in compile.c to test if the
|
||
# conditional for an if (and while) are constants. Exercise
|
||
# that code.
|
||
|
||
def test_fstring(x, expected):
|
||
flag = 0
|
||
if f'{x}':
|
||
flag = 1
|
||
else:
|
||
flag = 2
|
||
self.assertEqual(flag, expected)
|
||
|
||
def test_concat_empty(x, expected):
|
||
flag = 0
|
||
if '' f'{x}':
|
||
flag = 1
|
||
else:
|
||
flag = 2
|
||
self.assertEqual(flag, expected)
|
||
|
||
def test_concat_non_empty(x, expected):
|
||
flag = 0
|
||
if ' ' f'{x}':
|
||
flag = 1
|
||
else:
|
||
flag = 2
|
||
self.assertEqual(flag, expected)
|
||
|
||
test_fstring('', 2)
|
||
test_fstring(' ', 1)
|
||
|
||
test_concat_empty('', 2)
|
||
test_concat_empty(' ', 1)
|
||
|
||
test_concat_non_empty('', 1)
|
||
test_concat_non_empty(' ', 1)
|
||
|
||
def test_empty_format_specifier(self):
|
||
x = 'test'
|
||
self.assertEqual(f'{x}', 'test')
|
||
self.assertEqual(f'{x:}', 'test')
|
||
self.assertEqual(f'{x!s:}', 'test')
|
||
self.assertEqual(f'{x!r:}', "'test'")
|
||
|
||
def test_str_format_differences(self):
|
||
d = {'a': 'string',
|
||
0: 'integer',
|
||
}
|
||
a = 0
|
||
self.assertEqual(f'{d[0]}', 'integer')
|
||
self.assertEqual(f'{d["a"]}', 'string')
|
||
self.assertEqual(f'{d[a]}', 'integer')
|
||
self.assertEqual('{d[a]}'.format(d=d), 'string')
|
||
self.assertEqual('{d[0]}'.format(d=d), 'integer')
|
||
|
||
def test_errors(self):
|
||
# see issue 26287
|
||
self.assertAllRaise(TypeError, 'unsupported',
|
||
[r"f'{(lambda: 0):x}'",
|
||
r"f'{(0,):x}'",
|
||
])
|
||
self.assertAllRaise(ValueError, 'Unknown format code',
|
||
[r"f'{1000:j}'",
|
||
r"f'{1000:j}'",
|
||
])
|
||
|
||
def test_filename_in_syntaxerror(self):
|
||
# see issue 38964
|
||
with temp_cwd() as cwd:
|
||
file_path = os.path.join(cwd, 't.py')
|
||
with open(file_path, 'w', encoding="utf-8") as f:
|
||
f.write('f"{a b}"') # This generates a SyntaxError
|
||
_, _, stderr = assert_python_failure(file_path,
|
||
PYTHONIOENCODING='ascii')
|
||
self.assertIn(file_path.encode('ascii', 'backslashreplace'), stderr)
|
||
|
||
def test_loop(self):
|
||
for i in range(1000):
|
||
self.assertEqual(f'i:{i}', 'i:' + str(i))
|
||
|
||
def test_dict(self):
|
||
d = {'"': 'dquote',
|
||
"'": 'squote',
|
||
'foo': 'bar',
|
||
}
|
||
self.assertEqual(f'''{d["'"]}''', 'squote')
|
||
self.assertEqual(f"""{d['"']}""", 'dquote')
|
||
|
||
self.assertEqual(f'{d["foo"]}', 'bar')
|
||
self.assertEqual(f"{d['foo']}", 'bar')
|
||
|
||
def test_backslash_char(self):
|
||
# Check eval of a backslash followed by a control char.
|
||
# See bpo-30682: this used to raise an assert in pydebug mode.
|
||
self.assertEqual(eval('f"\\\n"'), '')
|
||
self.assertEqual(eval('f"\\\r"'), '')
|
||
|
||
def test_debug_conversion(self):
|
||
x = 'A string'
|
||
self.assertEqual(f'{x=}', 'x=' + repr(x))
|
||
self.assertEqual(f'{x =}', 'x =' + repr(x))
|
||
self.assertEqual(f'{x=!s}', 'x=' + str(x))
|
||
self.assertEqual(f'{x=!r}', 'x=' + repr(x))
|
||
self.assertEqual(f'{x=!a}', 'x=' + ascii(x))
|
||
|
||
x = 2.71828
|
||
self.assertEqual(f'{x=:.2f}', 'x=' + format(x, '.2f'))
|
||
self.assertEqual(f'{x=:}', 'x=' + format(x, ''))
|
||
self.assertEqual(f'{x=!r:^20}', 'x=' + format(repr(x), '^20'))
|
||
self.assertEqual(f'{x=!s:^20}', 'x=' + format(str(x), '^20'))
|
||
self.assertEqual(f'{x=!a:^20}', 'x=' + format(ascii(x), '^20'))
|
||
|
||
x = 9
|
||
self.assertEqual(f'{3*x+15=}', '3*x+15=42')
|
||
|
||
# There is code in ast.c that deals with non-ascii expression values. So,
|
||
# use a unicode identifier to trigger that.
|
||
tenπ = 31.4
|
||
self.assertEqual(f'{tenπ=:.2f}', 'tenπ=31.40')
|
||
|
||
# Also test with Unicode in non-identifiers.
|
||
self.assertEqual(f'{"Σ"=}', '"Σ"=\'Σ\'')
|
||
|
||
# Make sure nested fstrings still work.
|
||
self.assertEqual(f'{f"{3.1415=:.1f}":*^20}', '*****3.1415=3.1*****')
|
||
|
||
# Make sure text before and after an expression with = works
|
||
# correctly.
|
||
pi = 'π'
|
||
self.assertEqual(f'alpha α {pi=} ω omega', "alpha α pi='π' ω omega")
|
||
|
||
# Check multi-line expressions.
|
||
self.assertEqual(f'''{
|
||
3
|
||
=}''', '\n3\n=3')
|
||
|
||
# Since = is handled specially, make sure all existing uses of
|
||
# it still work.
|
||
|
||
self.assertEqual(f'{0==1}', 'False')
|
||
self.assertEqual(f'{0!=1}', 'True')
|
||
self.assertEqual(f'{0<=1}', 'True')
|
||
self.assertEqual(f'{0>=1}', 'False')
|
||
self.assertEqual(f'{(x:="5")}', '5')
|
||
self.assertEqual(x, '5')
|
||
self.assertEqual(f'{(x:=5)}', '5')
|
||
self.assertEqual(x, 5)
|
||
self.assertEqual(f'{"="}', '=')
|
||
|
||
x = 20
|
||
# This isn't an assignment expression, it's 'x', with a format
|
||
# spec of '=10'. See test_walrus: you need to use parens.
|
||
self.assertEqual(f'{x:=10}', ' 20')
|
||
|
||
# Test named function parameters, to make sure '=' parsing works
|
||
# there.
|
||
def f(a):
|
||
nonlocal x
|
||
oldx = x
|
||
x = a
|
||
return oldx
|
||
x = 0
|
||
self.assertEqual(f'{f(a="3=")}', '0')
|
||
self.assertEqual(x, '3=')
|
||
self.assertEqual(f'{f(a=4)}', '3=')
|
||
self.assertEqual(x, 4)
|
||
|
||
# Check debug expressions in format spec
|
||
y = 20
|
||
self.assertEqual(f"{2:{y=}}", "yyyyyyyyyyyyyyyyyyy2")
|
||
self.assertEqual(f"{datetime.datetime.now():h1{y=}h2{y=}h3{y=}}",
|
||
'h1y=20h2y=20h3y=20')
|
||
|
||
# Make sure __format__ is being called.
|
||
class C:
|
||
def __format__(self, s):
|
||
return f'FORMAT-{s}'
|
||
def __repr__(self):
|
||
return 'REPR'
|
||
|
||
self.assertEqual(f'{C()=}', 'C()=REPR')
|
||
self.assertEqual(f'{C()=!r}', 'C()=REPR')
|
||
self.assertEqual(f'{C()=:}', 'C()=FORMAT-')
|
||
self.assertEqual(f'{C()=: }', 'C()=FORMAT- ')
|
||
self.assertEqual(f'{C()=:x}', 'C()=FORMAT-x')
|
||
self.assertEqual(f'{C()=!r:*^20}', 'C()=********REPR********')
|
||
self.assertEqual(f"{C():{20=}}", 'FORMAT-20=20')
|
||
|
||
self.assertRaises(SyntaxError, eval, "f'{C=]'")
|
||
|
||
|
||
# Make sure leading and following text works.
|
||
x = 'foo'
|
||
self.assertEqual(f'X{x=}Y', 'Xx='+repr(x)+'Y')
|
||
|
||
# Make sure whitespace around the = works.
|
||
self.assertEqual(f'X{x =}Y', 'Xx ='+repr(x)+'Y')
|
||
self.assertEqual(f'X{x= }Y', 'Xx= '+repr(x)+'Y')
|
||
self.assertEqual(f'X{x = }Y', 'Xx = '+repr(x)+'Y')
|
||
self.assertEqual(f"sadsd {1 + 1 = :{1 + 1:1d}f}", "sadsd 1 + 1 = 2.000000")
|
||
|
||
self.assertEqual(f"{1+2 = # my comment
|
||
}", '1+2 = \n 3')
|
||
|
||
self.assertEqual(f'{""" # booo
|
||
"""=}', '""" # booo\n """=\' # booo\\n \'')
|
||
|
||
self.assertEqual(f'{" # nooo "=}', '" # nooo "=\' # nooo \'')
|
||
self.assertEqual(f'{" \" # nooo \" "=}', '" \\" # nooo \\" "=\' " # nooo " \'')
|
||
|
||
self.assertEqual(f'{ # some comment goes here
|
||
"""hello"""=}', ' \n """hello"""=\'hello\'')
|
||
self.assertEqual(f'{"""# this is not a comment
|
||
a""" # this is a comment
|
||
}', '# this is not a comment\n a')
|
||
|
||
# These next lines contains tabs. Backslash escapes don't
|
||
# work in f-strings.
|
||
# patchcheck doesn't like these tabs. So the only way to test
|
||
# this will be to dynamically created and exec the f-strings. But
|
||
# that's such a hassle I'll save it for another day. For now, convert
|
||
# the tabs to spaces just to shut up patchcheck.
|
||
#self.assertEqual(f'X{x =}Y', 'Xx\t='+repr(x)+'Y')
|
||
#self.assertEqual(f'X{x = }Y', 'Xx\t=\t'+repr(x)+'Y')
|
||
|
||
def test_debug_expressions_are_raw_strings(self):
|
||
with warnings.catch_warnings():
|
||
warnings.simplefilter('ignore', SyntaxWarning)
|
||
self.assertEqual(eval("""f'{b"\\N{OX}"=}'"""), 'b"\\N{OX}"=b\'\\\\N{OX}\'')
|
||
self.assertEqual(f'{r"\xff"=}', 'r"\\xff"=\'\\\\xff\'')
|
||
self.assertEqual(f'{r"\n"=}', 'r"\\n"=\'\\\\n\'')
|
||
self.assertEqual(f"{'\''=}", "'\\''=\"'\"")
|
||
self.assertEqual(f'{'\xc5'=}', r"'\xc5'='Å'")
|
||
|
||
def test_walrus(self):
|
||
x = 20
|
||
# This isn't an assignment expression, it's 'x', with a format
|
||
# spec of '=10'.
|
||
self.assertEqual(f'{x:=10}', ' 20')
|
||
|
||
# This is an assignment expression, which requires parens.
|
||
self.assertEqual(f'{(x:=10)}', '10')
|
||
self.assertEqual(x, 10)
|
||
|
||
def test_invalid_syntax_error_message(self):
|
||
with self.assertRaisesRegex(SyntaxError,
|
||
"f-string: expecting '=', or '!', or ':', or '}'"):
|
||
compile("f'{a $ b}'", "?", "exec")
|
||
|
||
def test_with_two_commas_in_format_specifier(self):
|
||
error_msg = re.escape("Cannot specify ',' with ','.")
|
||
with self.assertRaisesRegex(ValueError, error_msg):
|
||
f'{1:,,}'
|
||
|
||
def test_with_two_underscore_in_format_specifier(self):
|
||
error_msg = re.escape("Cannot specify '_' with '_'.")
|
||
with self.assertRaisesRegex(ValueError, error_msg):
|
||
f'{1:__}'
|
||
|
||
def test_with_a_commas_and_an_underscore_in_format_specifier(self):
|
||
error_msg = re.escape("Cannot specify both ',' and '_'.")
|
||
with self.assertRaisesRegex(ValueError, error_msg):
|
||
f'{1:,_}'
|
||
|
||
def test_with_an_underscore_and_a_comma_in_format_specifier(self):
|
||
error_msg = re.escape("Cannot specify both ',' and '_'.")
|
||
with self.assertRaisesRegex(ValueError, error_msg):
|
||
f'{1:_,}'
|
||
|
||
def test_syntax_error_for_starred_expressions(self):
|
||
with self.assertRaisesRegex(SyntaxError, "can't use starred expression here"):
|
||
compile("f'{*a}'", "?", "exec")
|
||
|
||
with self.assertRaisesRegex(SyntaxError,
|
||
"f-string: expecting a valid expression after '{'"):
|
||
compile("f'{**a}'", "?", "exec")
|
||
|
||
@unittest.expectedFailure # TODO: RUSTPYTHON; -
|
||
def test_not_closing_quotes(self):
|
||
self.assertAllRaise(SyntaxError, "unterminated f-string literal", ['f"', "f'"])
|
||
self.assertAllRaise(SyntaxError, "unterminated triple-quoted f-string literal",
|
||
['f"""', "f'''"])
|
||
# Ensure that the errors are reported at the correct line number.
|
||
data = '''\
|
||
x = 1 + 1
|
||
y = 2 + 2
|
||
z = f"""
|
||
sdfjnsdfjsdf
|
||
sdfsdfs{1+
|
||
2} dfigdf {3+
|
||
4}sdufsd""
|
||
'''
|
||
try:
|
||
compile(data, "?", "exec")
|
||
except SyntaxError as e:
|
||
self.assertEqual(e.text, 'z = f"""')
|
||
self.assertEqual(e.lineno, 3)
|
||
|
||
def test_syntax_error_after_debug(self):
|
||
self.assertAllRaise(SyntaxError, "f-string: expecting a valid expression after '{'",
|
||
[
|
||
"f'{1=}{;'",
|
||
"f'{1=}{+;'",
|
||
"f'{1=}{2}{;'",
|
||
"f'{1=}{3}{;'",
|
||
])
|
||
self.assertAllRaise(SyntaxError, "f-string: expecting '=', or '!', or ':', or '}'",
|
||
[
|
||
"f'{1=}{1;'",
|
||
"f'{1=}{1;}'",
|
||
])
|
||
|
||
def test_debug_in_file(self):
|
||
with temp_cwd():
|
||
script = 'script.py'
|
||
with open('script.py', 'w') as f:
|
||
f.write(f"""\
|
||
print(f'''{{
|
||
3
|
||
=}}''')""")
|
||
|
||
_, stdout, _ = assert_python_ok(script)
|
||
self.assertEqual(stdout.decode('utf-8').strip().replace('\r\n', '\n').replace('\r', '\n'),
|
||
"3\n=3")
|
||
|
||
def test_syntax_warning_infinite_recursion_in_file(self):
|
||
with temp_cwd():
|
||
script = 'script.py'
|
||
with open(script, 'w') as f:
|
||
f.write(r"print(f'\{1}')")
|
||
|
||
_, stdout, stderr = assert_python_ok(script)
|
||
self.assertIn(rb'\1', stdout)
|
||
self.assertEqual(len(stderr.strip().splitlines()), 2)
|
||
|
||
def test_fstring_without_formatting_bytecode(self):
|
||
# f-string without any formatting should emit the same bytecode
|
||
# as a normal string. See gh-99606.
|
||
def get_code(s):
|
||
return [(i.opname, i.oparg) for i in dis.get_instructions(s)]
|
||
|
||
for s in ["", "some string"]:
|
||
self.assertEqual(get_code(f"'{s}'"), get_code(f"f'{s}'"))
|
||
|
||
def test_gh129093(self):
|
||
self.assertEqual(f'{1==2=}', '1==2=False')
|
||
self.assertEqual(f'{1 == 2=}', '1 == 2=False')
|
||
self.assertEqual(f'{1!=2=}', '1!=2=True')
|
||
self.assertEqual(f'{1 != 2=}', '1 != 2=True')
|
||
|
||
self.assertEqual(f'{(1) != 2=}', '(1) != 2=True')
|
||
self.assertEqual(f'{(1*2) != (3)=}', '(1*2) != (3)=True')
|
||
|
||
self.assertEqual(f'{1 != 2 == 3 != 4=}', '1 != 2 == 3 != 4=False')
|
||
self.assertEqual(f'{1 == 2 != 3 == 4=}', '1 == 2 != 3 == 4=False')
|
||
|
||
self.assertEqual(f'{f'{1==2=}'=}', "f'{1==2=}'='1==2=False'")
|
||
self.assertEqual(f'{f'{1 == 2=}'=}', "f'{1 == 2=}'='1 == 2=False'")
|
||
self.assertEqual(f'{f'{1!=2=}'=}', "f'{1!=2=}'='1!=2=True'")
|
||
self.assertEqual(f'{f'{1 != 2=}'=}', "f'{1 != 2=}'='1 != 2=True'")
|
||
|
||
def test_newlines_in_format_specifiers(self):
|
||
cases = [
|
||
"""f'{1:d\n}'""",
|
||
"""f'__{
|
||
1:d
|
||
}__'""",
|
||
'''f"{value:.
|
||
{'2f'}}"''',
|
||
'''f"{value:
|
||
{'.2f'}f}"''',
|
||
'''f"{value:
|
||
#{'x'}}"''',
|
||
]
|
||
self.assertAllRaise(SyntaxError, "f-string: newlines are not allowed in format specifiers", cases)
|
||
|
||
valid_cases = [
|
||
"""f'''__{
|
||
1:d
|
||
}__'''""",
|
||
"""f'''{1:d\n}'''""",
|
||
]
|
||
|
||
for case in valid_cases:
|
||
compile(case, "<string>", "exec")
|
||
|
||
def test_raw_fstring_format_spec(self):
|
||
# Test raw f-string format spec behavior (Issue #137314).
|
||
#
|
||
# Raw f-strings should preserve literal backslashes in format specifications,
|
||
# not interpret them as escape sequences.
|
||
class UnchangedFormat:
|
||
"""Test helper that returns the format spec unchanged."""
|
||
def __format__(self, format):
|
||
return format
|
||
|
||
# Test basic escape sequences
|
||
self.assertEqual(f"{UnchangedFormat():\xFF}", 'ÿ')
|
||
self.assertEqual(rf"{UnchangedFormat():\xFF}", '\\xFF')
|
||
|
||
# Test nested expressions with raw/non-raw combinations
|
||
self.assertEqual(rf"{UnchangedFormat():{'\xFF'}}", 'ÿ')
|
||
self.assertEqual(f"{UnchangedFormat():{r'\xFF'}}", '\\xFF')
|
||
self.assertEqual(rf"{UnchangedFormat():{r'\xFF'}}", '\\xFF')
|
||
|
||
# Test continuation character in format specs
|
||
self.assertEqual(f"""{UnchangedFormat():{'a'\
|
||
'b'}}""", 'ab')
|
||
self.assertEqual(rf"""{UnchangedFormat():{'a'\
|
||
'b'}}""", 'ab')
|
||
|
||
# Test multiple format specs in same raw f-string
|
||
self.assertEqual(rf"{UnchangedFormat():\xFF} {UnchangedFormat():\n}", '\\xFF \\n')
|
||
|
||
def test_gh139516(self):
|
||
with temp_cwd():
|
||
script = 'script.py'
|
||
with open(script, 'wb') as f:
|
||
f.write('''def f(a): pass\nf"{f(a=lambda: 'à'\n)}"'''.encode())
|
||
assert_python_ok(script)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
unittest.main()
|