PyCQA / PyCQA/docformatter

1.7.8 crashes with a tokenize ValueError on a multi-line f-string inside parentheses

Open
#367 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

C: convention P: bug U: high
Dominant language
Python
Stars
598
Forks
93
PR merge metrics
No merged PRs in 30d

Description

docformatter 1.7.8 aborts with a ValueError out of tokenize.untokenize when a file contains a multi-line f-string inside a parenthesized expression. 1.7.7 formats the same file cleanly.

Reproducer

def build(x):
    return (
        f"""a
{x}""",
    )
$ docformatter --in-place t.py
  File ".../docformatter/format.py", line 876, in _do_format_code
    _code = tokenize.untokenize(self.new_tokens)
  File "/usr/lib/python3.11/tokenize.py", line 216, in untokenize
    self.add_whitespace(start)
  File "/usr/lib/python3.11/tokenize.py", line 177, in add_whitespace
    raise ValueError("start ({},{}) precedes previous end ({},{})"
ValueError: start (2,8) precedes previous end (3,4)
$ echo $?
1

No config file and no flags are needed. Two things narrow it:

  • Remove the f prefix and the run exits 3 (would reformat) with no error, so the f-string prefix is the trigger.
  • Assign the same f-string to a name instead of putting it in a tuple and it does not crash, so it has to sit inside parentheses.

Affected versions

docformatter Python 3.11 Python 3.12 Python 3.13
1.7.7 ok ok ok
1.7.8 ValueError ok ok
master ValueError ok ok

Root cause

Bisected to d9ace1f ("fix: resolve several empty line regressions", #330). Its parent 7798699 is clean.

That commit adds a Python < 3.12 workaround to classify.is_f_string:

elif any(
    [
        token.string.startswith('f"""'),
        prev_token.string.startswith('f"""'),
        token.string.startswith("f'''"),
        prev_token.string.startswith("f'''"),
    ]
):
    return True

The PY312 branch above it tests tokenize.FSTRING_MIDDLE, which only appears inside an f-string. The < 3.12 branch instead matches any token whose text begins with f""", including an ordinary triple-quoted f-string that is not a docstring — and, via prev_token, the token that follows one.

The single consumer is _get_unmatched_start_end_indices (format.py:499), where a true verdict sets _start_row = prev_token.end[0] (format.py:526-535). That rewrites the f-string's start row back to where the previous token ended, so untokenize sees a token starting above one it has already emitted and raises.

Impact

With several files on one command line, the run aborts at the first offending file and exits 1, leaving every file after it unprocessed. On a 1,255-file tree this stopped at file 318 with no indication that the remainder was skipped.

Environment

CPython 3.11.15, Linux x86-64, docformatter==1.7.8 from PyPI.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with classify.is_f_string and its consumer _get_unmatched_start_end_indices in format.py:499-535, then run the supplied multi-line f-string reproducer on Python 3.11. Trace how the pre-3.12 f-string check changes the token start row; done means the reproducer formats without ValueError and multi-file runs do not abort on this input.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
tooling
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.