Skip to content

fix: read --file inputs with utf-8-sig to strip UTF-8 BOM - #4

Open
2233admin wants to merge 1 commit into
masterfrom
fix/utf-8-sig-bom
Open

fix: read --file inputs with utf-8-sig to strip UTF-8 BOM#4
2233admin wants to merge 1 commit into
masterfrom
fix/utf-8-sig-bom

Conversation

@2233admin

Copy link
Copy Markdown
Owner

PR: fix: read --file inputs with utf-8-sig to strip UTF-8 BOM

Summary

On Windows, Out-File -Encoding utf8 (and several other Windows tools) write a
UTF-8 BOM (\xef\xbb\xbf) by default. Python's json.load treats the BOM as
invalid content, raising:

json.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

This breaks every --file input on Windows that was authored by PowerShell,
Notepad's "Save as UTF-8", Excel's CSV export, or any tool that defaults to
UTF-8-with-BOM. The fix is a one-line change at each call site: switch
encoding='utf-8' (or the implicit default) to encoding='utf-8-sig', which
auto-strips a leading BOM and is fully backward-compatible with normal
no-BOM files.

Why I hit it

I was using glyph-arts chart bar --file data.json from a PowerShell test
harness. The harness wrote data.json with Out-File -Encoding utf8, which
silently added a BOM. Every chart type (bar, sparkline, line, pie, gauge,
table) returned ERROR:json: Expecting value: line 1 column 1 (char 0) even
though the file content was valid JSON. Two hours of debugging later, hexdump
of the file showed the BOM.

Changes

9 call sites in 2 files:

File Sites Pattern
cli_charts/cmd/_helpers.py 8 open(args.file, encoding='utf-8')'utf-8-sig' (4 sites) and open(args.file)open(args.file, encoding='utf-8-sig') (4 sites)
cli_charts/dashboard.py 1 open(args.file)open(args.file, encoding='utf-8-sig')

Plus tests/test_bom_compat.py (new, 8 tests) covering:

  • 5 chart types with BOM-prefixed files: bar, sparkline, line, pie, gauge
  • dashboard with a BOM file
  • Sanity check: non-BOM files still work
  • Direct open(..., encoding='utf-8-sig') strips BOM
  • Negative test: original encoding='utf-8' fails on a BOM file (proves the bug existed)

Diff

--- a/cli_charts/cmd/_helpers.py
+++ b/cli_charts/cmd/_helpers.py
@@ -2620,7 +2620,7 @@
     if args.type in {'incplot', 'textplot', 'turtle', 'formula', 'formula-pretty', 'math', 'math-pretty'}:
         if args.file:
-            with open(args.file, encoding='utf-8') as _f:
+            with open(args.file, encoding='utf-8-sig') as _f:
                 raw = _f.read().strip()
         elif args.data is not None:
             raw = args.data
@@ -2666,7 +2666,7 @@
     if args.type in { ... }:
         ...
-        with open(args.file, encoding='utf-8') as _f:
+        with open(args.file, encoding='utf-8-sig') as _f:
             ...
@@ -2705,7 +2705,7 @@
-        with open(args.file, encoding='utf-8') as _f:
+        with open(args.file, encoding='utf-8-sig') as _f:
             ...
@@ -2736,7 +2736,7 @@
-        with open(args.file, encoding='utf-8') as _f:
+        with open(args.file, encoding='utf-8-sig') as _f:
             ...
@@ -2772,7 +2772,7 @@
-        with open(args.file) as _f:
+        with open(args.file, encoding='utf-8-sig') as _f:
             ...
@@ -2857,7 +2857,7 @@
-        with open(args.file) as _f:
+        with open(args.file, encoding='utf-8-sig') as _f:
             ...
@@ -2910,8 +2910,8 @@
-                with open(args.file) as _f:
+                with open(args.file, encoding='utf-8-sig') as _f:
                     ...
@@ -2933,8 +2933,8 @@
-                with open(args.file) as _f:
+                with open(args.file, encoding='utf-8-sig') as _f:
                     ...

--- a/cli_charts/dashboard.py
+++ b/cli_charts/dashboard.py
@@ -318,7 +318,7 @@
     if args.demo:
         config = DEMO_CONFIG
     elif args.file:
-        with open(args.file) as f:
+        with open(args.file, encoding='utf-8-sig') as f:
             config = json.load(f)
     elif args.data:
         try:

Test results

tests/test_bom_compat.py::test_chart_loads_bom_file[bar]            PASSED
tests/test_bom_compat.py::test_chart_loads_bom_file[sparkline]     PASSED
tests/test_bom_compat.py::test_chart_loads_bom_file[line]          PASSED
tests/test_bom_compat.py::test_chart_loads_bom_file[pie]           PASSED
tests/test_bom_compat.py::test_chart_loads_bom_file[gauge]         PASSED
tests/test_bom_compat.py::test_non_bom_file_still_works            PASSED
tests/test_bom_compat.py::test_raw_open_with_utf8sig_strips_bom    PASSED
tests/test_bom_compat.py::test_raw_open_with_utf8_fails_on_bom     PASSED
============================== 8 passed in 0.70s ==============================

