ftfy mojibake repair and curly-quote normalization: working versions (tested 2026-10-16)
Repair Latin-1 mojibake with ftfy, undo double-encoding, straighten curly quotes to ASCII and control HTML unescaping, all tested offline under pytest.
passed6 passed · 0 failed2026-10-16 09:57 UTC- Status
- passed
- Last passed
- 2026-10-16 09:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
ftfy@6.3.1pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38883899161
The code
Exactly the files that passed. Your agent can pull them with get_recipe("ftfy-mojibake-fix") (install), or see them on GitHub.
requirements.txt
ftfy==6.3.1 pytest==9.1.1
fixer.py
import ftfy
from ftfy import TextFixerConfig, fix_and_explain, fix_text
KEEP_TYPOGRAPHY = TextFixerConfig(uncurl_quotes=False, unescape_html=False)
def clean(text: str) -> str:
"""Fix mojibake and reduce typographic characters to ASCII."""
return fix_text(text)
def clean_keep_typography(text: str) -> str:
"""Fix mojibake but keep curly quotes and HTML entities as written."""
return fix_text(text, config=KEEP_TYPOGRAPHY)
def explain(text: str) -> tuple[str, list[str]]:
"""Return the fixed text plus the labels of the fixers that fired."""
fixed = fix_and_explain(text)
return fixed.text, [f"{kind}:{name}" for kind, name in fixed.explanation]
tests/test_fixer.py
from fixer import clean, clean_keep_typography, explain
def mojibake(text: str) -> str:
"""Reproduce the classic bug: UTF-8 bytes decoded as Latin-1."""
return text.encode("utf-8").decode("latin-1")
def test_fixes_single_encoded_mojibake():
assert mojibake("café") == "café"
assert clean(mojibake("café")) == "café"
assert clean("plain ASCII") == "plain ASCII"
def test_fixes_double_encoded_mojibake():
assert clean(mojibake(mojibake("café"))) == "café"
def test_uncurls_quotes_by_default():
# fix_text does more than repair encoding: it also straightens quotes.
assert clean(mojibake("The “smart” quote")) == 'The "smart" quote'
def test_keep_typography_config_preserves_quotes():
assert clean_keep_typography("“smart”") == "“smart”"
def test_html_entities_unescaped_by_default():
assert clean("Tom & Jerry") == "Tom & Jerry"
assert clean_keep_typography("Tom & Jerry") == "Tom & Jerry"
def test_explain_reports_encoding_steps():
text, steps = explain(mojibake("café"))
assert text == "café"
assert steps == ["encode:latin-1", "decode:utf-8"]
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.