mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-29 03:04:38 +08:00
* Match CPython's SyntaxError range for unparenthesized `except` types
CPython's `invalid_except_stmt` rule raises the error only once the whole
clause has matched, and reports a range that starts at the first exception
type and ends at the `:` closing the clause, so it covers the `as NAME`
part as well:
invalid_except_stmt:
| 'except' a=expression ',' expressions 'as' NAME ':' {
RAISE_SYNTAX_ERROR_STARTING_FROM(a, "multiple exception types
must be parenthesized when using 'as'") }
The parser reports the exception types alone, so look up that `:` in the
source and widen the range to match. Only `as NAME` may follow the
exception types, which is why the first `:` after them is the one closing
the clause.
try:
pass
except A, B, C as e:
pass
CPython 3.14: ('x.py', 3, 8, 'except A, B, C as e:\n', 3, 20)
before: ('x.py', 3, 8, 'except A, B, C as e:\n', 3, 15)
Fixes #8496
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Clarify that the `except` error range stops before the closing colon
`invalid_except_stmt_end` returns the exclusive end of the range, so the
column it yields is the one the `:` sits on even though the `:` is not part
of the range. The previous "ending at the `:`" wording read as if the colon
were included, which invites an off-by-one "fix".
Comments only; no behavior change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Fix the `except ... as` error column for non-ASCII names and line joins
`invalid_except_stmt_end` recovers the end of CPython's range by scanning the
source, which has to redo by hand what CPython gets from its tokenizer:
- The column was the byte distance from the line start, but `end_offset` is a
character column. `except Ä, B as e:` reported 18 instead of 17. CPython
converts explicitly, in `_PyPegen_byte_offset_to_character_offset`.
- The colon scan already spans explicit line joins, but the `as` check ran on
the raw slice, so a `\` before `as` left the range unextended.
`except A, B \` + `as exc:` reported (3, 12) instead of (4, 7).
Add regression cases to `syntax_invalid.py` covering both, plus the whitespace,
`except*` and line-join placements that already worked. All values are verified
against CPython 3.14.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Update the Ruff fork to 0.16.10
Point the rustpython-ruff_* dependencies at the 0.16.10-rustpython tag
of the RustPython Ruff fork and port to its AST changes:
`ExprCompare` stores all operands in `operands`, call arguments use
`ThinVec`, dict comprehension generators and f-string elements are boxed
slices, `FStringValue` iterates `FStringPartRef`, and `TokenKind::Name`
is now `TokenKind::Identifier`.
Assisted-by: Grok:grok-4.6
Assisted-by: Claude Code:claude-opus-5-5
* Drop the source scan for the `except ... as` error range
The parser now reports the unparenthesized exception types error with a
range ending at the `:` that closes the clause, so
`invalid_except_stmt_end` and the message match that widened the range
are removed.
Assisted-by: Claude Code:claude-opus-5-5
* Take parse error messages from the parser as they are
The Ruff fork now emits the final message texts, so `vm_new.rs` no
longer rewrites parse error messages by matching their text and no
longer lowercases the first character of every message.
`analyze_compile_error` keeps only the caret narrowing for starred
expressions and the line number in the unterminated string message.
The compiler matches the new lowercase texts for indented block and
mixed bytes literal errors.
The fork is referenced by revision until it is published.
Assisted-by: Claude Code:claude-opus-5-5
* Take starred and bytes literal error ranges from the parser
The Ruff fork now reports misplaced starred expressions at the `*` and
non-ASCII bytes literal errors over the whole literal. `vm_new.rs` drops
`narrow_caret` together with `SyntaxErrorInfo`, which only carried the
message after that, and the compiler drops `bytes_literal_span`.
Assisted-by: Claude Code:claude-opus-5-5
* Take indentation error kinds and positions from the parser
The Ruff fork now reports `TabError` and `TooDeepIndentation` as their
own lexer errors and places indentation and line continuation errors
itself. `vm_new.rs` picks TabError or IndentationError from the error
kind instead of scanning the source for mixed indentation, drops the
TabError message override, and sets the end offset of tokenizer errors
to 0 or -1 by kind. The compiler drops the line-end relocation of
unindent errors and the line continuation rewrite. `_tokenize` raises
TabError and depth errors for the token they replace, also for sources
containing tabs.
Remove expectedFailure from test_tokenize.test_max_indent and four
test_tabnanny tests.
Assisted-by: Claude Code:claude-opus-5-5
* Take unterminated string messages from the parser
The Ruff fork's unterminated string errors now include the detected
line and are reported at the start of the string. `vm_new.rs` drops its
message override for `UnclosedStringError`, and the shell continues a
line on an unterminated triple-quoted string by the error's
`triple_quoted` flag instead of reading the quotes from the source.
Assisted-by: Claude Code:claude-opus-5-5
* Take bracket errors and tokenizer error priority from the parser
Bump the ruff fork, which now reports unmatched, mismatched, unclosed
and too deeply nested brackets and decides whether a tokenizer error
replaces the parser's first error.
Remove the bracket, unterminated string and nesting depth source
scanners and `pre_parse_source_error`. A tokenizer error from the
parser now wins over source scanner diagnostics found after it. An
unclosed bracket is incomplete input only when the parser reports it
as such, and other tokenizer errors end where they start.
Remove the expected failure markers from test_unicode_identifiers
test_invalid and the `import ä £` doctest in test_syntax.
Assisted-by: Claude Code:claude-opus-5-5
* Take number literal, string prefix and non-printable errors from the parser
Bump the ruff fork, which now reports malformed number literals,
incompatible string prefixes and non-printable characters itself.
Remove the number literal, string prefix and non-printable character
source scanners and the override classes that ranked them. Leading zero
and string prefix errors keep their range instead of ending where they
start.
Assisted-by: Claude Code:claude-opus-5-5
* Take the missing indented block error from the parser
Bump the ruff fork, which now reports `ExpectedIndentedBlock` with the
clause and the line of its header keyword.
Remove the message rewrite that derived the clause and line from the
source. The VM chooses IndentationError or incomplete input by the
error variant instead of its message.
Assisted-by: Claude Code:claude-opus-5-5
* Take binding target errors from the parser
Bump the ruff fork, which now names the invalid expression in
assignment, augmented assignment, delete, `for`, `with`, import,
pattern and `except` target errors.
Remove the source scanners for those targets and the message rewrites
of `InvalidAssignmentTarget` and `InvalidNamedAssignmentTarget`.
Remove the expected failure markers from test_syntax doctests that now
pass.
Assisted-by: Claude Code:claude-opus-5-5
* Take parameter, argument, star, comma and dictionary errors from the parser
Bump the ruff fork to 36919b62fa. Remove the source scanners for type
parameters, parameter lists, star annotations, call arguments, def type
parameters, parenthesized groups, missing commas and dictionary items.
Set end_offset 0 for ExpectedColonAfterDictionaryKey. Remove 14
EXPECTED_FAILURE markers in test_syntax and test_unpack_ex.
Assisted-by: Claude Code:claude-opus-5-5
* Take expression errors from the parser
Bump the ruff fork to 4f922d2124. Remove the source scanners for
comprehensions, collection and condition assignments, expressions
between strings, named expression targets, missing `in` after for-loop
variables and statements in `if` expressions. Remove the now passing
expected failure markers in test_syntax and test_named_expressions.
Assisted-by: Claude Code:claude-opus-5-5
* Take statement errors from the parser
Bump the ruff fork to c7f6594ed6. Remove the source scanners for
standalone except, `import ... from`, `**_` mapping rests, `elif` after
`else` and mixed except handlers, and the post-parse scanners for call
arguments, match targets, yield after a comma and parenthesized star
imports.
Assisted-by: Claude Code:claude-opus-5-5
* Take string and f-string errors from the parser
Bump the ruff fork to 6b46d0dfc8. Remove the source scanners for
malformed `\N` escapes, f-string and t-string replacement fields, mixed
t-string literals and `print`/`exec` statements, and the override
plumbing they used. Drop the `f'{x'; '` case from the compiler test,
whose expected message differs from CPython 3.14.
Assisted-by: Claude Code:claude-opus-5-5
* Restore the bracket depth check and fix incomplete unclosed strings
Restore `pre_parse_source_error`, which rejects bracket nesting past 200
levels before parsing. The parser reports the same error but still
builds the full tree, which overflows the native stack when dropped.
Remove the VM's unclosed string scanner, which ignored the mode and
treated `'abc` in exec mode as incomplete input. The compiler's check
now skips escaped newlines.
Assisted-by: Claude Code:claude-opus-5-5
* Report multiple statements before later errors in single mode
In single mode, when the first statement is a simple statement, report
code after its line as multiple statements before any error found
later, at the newline token or the comment before it. This replaces the
multiple statements check on the parsed body and also applies to
`ast.parse(mode='single')`.
An unclosed string that follows an earlier syntax error is no longer
incomplete input. The bracket depth error range is now empty.
Assisted-by: Claude Code:claude-opus-5-5
* Update the Ruff fork for parser speed and starred annotations
Bump the ruff fork to 8b59b85344, which skips the tokenizer error pass
when parsing finds no errors, stops allocating for every identifier's
string prefix check, and reports `def f(*args: *b = ...)` as invalid
syntax.
Remove expected failure markers from test_code_module
test_indentation_error and test_sysexcepthook_indentation_error, and
from test_dictcomps test_illegal_assignment.
Assisted-by: Claude Code:claude-opus-5-5
* Use the published Ruff fork 0.16.10 crates
Switch the rustpython-ruff_* dependencies from the git revision to the
0.16.10 crates.io release, published from the same commit (8b59b85344).
Assisted-by: Claude Code:claude-opus-5-5
---------
Co-authored-by: zzarbttoo <qodbwls70@naver.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1874 lines
71 KiB
Python
Vendored
1874 lines
71 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.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')
|
||
|
||
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}', '\\&')
|
||
|
||
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')
|
||
|
||
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')
|
||
|
||
@unittest.expectedFailureIf(unittest.__spec__.origin == "frozen", "TODO: RUSTPYTHON; frozen module repr has no 'from' path")
|
||
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')
|
||
|
||
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")
|
||
|
||
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()
|