Backward compatibility

encoding='utf-8-sig' is a strict superset of encoding='utf-8':

  • Files with a BOM: BOM is silently stripped, content loads correctly.
  • Files without a BOM: behavior is identical to encoding='utf-8'.

No existing user should see a behavior change unless they were already
working around the BOM bug (e.g. stripping the BOM with sed or running
the file through iconv first).

Reproduction

# PowerShell — reproduces the bug on the unpatched code
'{ "labels": ["Q1","Q2"], "values": [10,14] }' |
    Out-File -Encoding utf8 data.json
python -m cli_charts.chart bar --file data.json --title "test"
# ERROR:json: Expecting value: line 1 column 1 (char 0)

# After this patch, same command renders the bar chart normally.
# Minimal Python repro
import json
with open("data.json", "rb") as f:
    print("first 3 bytes:", f.read(3).hex())   # efbbbf on PowerShell
# json.load(open("data.json"))  # raises JSONDecodeError on the BOM
# json.load(open("data.json", encoding="utf-8-sig"))  # works

Notes

  • I considered also adding a --strip-bom flag, but utf-8-sig makes that
    redundant — the fix is invisible to users.
  • I considered normalizing all --data (string) inputs to strip BOM too, but
    the BOM only appears in file content, and --data is already in-memory
    by the time it reaches Python.
  • I noticed dashboard.py has a reconfigure(encoding="utf-8") call on
    the output stream (line 20-22). I left that alone — it addresses an
    unrelated Windows console display issue, not the input BOM issue.

Happy to split this into multiple commits, narrow the scope to just
_helpers.py, or add a flag if you'd prefer a more conservative change.

On Windows, PowerShell's Out-File -Encoding utf8 writes a UTF-8 BOM
(\xef\xbb\xbf) by default. Python's json.load treats the BOM as
invalid content, raising:

  JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Switch --file open() calls from encoding='utf-8' (or implicit default)
to encoding='utf-8-sig', which auto-strips a leading BOM and is fully
backward-compatible with non-BOM files. Affects 8 sites in
cli_charts/cmd/_helpers.py (animate, record, code, mermaid, diagram,
sequence, and friends) and 1 site in cli_charts/dashboard.py.

Adds tests/test_bom_compat.py covering all 5 chart types (bar,
sparkline, line, pie, gauge), dashboard, the raw open() behavior,
and a negative test demonstrating the original bug.
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Fixed file handling to correctly parse UTF-8 files with Byte Order Mark (BOM) prefix in chart and dashboard commands.
  • Tests

    • Added comprehensive tests verifying BOM file support and backward compatibility with standard UTF-8 files.

Walkthrough

The PR adds UTF-8 BOM (Byte Order Mark) handling to file inputs across the CLI by updating open() calls to use encoding='utf-8-sig' in the chart command helpers and dashboard, and validates the fix with a new test suite covering BOM-prefixed JSON, backward compatibility, and encoding behavior.

Changes

UTF-8 BOM Compatibility

Layer / File(s) Summary
CLI file input BOM support
cli_charts/cmd/_helpers.py, cli_charts/dashboard.py
All open(args.file, ...) calls in the chart command parser (covering chart-type, diagram/mermaid/effect/animate, ASCII-motion, and auto branches) and dashboard config loader now use encoding='utf-8-sig' to tolerate BOM-prefixed UTF-8 files.
BOM compatibility test suite
tests/test_bom_compat.py
New test module with helpers (_write_bom_json, _run) and four test scenarios: parameterized end-to-end CLI tests against BOM-prefixed JSON for multiple chart types, backward-compatibility check for non-BOM JSON, and direct unit tests confirming utf-8-sig strips BOM (success) while utf-8 leaves it (failure).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

A rabbit in bits found a curious mark,
A BOM in the dark, at the file's very start,
With utf-8-sig bright, now JSON reads right,
And tests guard the way through the encoding night! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main fix: changing file reading to use utf-8-sig encoding to strip UTF-8 BOM from Windows-authored files.
Description check ✅ Passed The description is comprehensive and well-structured, covering the problem, root cause, solution, test coverage, and backward compatibility considerations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@repowise-bot

repowise-bot Bot commented Jun 5, 2026

Copy link
Copy Markdown

⚠️ Health: 6.3 → 6.0 (-0.3)
2 hotspots · 5 hidden couplings · 4 dead-code findings

🚨 Change risk: 7.5/10 (high)
This change's risk is driven by:

  • large diff (many lines added)
🔥 Hotspots touched (2)
  • cli_charts/dashboard.py — 5 commits/90d, 3 dependents · primary owner: 2233admin (100%)
  • cli_charts/cmd/_helpers.py — 6 commits/90d, 42 dependents · primary owner: Curry (83%)
