Rust-backed transliteration similar to Python Unidecode, with optional PyO3 bindings for Python
Project description
unidecode-rs - Unicode → ASCII transliteration faithful to Python
Fast Rust implementation (optional Python bindings via PyO3) targeting bit‑for‑bit equivalence with Python Unidecode. Provides:
- Same output as
Unidecodefor all covered tables - Noticeably higher performance (see perf snapshot in tests)
- Golden tests comparing dynamically against the Python version
- High coverage on critical paths (bitmap + per‑block dispatch)
Repository layout
src/ # Core library sources + generated tables
benches/ # Criterion benchmarks (Rust)
scripts/ # Developer helper scripts (bench_compare, coverage)
tests/ # Rust integration & golden tests
tests/python/ # Python parity & upstream harness
python/ # Python shim for upstream-compatible API
docs/ # Coverage and performance documentation
Quick summary
- Rust usage:
unidecode_rs::unidecode("déjà") -> "deja" - Python usage: build extension with
maturin develop --features python - Idempotence:
unidecode(unidecode(x)) == unidecode(x)(after first pass everything is ASCII) - Golden tests: ensure exact parity with Python
Rust example
use unidecode_rs::unidecode;
fn main() {
println!("{}", unidecode("PŘÍLIŠ ŽLUŤOUČKÝ KŮŇ")); // PRILIS ZLUTOUCKY KUN
}
Install / build (Rust only)
cargo add unidecode-rs
# or add manually in Cargo.toml then
cargo build
Build the Python extension (development)
Prerequisites: Rust stable, Python ≥3.8, pip.
python -m venv .venv
source .venv/bin/activate
pip install --upgrade pip maturin
maturin develop --release --features python
python -c "import unidecode_rs; print(unidecode_rs.unidecode('déjà vu'))"
To build a distributable wheel:
maturin build --release --features python -o dist/
# Wheels are placed in dist/ directory
pip install dist/unidecode_pyo3-*.whl
Or install from PyPI:
pip install unidecode-pyo3
Python API
import unidecode_rs
print(unidecode_rs.unidecode("Příliš žluťoučký kůň"))
Minimal API: single function unidecode(text: str, errors: Optional[str] = None, replace_str: Optional[str] = None) -> str.
Idempotence - what is it?
A function is idempotent if applying it multiple times yields the same result as applying it once. Here:
unidecode(unidecode(s)) == unidecode(s)
After the first transliteration the output is pure ASCII; a second pass does nothing. A dedicated test validates this over multi‑script samples.
Golden tests (Python parity)
golden_equivalence tests run the Python Unidecode library in a subprocess and diff outputs across samples (Latin + accents, Cyrillic, Greek, CJK, emoji). Any mismatch fails the test.
Targeted run:
cargo test -- --nocapture golden_equivalence
Coverage & critical paths
Dispatch design:
- Presence bitmap per 256‑codepoint block (
BLOCK_BITMAPS) for quick negative checks. - Large generated
matchproviding PHF table access per block.
Extra tests (lookup_paths.rs + internal tests in lib.rs) exercise:
- Bit zero ⇒
lookupreturnsNone(negative path) - Bit one ⇒
lookupreturns non‑empty string - Out‑of‑range block ⇒ early exit
- ASCII parity / idempotence
Generate local report via cargo llvm-cov (alias if configured). Detailed guidance moved to docs/COVERAGE.md.
cargo llvm-cov --html
# Or use the provided script:
./scripts/coverage.sh
Upstream test harness
Beyond Rust & golden tests, a Python harness reuses the original upstream Unidecode test suite to assert behavioral parity.
Main file: tests/python/test_reference_suite.py
Characteristics:
- Dynamically loads the upstream base test class (via
_reference/upstream_loader.py). - Monkeypatches
unidecode.unidecodeto point to the Rust implementation (unidecode_rs.unidecode). - Implements full
errors=modes (ignore,replace,strict,preserve) for parity. - Overrides surrogate tests with lean variants to avoid warning noise while maintaining assertions.
Run only this suite:
pytest -q tests/python/test_reference_suite.py
Expected (evolving) report:
14 passed, 2 xfailed, 4 xpassed # exemple actuel
xfail / xpass policy:
- Temporary
xfailremoved once feature implemented; a formerxfailthat passes becomes a normal pass.
Parity roadmap:
- (Done) Implement
errors=modes. - Finalize surrogate handling parity (optional warning replication toggle).
- Extend tables to cover remaining mathematical alphanumeric symbols not yet mapped (e.g., script variants currently partial).
- Add multi‑corpus benchmarks (Latin, mixed CJK, emoji) for stable metrics.
- Provide exhaustive table diff script (block by block) with machine‑readable output.
Current limitations:
- Some mathematical script / stylistic letter ranges may still map to empty until table extension is complete.
- Generated table lines unexecuted in coverage are data-only, low semantic value.
How to contribute:
- Add a targeted parity test (Rust or Python) reproducing a divergence.
- Extend the table or adjust logic.
- Run
pytest tests/python/test_reference_suite.pyandcargo test. - Update this section if a batch of former gaps is closed.
Performance
A micro performance snapshot in golden_equivalence.rs::performance_snapshot runs 5 iterations on mixed‑script text vs Python. Numbers are indicative only; for robust measurement use Criterion benchmarks or the comparison script:
python scripts/bench_compare.py
Philosophy
- Fidelity: match Python before adding new rules.
- Safety: no panics for any valid Unicode scalar value.
- Performance: avoid unnecessary copies (ASCII fast path, heuristic pre‑allocation).
- Maintainability: generated code isolated, core logic compact.
Development / tests
cargo test
# (optional) fallback feature using deunicode
cargo test --features fallback-deunicode
Python tests (after building extension):
pytest tests/python
License
GPL-3.0-or-later. Tables derived from public data of the Python Unidecode project.
Acknowledgements
- Original Python project Unidecode
- Rust & PyO3 community
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file unidecode_pyo3-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: unidecode_pyo3-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 7.5 kB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9ad0776cb1cb13fea82b634933a21faf4b1b438919037a27212c3a3f80e98f35
|
|
| MD5 |
2fa3472ba2f9db889dcb9eb016c9c6cd
|
|
| BLAKE2b-256 |
7af7ea80b225b5cb6df50775e09a5441659ac0ba58da8957098767aa1dcabd77
|
Provenance
The following attestation bundles were made for unidecode_pyo3-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
publish-pypi-oidc.yml on gmaOCR/unidecode-rs
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
unidecode_pyo3-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
9ad0776cb1cb13fea82b634933a21faf4b1b438919037a27212c3a3f80e98f35 - Sigstore transparency entry: 575807238
- Sigstore integration time:
-
Permalink:
gmaOCR/unidecode-rs@fbd4ecc281cc8d47f5f4d9d6842a64c32b833ff1 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/gmaOCR
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi-oidc.yml@fbd4ecc281cc8d47f5f4d9d6842a64c32b833ff1 -
Trigger Event:
push
-
Statement type: