unicode crate - #8211
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (3)
📒 Files selected for processing (42)
💤 Files with no reviewable changes (2)
✅ Files skipped from review due to trivial changes (3)
🚧 Files skipped from review as they are similar to previous changes (24)
📝 WalkthroughWalkthroughThis PR adds a shared ChangesShared Unicode crate extraction and adoption
Estimated code review effort: 4 (Complex) | ~75 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
📦 Library DependenciesThe following Lib/ modules were modified. Here are their dependencies: [x] lib: cpython/Lib/pkgutil.py dependencies:
dependent tests: (10 tests)
[x] lib: cpython/Lib/re dependencies:
dependent tests: (81 tests)
Legend:
|
| #[inline] | ||
| pub(crate) fn is_word(ch: u32) -> bool { | ||
| ch == '_' as u32 || u8::try_from(ch).is_ok_and(|x| x.is_ascii_alphanumeric()) | ||
| unicode::regex::is_word(ch) |
There was a problem hiding this comment.
pretty much all changes in this file can follow that pattern I believe
| )); | ||
|
|
||
| #[derive(Clone, Copy)] | ||
| #[repr(u8)] |
There was a problem hiding this comment.
Is the repr(u8) needed?
| NUMERIC_VAL_EXISTS_32 | ||
| .binary_search_by(|&(start, end)| { | ||
| let ch = ch as u32; | ||
| if ch > end { |
💪 |
81b7e6e to
fdb08aa
Compare
|
@ShaharNaveh thank you so much. could you give another look? |
ShaharNaveh
left a comment
There was a problem hiding this comment.
feel free to dismiss this if it's not posible
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/stdlib/src/unicodedata.rs (1)
101-130: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winKeep
lookupandnameversion-aware. Both methods still go throughunicode_core::lookup_character/character_name, which ignoreself.modern, sounicodedata.ucd_3_2_0still uses the latest name table. Restore the tracking note or route these through the legacy UCD path.🤖 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 `@crates/stdlib/src/unicodedata.rs` around lines 101 - 130, The lookup and name methods in unicodedata are still using the shared unicode_core tables and bypassing the version-specific behavior tied to self.modern, so ucd_3_2_0 is not honoring the legacy name data. Update unicodedata::lookup and unicodedata::name to branch on self.modern or use the legacy UCD path so they remain version-aware, and keep the existing version-tracking behavior consistent with the other UnicodeData methods in this module.Source: Coding guidelines
🧹 Nitpick comments (3)
crates/stdlib/src/unicodedata.rs (1)
15-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate the parsing logic already provided by
NormalizeForm::FromStr.
rustpython_unicode::NormalizeFormalready implementsFromStrwith the exact same"NFC"/"NFKC"/"NFD"/"NFKD"mapping. Re-implementing it here means the two copies can drift if a new form is ever added upstream.♻️ Proposed refactor to reuse `NormalizeForm::FromStr`
impl<'a> TryFromBorrowedObject<'a> for NormalizeFormArg { fn try_from_borrowed_object(vm: &VirtualMachine, obj: &'a PyObject) -> PyResult<Self> { obj.try_value_with( - |form: &PyStr| match form.as_bytes() { - b"NFC" => Ok(Self(NormalizeForm::Nfc)), - b"NFKC" => Ok(Self(NormalizeForm::Nfkc)), - b"NFD" => Ok(Self(NormalizeForm::Nfd)), - b"NFKD" => Ok(Self(NormalizeForm::Nfkd)), - _ => Err(vm.new_value_error("invalid normalization form")), - }, + |form: &PyStr| { + form.to_str() + .and_then(|s| s.parse().ok()) + .map(Self) + .ok_or_else(|| vm.new_value_error("invalid normalization form")) + }, vm, ) } }🤖 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 `@crates/stdlib/src/unicodedata.rs` around lines 15 - 29, The normalization form parsing in NormalizeFormArg duplicates the existing NormalizeForm::FromStr mapping, so refactor the TryFromBorrowedObject implementation to delegate to that parser instead of matching on the byte strings directly. Update the logic in unicodedata::NormalizeFormArg::try_from_borrowed_object to convert the PyStr into a Rust string and parse it through NormalizeForm::from_str, keeping the same invalid-input error handling.crates/unicode/src/classify.rs (1)
99-109: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRedundant round-trip in test iteration.
('\0'..='\u{2FFFF}')already yieldscharvalues (Rust's char range excludes surrogates), so.filter_map(|c| char::from_u32(c as u32))always returnsSome(c)unchanged — it never filters anything.♻️ Simplify the iteration
- for c in ('\0'..='\u{2FFFF}').filter_map(|c| char::from_u32(c as u32)) { + for c in '\0'..='\u{2FFFF}' {🤖 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 `@crates/unicode/src/classify.rs` around lines 99 - 109, The iteration in numeric_type_chain_holds is doing a redundant char::from_u32 round-trip because the '\0'..='\u{2FFFF}' range already produces valid char values; simplify the loop to iterate directly over the char range and remove the filter_map conversion. Keep the rest of the assertions in numeric_type_chain_holds unchanged.crates/unicode/tests/differential.rs (1)
28-52: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
is_continue(XID_Continue) is never differentially validated.
crate_predicate/classify_is_identifier_charonly exerciseidentifier::is_startvia the single-charisidentifierreference data. There's no equivalent sweep validatingidentifier::is_continue(used for all characters after the first instr.isidentifier) against CPython, so a divergence in XID_Continue handling would go undetected by this suite.Consider adding a second reference predicate (e.g.
"isident_continue", generated ingenerate_reference.pyvia('_' + chr(cp)).isidentifier()sliced appropriately, or directly viaunicodedata-adjacent XID_Continue data) and mapping it here toidentifier::is_continue.🤖 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 `@crates/unicode/tests/differential.rs` around lines 28 - 52, The differential test only validates identifier start behavior through `classify_is_identifier_char` and `identifier::is_start`, so XID_Continue is never compared against CPython. Add a second predicate path in `crate_predicate` for identifier continuation (for example an `"isident_continue"` case) and map it to `rustpython_unicode::identifier::is_continue`. Update the reference generation to produce a matching single-character continue oracle, so the test suite exercises both start and continue behavior.
🤖 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.
Inline comments:
In `@crates/stdlib/src/unicodedata.rs`:
- Around line 157-160: The is_normalized method in unicodedata::is_normalized is
using a raw ICU byte path that does not match normalize’s WTF-8-aware handling.
Update is_normalized to follow the same UnicodeCore/normalize logic used by
normalize so lone surrogates are preserved and both methods classify the same
input consistently. Keep the change localized to the is_normalized pymethod and
reuse the same WTF-8-aware conversion/path as normalize rather than calling
unicode_core::is_normalized directly.
In `@crates/unicode/Cargo.toml`:
- Line 17: The crates/unicode dependency on unicode_names2 needs its no_std
support enabled because this crate is marked #![no_std]. Update the
unicode_names2 entry in Cargo.toml to turn on the library’s no_std feature while
keeping the existing workspace-based dependency setup intact.
---
Outside diff comments:
In `@crates/stdlib/src/unicodedata.rs`:
- Around line 101-130: The lookup and name methods in unicodedata are still
using the shared unicode_core tables and bypassing the version-specific behavior
tied to self.modern, so ucd_3_2_0 is not honoring the legacy name data. Update
unicodedata::lookup and unicodedata::name to branch on self.modern or use the
legacy UCD path so they remain version-aware, and keep the existing
version-tracking behavior consistent with the other UnicodeData methods in this
module.
---
Nitpick comments:
In `@crates/stdlib/src/unicodedata.rs`:
- Around line 15-29: The normalization form parsing in NormalizeFormArg
duplicates the existing NormalizeForm::FromStr mapping, so refactor the
TryFromBorrowedObject implementation to delegate to that parser instead of
matching on the byte strings directly. Update the logic in
unicodedata::NormalizeFormArg::try_from_borrowed_object to convert the PyStr
into a Rust string and parse it through NormalizeForm::from_str, keeping the
same invalid-input error handling.
In `@crates/unicode/src/classify.rs`:
- Around line 99-109: The iteration in numeric_type_chain_holds is doing a
redundant char::from_u32 round-trip because the '\0'..='\u{2FFFF}' range already
produces valid char values; simplify the loop to iterate directly over the char
range and remove the filter_map conversion. Keep the rest of the assertions in
numeric_type_chain_holds unchanged.
In `@crates/unicode/tests/differential.rs`:
- Around line 28-52: The differential test only validates identifier start
behavior through `classify_is_identifier_char` and `identifier::is_start`, so
XID_Continue is never compared against CPython. Add a second predicate path in
`crate_predicate` for identifier continuation (for example an
`"isident_continue"` case) and map it to
`rustpython_unicode::identifier::is_continue`. Update the reference generation
to produce a matching single-character continue oracle, so the test suite
exercises both start and continue behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 2989b853-22c9-46d0-afdb-5d2205d8a519
⛔ Files ignored due to path filters (2)
Cargo.lockis excluded by!**/*.lockLib/test/test_re.pyis excluded by!Lib/**
📒 Files selected for processing (40)
Cargo.tomlcrates/codegen/Cargo.tomlcrates/codegen/src/string_parser.rscrates/common/Cargo.tomlcrates/common/src/encodings.rscrates/literal/Cargo.tomlcrates/literal/src/char.rscrates/literal/src/escape.rscrates/literal/src/lib.rscrates/sre_engine/Cargo.tomlcrates/sre_engine/src/string.rscrates/stdlib/Cargo.tomlcrates/stdlib/build.rscrates/stdlib/src/unicodedata.rscrates/unicode/Cargo.tomlcrates/unicode/build.rscrates/unicode/src/case.rscrates/unicode/src/classify.rscrates/unicode/src/data.rscrates/unicode/src/identifier.rscrates/unicode/src/lib.rscrates/unicode/src/normalize.rscrates/unicode/tests/data/cpython3.14_predicates.txtcrates/unicode/tests/data/version_skew_cpython3.14.txtcrates/unicode/tests/differential.rscrates/unicode/tests/generate_reference.pycrates/unicode/unicode/README.mdcrates/unicode/unicode/latest/DerivedNumericValues.txtcrates/unicode/unicode/latest/NormalizationCorrections.txtcrates/unicode/unicode/latest/UnicodeData.txtcrates/unicode/unicode/ucd32/DerivedBidiClass-3.2.0.txtcrates/unicode/unicode/ucd32/DerivedBinaryProperties-3.2.0.txtcrates/unicode/unicode/ucd32/DerivedCombiningClass-3.2.0.txtcrates/unicode/unicode/ucd32/DerivedEastAsianWidth-3.2.0.txtcrates/unicode/unicode/ucd32/DerivedGeneralCategory-3.2.0.txtcrates/unicode/unicode/ucd32/DerivedNumericType-3.2.0.txtcrates/unicode/unicode/ucd32/DerivedNumericValues-3.2.0.txtcrates/vm/Cargo.tomlcrates/vm/src/builtins/str.rsextra_tests/snippets/stdlib_unicode_shared.py
💤 Files with no reviewable changes (2)
- crates/literal/src/lib.rs
- crates/literal/src/char.rs
There was a problem hiding this comment.
🧹 Nitpick comments (1)
extra_tests/snippets/stdlib_unicode_shared.py (1)
83-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRuff RUF001 false positive — intentional Unicode digit test.
The flagged ambiguous-character warning on line 84 is expected here since the test intentionally exercises non-ASCII decimal digits. Consider adding a
# noqa: RUF001if you want clean lint output, otherwise safe to ignore.🤖 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 `@extra_tests/snippets/stdlib_unicode_shared.py` around lines 83 - 86, The Unicode digit assertions in the test are intentional, so the Ruff RUF001 warning should be silenced where the non-ASCII decimal digit cases are exercised. Update the relevant assert in stdlib_unicode_shared.py, near the unicode digit fullmatch checks in the test snippet, to explicitly suppress RUF001 so the intentional ambiguity does not fail linting.Source: Linters/SAST tools
🤖 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 `@extra_tests/snippets/stdlib_unicode_shared.py`:
- Around line 83-86: The Unicode digit assertions in the test are intentional,
so the Ruff RUF001 warning should be silenced where the non-ASCII decimal digit
cases are exercised. Update the relevant assert in stdlib_unicode_shared.py,
near the unicode digit fullmatch checks in the test snippet, to explicitly
suppress RUF001 so the intentional ambiguity does not fail linting.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: faada467-a1a0-4608-bd41-f55bd8d52f5e
⛔ Files ignored due to path filters (2)
Lib/test/test_pkgutil.pyis excluded by!Lib/**Lib/test/test_re.pyis excluded by!Lib/**
📒 Files selected for processing (11)
crates/literal/src/char.rscrates/literal/src/escape.rscrates/literal/src/lib.rscrates/sre_engine/src/string.rscrates/unicode/src/case.rscrates/unicode/src/classify.rscrates/unicode/src/data.rscrates/unicode/src/identifier.rscrates/unicode/src/lib.rscrates/unicode/src/normalize.rsextra_tests/snippets/stdlib_unicode_shared.py
💤 Files with no reviewable changes (3)
- crates/literal/src/lib.rs
- crates/literal/src/char.rs
- crates/unicode/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (7)
- crates/unicode/src/identifier.rs
- crates/literal/src/escape.rs
- crates/unicode/src/classify.rs
- crates/unicode/src/normalize.rs
- crates/unicode/src/case.rs
- crates/unicode/src/data.rs
- crates/sre_engine/src/string.rs
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/unicode/src/normalize.rs (1)
81-111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd NFKC/NFKD coverage to
is_normalizedtests.Tests exercise Nfc/Nfd round-tripping and the surrogate-skip behavior, but no case exercises
NormalizeForm::Nfkc/Nfkdbranches ofis_normalized. Since these are separate match arms dispatching to distinct ICU normalizer types, a regression there wouldn't be caught by the current suite.✅ Suggested addition
#[test] fn is_normalized_skips_lone_surrogates() { // A lone surrogate splits the text into UTF-8 runs; each run is checked // independently, so a surrogate next to normalized text stays normalized. let mut buf = Wtf8Buf::from("é"); buf.push(CodePoint::from_u32(0xD800).unwrap()); assert!(is_normalized(NormalizeForm::Nfc, &buf)); assert!(!is_normalized(NormalizeForm::Nfd, &buf)); } + + #[test] + fn is_normalized_compatibility_forms() { + let composed = Wtf8Buf::from("fi"); // U+FB01 LATIN SMALL LIGATURE FI + assert!(is_normalized(NormalizeForm::Nfc, &composed)); + assert!(!is_normalized(NormalizeForm::Nfkc, &composed)); + assert!(!is_normalized(NormalizeForm::Nfkd, &composed)); + } }🤖 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 `@crates/unicode/src/normalize.rs` around lines 81 - 111, The current tests in normalize.rs cover NFC/NFD only, so add `is_normalized` cases that explicitly exercise the `NormalizeForm::Nfkc` and `NormalizeForm::Nfkd` match arms. Extend the `tests` module with assertions using a string that differs under compatibility normalization (for example, a compatibility character plus its normalized equivalent) and verify both `is_normalized` and `normalize` behavior for `Nfkc` and `Nfkd`, alongside the existing `normalization_round_trips` coverage.
🤖 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 `@crates/unicode/src/normalize.rs`:
- Around line 81-111: The current tests in normalize.rs cover NFC/NFD only, so
add `is_normalized` cases that explicitly exercise the `NormalizeForm::Nfkc` and
`NormalizeForm::Nfkd` match arms. Extend the `tests` module with assertions
using a string that differs under compatibility normalization (for example, a
compatibility character plus its normalized equivalent) and verify both
`is_normalized` and `normalize` behavior for `Nfkc` and `Nfkd`, alongside the
existing `normalization_round_trips` coverage.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 86332721-2186-4d06-a746-01fb6abb1293
📒 Files selected for processing (4)
crates/stdlib/src/unicodedata.rscrates/unicode/src/data.rscrates/unicode/src/normalize.rscrates/unicode/tests/differential.rs
💤 Files with no reviewable changes (1)
- crates/unicode/src/data.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- crates/unicode/tests/differential.rs
- crates/stdlib/src/unicodedata.rs
Move the vm-free Unicode character database access — the generated UCD 3.2.0 / latest tables, their build-time generation, and the icu4x/unicode_names2 lookups for category/bidirectional/combining/east_asian_width/mirrored/decomposition/ normalize/is_normalized/digit/decimal/numeric/name/lookup — into a new leaf crate over char/CodePoint/&Wtf8. stdlib/unicodedata.rs keeps the UCD pyclass binding: it extracts the code point, boxes results, and maps errors. The unicode data files, build.rs table generation, and the icu_properties/icu_normalizer/unicode_names2 dependencies move out of stdlib. Assisted-by: Claude
Add classify/case/identifier modules to the shared crate over char/&str/&Wtf8: isalpha/isalnum/isdecimal/isdigit/isnumeric/isspace/isprintable classification, XID identifier predicates, and full-mapping casefold. vm/builtins/str.rs and literal/char.rs now call these instead of icu_properties/icu_casemap directly. String-level iteration with final-sigma handling (lower/upper/title/capitalize, islower/isupper) stays in the runtime. Assisted-by: Claude
Add the regex module with the SRE character-class and case predicates (is_word/is_space/is_digit, is_uni_* Unicode variants, ascii/locale/unicode case folding). sre_engine/string.rs now forwards to it, keeping its public API and behavior identical — including the ASCII-only is_uni_digit and the hardcoded is_uni_space table. Assisted-by: Claude
codegen/string_parser.rs (\N{...} escapes) and common/encodings.rs (the
namereplace error handler) now resolve character names via rustpython-unicode
instead of depending on unicode_names2 directly, leaving the crate as the sole
owner of the name database.
Assisted-by: Claude
tests/differential.rs sweeps the full 0..0x110000 range and compares each str
classification predicate against a committed CPython 3.14 reference dataset
(tests/data/cpython3.14_predicates.txt, produced by generate_reference.py). Code
points that differ only because the Rust std / icu4x build ships a later Unicode
release than CPython 3.14's 16.0.0 are recorded in
tests/data/version_skew_cpython3.14.txt, regenerable via
RUSTPYTHON_UNICODE_REGEN_SKEW=1; the regen refuses to record any
cpython=true/crate=false divergence, so only newly-assigned code points are
allowed. Any other divergence fails. Both data files use a run-length
`predicate start:end,...` encoding.
extra_tests/snippets/stdlib_unicode_shared.py exercises the routed surface end to
end (str predicates, casefold, identifiers, unicodedata, normalize, \N{}, and re
character classes) and passes identically on CPython 3.14 and RustPython.
Assisted-by: Claude
is_uni_digit previously matched only ASCII 0-9, so re's \d in Unicode mode missed decimal digits like ٥ and ५. SRE_UNI_IS_DIGIT matches Py_UNICODE_ISDECIMAL (category Nd), so route it through classify::is_decimal. Unmasks test_bug_6561 in test_re and extends the shared-crate snippet with the Nd/Nl/No cases. Assisted-by: Claude
Address review feedback on the extraction. Functions that only forwarded to the shared crate become `use ... as` re-exports rather than hand-written wrappers: - literal: is_printable is dropped; escape.rs calls rustpython_unicode::classify::is_repr_printable directly. - unicode::identifier::is_continue re-exports is_xid_continue. - unicode::data::lookup_character re-exports unicode_names2::character. - Drop the unused repr(u8) on DecompositionType. The SRE character-class and case predicates move back into sre_engine::string (their pre-extraction home) instead of living in a unicode::regex module that only sre_engine and the vm _sre binding used; is_uni_digit/is_uni_alnum still delegate to rustpython_unicode::classify. engine.rs and _sre consume them from sre_engine::string again. The unicode crate's tests move from a single root tests module in lib.rs into per-module test submodules (case, classify, data, identifier, normalize). Assisted-by: Claude
- differential.rs: move the sweep tests into a `mod tests` block and drop the file-level allow of clippy::tests_outside_test_module and std_instead_of_alloc; the test uses alloc collections instead. - data.rs: remove the never-constructed DecompositionType::Canonical variant and its allow(unused); compatibility decomposition never produces it and canonical decomposition is handled through icu4x. - normalize.rs: is_normalized now takes &Wtf8 and checks each UTF-8 run, skipping lone surrogates, matching normalize's run-wise behavior. - unicodedata.rs: pass as_wtf8() to is_normalized. Assisted-by: Claude
rustpython-unicodecrate and route core Unicode semantics through it #7561Summary
Summary by CodeRabbit
recharacter-class handling (\w,\d,\s), including better case-insensitive matching.unicodedatato use unified Unicode property data and updated Unicode version reporting.