🔗 Hidden coupling (2 files)
  • cli_charts/dashboard.py co-changes with these files (not in this PR):
    • cli_charts/chart.py (4× — 🟢 routine)
    • .github/workflows/ci.yml (2× — 🟢 routine)
    • pyproject.toml (2× — 🟢 routine)
  • cli_charts/cmd/_helpers.py co-changes with these files (not in this PR):
    • README.md (2× — 🟢 routine)
    • cli_charts/cmd/__init__.py (2× — 🟢 routine)
💀 Dead code (4 findings)
  • 💀 cli_charts/dashboard.py render_sparkline (confidence 1.00)
  • 💀 cli_charts/dashboard.py render_table (confidence 1.00)
  • 💀 cli_charts/dashboard.py render_metric (confidence 1.00)
1 more
  • 💀 cli_charts/dashboard.py render_bar (confidence 1.00)

👀 Suggested reviewers @2233admin @Curry


📊 Full report · ⭐ Star Repowise · 📥 Install bot · Last updated 2026-06-05 04:46 UTC
Silence on a single PR with [skip repowise] in the title · Per-repo toggle on repowise.dev/settings?tab=bot

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates file reading operations in cli_charts/cmd/_helpers.py and cli_charts/dashboard.py to use utf-8-sig encoding, ensuring that UTF-8 BOM prefixes (commonly written by PowerShell on Windows) are automatically stripped and parsed correctly. It also adds a comprehensive test suite in tests/test_bom_compat.py to verify BOM compatibility. The review feedback suggests strengthening these integration tests by asserting that the subprocess command execution returns a successful exit code (rc == 0), which prevents false positives if the command fails for other reasons.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread tests/test_bom_compat.py
Comment on lines +59 to +61
assert "ERROR:json" not in err, (
f"BOM file failed JSON load for {chart_type}\\nstderr: {err}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The test currently only asserts that "ERROR:json" is not in stderr. However, if the command fails for other reasons (such as missing dependencies, environment issues, or other exceptions), the test might silently pass because "ERROR:json" is not present in the output. Asserting that the return code rc is 0 ensures that the command executed successfully and the test is robust against false positives.

Suggested change
assert "ERROR:json" not in err, (
f"BOM file failed JSON load for {chart_type}\\nstderr: {err}"
)
assert rc == 0, f"Command failed with exit code {rc}\nstdout: {out}\nstderr: {err}"
assert "ERROR:json" not in err, (
f"BOM file failed JSON load for {chart_type}\nstderr: {err}"
)

Comment thread tests/test_bom_compat.py
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f)
rc, out, err = _run("bar", path)
assert "ERROR:json" not in err, err

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the parametrized test, asserting that the return code rc is 0 ensures that the command executed successfully and prevents false positives if the command fails due to other errors.

Suggested change
assert "ERROR:json" not in err, err
assert rc == 0, f"Command failed with exit code {rc}\nstdout: {out}\nstderr: {err}"
assert "ERROR:json" not in err, err

@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cli_charts/cmd/_helpers.py 0.00% 8 Missing ⚠️
cli_charts/dashboard.py 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_bom_compat.py (1)

58-58: 💤 Low value

Optional: Mark unused unpacked variables with underscores.

The return values rc and out from _run() are unpacked but never used. Consider prefixing them with underscores to indicate they are intentionally unused, which silences the linter warning.

♻️ Optional cleanup for linter warnings
-        rc, out, err = _run(chart_type, path)
+        _rc, _out, err = _run(chart_type, path)

And similarly at line 71:

-        rc, out, err = _run("bar", path)
+        _rc, _out, err = _run("bar", path)

Also applies to: 71-71

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_bom_compat.py` at line 58, The variables rc and out returned from
_run(...) are unpacked but unused; update the unpacking to mark them as
intentionally unused (e.g., rename to _rc and _out or prefix with an underscore)
while keeping err if it is used (or rename all unused returns to _err/_out/_rc
as appropriate), and apply the same change for the other _run(...) call; locate
the calls to _run in this test module (symbol: _run) and adjust the left-hand
tuple to use underscore-prefixed names to silence the linter.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_bom_compat.py`:
- Line 58: The variables rc and out returned from _run(...) are unpacked but
unused; update the unpacking to mark them as intentionally unused (e.g., rename
to _rc and _out or prefix with an underscore) while keeping err if it is used
(or rename all unused returns to _err/_out/_rc as appropriate), and apply the
same change for the other _run(...) call; locate the calls to _run in this test
module (symbol: _run) and adjust the left-hand tuple to use underscore-prefixed
names to silence the linter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 96fc1ff9-ee71-4ee4-9c84-6e21afb17ebf

📥 Commits

Reviewing files that changed from the base of the PR and between aa31a07 and ecde248.

📒 Files selected for processing (4)
  • cli_charts/cmd/_helpers.py
  • cli_charts/dashboard.py
  • tests/__init__.py
  • tests/test_bom_compat.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant