Repeated standalone docformatter runs alternate between two layouts of the same UTF-8 Python file. Every run exits 3. A pre-commit hook therefore never reaches a clean result.
Environment
- Python 3.10.11, macOS 27.0
- docformatter 1.7.8
- charset-normalizer 3.4.7
- Original pre-commit environment also has tomli 2.4.1.
- Standalone reproduction below requires neither pre-commit nor Ruff, Markdown formatting, RF-DETR, or ML dependencies.
Reproduce
In an empty directory, prepare a Python 3.10 virtual environment:
python3.10 -m venv .venv
.venv/bin/python -m pip install "docformatter==1.7.8" "charset-normalizer==3.4.7"
Save this as repro.py using UTF-8. Preserve the space after the final a_b in the first comment; file contents affect encoding detection.
# a_b a_b a_b a_b a_b
class ModelWrapper:
"""``ModelWrapper`` (module-scope, importable in isolation) normalizes export-mode output to a tuple.
The wrapped model is expected to already be in export mode (``forward_export``), which returns a tuple (full
detector) or a plain list (:class:`rfdetr.export._backend._BackboneExport`) — never a dict. A dict output means
the caller forgot the mode-switch, which is a caller bug the wrapper must surface loudly rather than silently
reshape.
"""
Run the same formatter repeatedly (do not use set -e; exit 3 is the symptom):
for n in 1 2 3 4; do
.venv/bin/python -m docformatter --wrap-summaries 120 --wrap-descriptions 120 --in-place repro.py
echo "exit=$?"
done
Expected
After initial formatting, further runs leave the file unchanged and exit 0.
Actual
All four runs exit 3. Pass 2 restores the original file byte-for-byte; pass 3 restores pass 1; pass 4 restores the original again.
First-pass diff (second pass reverses it):
--- before.py
+++ after.py
@@ -3,7 +3,6 @@
"""``ModelWrapper`` (module-scope, importable in isolation) normalizes export-mode output to a tuple.
The wrapped model is expected to already be in export mode (``forward_export``), which returns a tuple (full
- detector) or a plain list (:class:`rfdetr.export._backend._BackboneExport`) — never a dict. A dict output means
- the caller forgot the mode-switch, which is a caller bug the wrapper must surface loudly rather than silently
- reshape.
+ detector) or a plain list (:class:`rfdetr.export._backend._BackboneExport`) — never a dict. A dict output means the
+ caller forgot the mode-switch, which is a caller bug the wrapper must surface loudly rather than silently reshape.
"""
Diagnostic evidence
Before successive runs, charset_normalizer.from_path("repro.py").best().encoding returns utf_8, mac_iceland, utf_8, mac_iceland for this small example. Both file states remain valid UTF-8. In the original larger file, the alternate guess was cp1250 instead.
The installed docformatter/encode.py:72-91 selects the heuristic result for decoding. The same UTF-8 em dash is consequently interpreted with a different character count, affecting line wrapping. Formatting changes the input to the next encoding guess, producing the cycle.
On the original file, a diagnostic run forcing UTF-8 decoding stabilized after one pass. Replacing em dashes with ASCII also stabilized; neither diagnostic modification is proposed as a project fix.
Python source encoding has deterministic rules: BOM or encoding cookie, otherwise UTF-8. See Python tokenize.detect_encoding. Please consider using those rules when reading Python files and add a repeated-run idempotence regression check.
Verified locally with the pinned versions above; other dependency versions and platforms untested. No upstream fix verified.
Repeated standalone
docformatterruns alternate between two layouts of the same UTF-8 Python file. Every run exits 3. A pre-commit hook therefore never reaches a clean result.Environment
Reproduce
In an empty directory, prepare a Python 3.10 virtual environment:
Save this as
repro.pyusing UTF-8. Preserve the space after the finala_bin the first comment; file contents affect encoding detection.Run the same formatter repeatedly (do not use
set -e; exit 3 is the symptom):Expected
After initial formatting, further runs leave the file unchanged and exit 0.
Actual
All four runs exit 3. Pass 2 restores the original file byte-for-byte; pass 3 restores pass 1; pass 4 restores the original again.
First-pass diff (second pass reverses it):
Diagnostic evidence
Before successive runs,
charset_normalizer.from_path("repro.py").best().encodingreturnsutf_8,mac_iceland,utf_8,mac_icelandfor this small example. Both file states remain valid UTF-8. In the original larger file, the alternate guess wascp1250instead.The installed
docformatter/encode.py:72-91selects the heuristic result for decoding. The same UTF-8 em dash is consequently interpreted with a different character count, affecting line wrapping. Formatting changes the input to the next encoding guess, producing the cycle.On the original file, a diagnostic run forcing UTF-8 decoding stabilized after one pass. Replacing em dashes with ASCII also stabilized; neither diagnostic modification is proposed as a project fix.
Python source encoding has deterministic rules: BOM or encoding cookie, otherwise UTF-8. See Python tokenize.detect_encoding. Please consider using those rules when reading Python files and add a repeated-run idempotence regression check.
Verified locally with the pinned versions above; other dependency versions and platforms untested. No upstream fix verified.