Skip to content

unicode crate - #8211

Merged
youknowone merged 9 commits into
RustPython:mainfrom
youknowone:unicode
Jul 7, 2026
Merged

unicode crate#8211
youknowone merged 9 commits into
RustPython:mainfrom
youknowone:unicode

Conversation

@youknowone

@youknowone youknowone commented Jul 5, 2026

Copy link
Copy Markdown
Member

Summary

Summary by CodeRabbit

  • New Features
    • Expanded Unicode support for case folding and normalization, Unicode character-name lookup, and identifier start/continue checks.
    • Improved Unicode-aware string escaping and re character-class handling (\w, \d, \s), including better case-insensitive matching.
    • Refreshed unicodedata to use unified Unicode property data and updated Unicode version reporting.
  • Bug Fixes
    • More consistent Python-aligned behavior for printability/whitespace and lone-surrogate handling.
  • Tests
    • Added CPython-referenced differential predicate coverage and shared Unicode stdlib snippet coverage.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 369ef061-cc70-43da-a4ff-d95c0be5dc1b

📥 Commits

Reviewing files that changed from the base of the PR and between 9207dd4 and a6271d2.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • Lib/test/test_pkgutil.py is excluded by !Lib/**
  • Lib/test/test_re.py is excluded by !Lib/**
📒 Files selected for processing (42)
  • Cargo.toml
  • crates/codegen/Cargo.toml
  • crates/codegen/src/string_parser.rs
  • crates/common/Cargo.toml
  • crates/common/src/encodings.rs
  • crates/literal/Cargo.toml
  • crates/literal/src/char.rs
  • crates/literal/src/escape.rs
  • crates/literal/src/lib.rs
  • crates/sre_engine/Cargo.toml
  • crates/sre_engine/src/string.rs
  • crates/stdlib/Cargo.toml
  • crates/stdlib/build.rs
  • crates/stdlib/src/unicodedata.rs
  • crates/unicode/Cargo.toml
  • crates/unicode/build.rs
  • crates/unicode/src/case.rs
  • crates/unicode/src/classify.rs
  • crates/unicode/src/data.rs
  • crates/unicode/src/identifier.rs
  • crates/unicode/src/lib.rs
  • crates/unicode/src/normalize.rs
  • crates/unicode/tests/data/cpython3.14_predicates.txt
  • crates/unicode/tests/data/version_skew_cpython3.14.txt
  • crates/unicode/tests/differential.rs
  • crates/unicode/tests/generate_reference.py
  • crates/unicode/unicode/README.md
  • crates/unicode/unicode/latest/DerivedNumericValues.txt
  • crates/unicode/unicode/latest/NormalizationCorrections.txt
  • crates/unicode/unicode/latest/UnicodeData.txt
  • crates/unicode/unicode/ucd32/DerivedBidiClass-3.2.0.txt
  • crates/unicode/unicode/ucd32/DerivedBinaryProperties-3.2.0.txt
  • crates/unicode/unicode/ucd32/DerivedCombiningClass-3.2.0.txt
  • crates/unicode/unicode/ucd32/DerivedEastAsianWidth-3.2.0.txt
  • crates/unicode/unicode/ucd32/DerivedGeneralCategory-3.2.0.txt
  • crates/unicode/unicode/ucd32/DerivedNumericType-3.2.0.txt
  • crates/unicode/unicode/ucd32/DerivedNumericValues-3.2.0.txt
  • crates/vm/Cargo.toml
  • crates/vm/src/builtins/str.rs
  • crates/wtf8/Cargo.toml
  • crates/wtf8/src/lib.rs
  • extra_tests/snippets/stdlib_unicode_shared.py
💤 Files with no reviewable changes (2)
  • crates/literal/src/lib.rs
  • crates/literal/src/char.rs
✅ Files skipped from review due to trivial changes (3)
  • crates/wtf8/Cargo.toml
  • crates/wtf8/src/lib.rs
  • crates/unicode/tests/data/version_skew_cpython3.14.txt
🚧 Files skipped from review as they are similar to previous changes (24)
  • crates/vm/Cargo.toml
  • crates/unicode/Cargo.toml
  • crates/unicode/src/lib.rs
  • crates/common/src/encodings.rs
  • crates/unicode/src/identifier.rs
  • crates/unicode/tests/generate_reference.py
  • crates/codegen/src/string_parser.rs
  • crates/literal/Cargo.toml
  • crates/sre_engine/Cargo.toml
  • crates/stdlib/build.rs
  • crates/common/Cargo.toml
  • crates/unicode/src/classify.rs
  • crates/unicode/tests/differential.rs
  • crates/unicode/src/normalize.rs
  • crates/unicode/src/case.rs
  • crates/codegen/Cargo.toml
  • crates/unicode/build.rs
  • crates/stdlib/Cargo.toml
  • Cargo.toml
  • crates/unicode/src/data.rs
  • crates/sre_engine/src/string.rs
  • crates/stdlib/src/unicodedata.rs
  • crates/literal/src/escape.rs
  • crates/vm/src/builtins/str.rs

📝 Walkthrough

Walkthrough

This PR adds a shared rustpython-unicode crate for Unicode classification, identifiers, casefolding, normalization, and UCD queries. It updates runtime, stdlib, and helper crates to use those APIs, and adds reference and differential tests for CPython-aligned Unicode behavior.

Changes

Shared Unicode crate extraction and adoption

Layer / File(s) Summary
Workspace and crate wiring
Cargo.toml, crates/unicode/Cargo.toml, crates/unicode/src/lib.rs, crates/wtf8/Cargo.toml, crates/wtf8/src/lib.rs, crates/codegen/Cargo.toml, crates/common/Cargo.toml, crates/literal/Cargo.toml, crates/sre_engine/Cargo.toml, crates/stdlib/Cargo.toml, crates/vm/Cargo.toml
Adds the workspace dependency entry, the new rustpython-unicode crate entrypoint, and related dependency feature changes.
Unicode tables and data access
crates/unicode/build.rs, crates/unicode/src/data.rs
Adds the Unicode build script and data layer that generate and expose Unicode tables, lookups, normalization, and numeric metadata.
Classification, casefold, identifier, and normalization helpers
crates/unicode/src/case.rs, crates/unicode/src/classify.rs, crates/unicode/src/identifier.rs, crates/unicode/src/normalize.rs
Adds the predicate and transformation helpers used by runtimes for Unicode classification, casefolding, identifiers, and normalization.
Runtime Unicode call sites
crates/codegen/src/string_parser.rs, crates/common/src/encodings.rs, crates/literal/src/*, crates/sre_engine/src/string.rs, crates/vm/src/builtins/str.rs
Updates codegen, encoding escapes, regex predicates, and Python string methods to call the shared Unicode helpers.
unicodedata delegation and validation
crates/stdlib/build.rs, crates/stdlib/src/unicodedata.rs
Removes stdlib Unicode generation and delegates unicodedata queries and normalization through the shared crate.
Unicode differential and reference tests
crates/unicode/tests/*, extra_tests/snippets/stdlib_unicode_shared.py
Adds CPython reference data, version-skew allow-list data, the differential test harness, the Python reference generator, and an integration snippet.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

Suggested reviewers: ShaharNaveh

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is too generic to identify the main change in the PR. Use a concise, specific title such as "extract shared Unicode semantics crate".
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR adds the shared unicode crate and routes str, re, literal, and unicodedata logic through it, matching the linked goals.
Out of Scope Changes check ✅ Passed The remaining edits support the Unicode extraction work and don't show clear unrelated scope creep.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[x] lib: cpython/Lib/pkgutil.py
[x] test: cpython/Lib/test/test_pkgutil.py

dependencies:

  • pkgutil

dependent tests: (10 tests)

  • pkgutil: test_pkgutil test_pydoc test_pyrepl test_runpy
    • pydoc: test_enum
      • pdb: test_pdb
      • xmlrpc.server: test_docxmlrpc test_xmlrpc
    • runpy:
      • profile: test_profile
      • trace: test_trace

[x] lib: cpython/Lib/re
[x] lib: cpython/Lib/sre_compile.py
[x] lib: cpython/Lib/sre_constants.py
[x] lib: cpython/Lib/sre_parse.py
[ ] test: cpython/Lib/test/test_re.py (TODO: 13)
[x] test: cpython/Lib/test/re_tests.py

dependencies:

  • re

dependent tests: (81 tests)

  • re: test_android test_ast test_asyncio test_binascii test_builtin test_bytes test_clinic test_cmd_line test_ctypes test_dict test_dis test_docxmlrpc test_dtrace test_email test_embed test_faulthandler test_filecmp test_fileinput test_fnmatch test_format test_frame test_fstring test_functools test_future_stmt test_generated_cases test_genericalias test_glob test_hashlib test_http_cookiejar test_httplib test_httpservers test_imaplib test_importlib test_inspect test_ipaddress test_launcher test_logging test_mailbox test_mmap test_optparse test_ordered_dict test_platform test_pprint test_pydoc test_pyexpat test_pyrepl test_re test_regrtest test_runpy test_set test_site test_smtplib test_socket test_ssl test_strftime test_strptime test_strtod test_structseq test_symtable test_syntax test_sys test_sysconfig test_tarfile test_tempfile test_tokenize test_tools test_traceback test_turtle test_typing test_unittest test_unparse test_venv test_webbrowser test_winapi test_with test_wsgiref test_xmlrpc test_zipfile test_zipimport test_zoneinfo test_zstd

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

Comment thread crates/literal/src/char.rs Outdated
Comment thread crates/sre_engine/src/string.rs Outdated
#[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)

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.

same

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.

pretty much all changes in this file can follow that pattern I believe

Comment thread crates/unicode/src/data.rs Outdated
));

#[derive(Clone, Copy)]
#[repr(u8)]

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.

Is the repr(u8) needed?

Comment thread crates/unicode/src/data.rs
NUMERIC_VAL_EXISTS_32
.binary_search_by(|&(start, end)| {
let ch = ch as u32;
if ch > end {

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.

same

Comment thread crates/unicode/src/data.rs Outdated
Comment thread crates/unicode/src/identifier.rs Outdated
@ShaharNaveh

Copy link
Copy Markdown
Contributor

UNEXPECTED SUCCESS: test_name_resolution (test.test_pkgutil.PkgutilTests.test_name_resolution)

💪

@youknowone
youknowone force-pushed the unicode branch 2 times, most recently from 81b7e6e to fdb08aa Compare July 6, 2026 05:06
@youknowone
youknowone requested a review from ShaharNaveh July 6, 2026 05:09
@youknowone
youknowone marked this pull request as ready for review July 6, 2026 05:10
@youknowone

Copy link
Copy Markdown
Member Author

@ShaharNaveh thank you so much. could you give another look?

@ShaharNaveh ShaharNaveh 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.

feel free to dismiss this if it's not posible

Comment thread crates/unicode/tests/differential.rs Outdated

@coderabbitai coderabbitai 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.

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 win

Keep lookup and name version-aware. Both methods still go through unicode_core::lookup_character/character_name, which ignore self.modern, so unicodedata.ucd_3_2_0 still 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 win

Duplicate the parsing logic already provided by NormalizeForm::FromStr.

rustpython_unicode::NormalizeForm already implements FromStr with 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 value

Redundant round-trip in test iteration.

('\0'..='\u{2FFFF}') already yields char values (Rust's char range excludes surrogates), so .filter_map(|c| char::from_u32(c as u32)) always returns Some(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_char only exercise identifier::is_start via the single-char isidentifier reference data. There's no equivalent sweep validating identifier::is_continue (used for all characters after the first in str.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 in generate_reference.py via ('_' + chr(cp)).isidentifier() sliced appropriately, or directly via unicodedata-adjacent XID_Continue data) and mapping it here to identifier::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

📥 Commits

Reviewing files that changed from the base of the PR and between 045a6d5 and fdb08aa.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • Lib/test/test_re.py is excluded by !Lib/**
📒 Files selected for processing (40)
  • Cargo.toml
  • crates/codegen/Cargo.toml
  • crates/codegen/src/string_parser.rs
  • crates/common/Cargo.toml
  • crates/common/src/encodings.rs
  • crates/literal/Cargo.toml
  • crates/literal/src/char.rs
  • crates/literal/src/escape.rs
  • crates/literal/src/lib.rs
  • crates/sre_engine/Cargo.toml
  • crates/sre_engine/src/string.rs
  • crates/stdlib/Cargo.toml
  • crates/stdlib/build.rs
  • crates/stdlib/src/unicodedata.rs
  • crates/unicode/Cargo.toml
  • crates/unicode/build.rs
  • crates/unicode/src/case.rs
  • crates/unicode/src/classify.rs
  • crates/unicode/src/data.rs
  • crates/unicode/src/identifier.rs
  • crates/unicode/src/lib.rs
  • crates/unicode/src/normalize.rs
  • crates/unicode/tests/data/cpython3.14_predicates.txt
  • crates/unicode/tests/data/version_skew_cpython3.14.txt
  • crates/unicode/tests/differential.rs
  • crates/unicode/tests/generate_reference.py
  • crates/unicode/unicode/README.md
  • crates/unicode/unicode/latest/DerivedNumericValues.txt
  • crates/unicode/unicode/latest/NormalizationCorrections.txt
  • crates/unicode/unicode/latest/UnicodeData.txt
  • crates/unicode/unicode/ucd32/DerivedBidiClass-3.2.0.txt
  • crates/unicode/unicode/ucd32/DerivedBinaryProperties-3.2.0.txt
  • crates/unicode/unicode/ucd32/DerivedCombiningClass-3.2.0.txt
  • crates/unicode/unicode/ucd32/DerivedEastAsianWidth-3.2.0.txt
  • crates/unicode/unicode/ucd32/DerivedGeneralCategory-3.2.0.txt
  • crates/unicode/unicode/ucd32/DerivedNumericType-3.2.0.txt
  • crates/unicode/unicode/ucd32/DerivedNumericValues-3.2.0.txt
  • crates/vm/Cargo.toml
  • crates/vm/src/builtins/str.rs
  • extra_tests/snippets/stdlib_unicode_shared.py
💤 Files with no reviewable changes (2)
  • crates/literal/src/lib.rs
  • crates/literal/src/char.rs

Comment thread crates/stdlib/src/unicodedata.rs
Comment thread crates/unicode/Cargo.toml

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
extra_tests/snippets/stdlib_unicode_shared.py (1)

83-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Ruff 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: RUF001 if 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

📥 Commits

Reviewing files that changed from the base of the PR and between fdb08aa and 2491341.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_pkgutil.py is excluded by !Lib/**
  • Lib/test/test_re.py is excluded by !Lib/**
📒 Files selected for processing (11)
  • crates/literal/src/char.rs
  • crates/literal/src/escape.rs
  • crates/literal/src/lib.rs
  • crates/sre_engine/src/string.rs
  • crates/unicode/src/case.rs
  • crates/unicode/src/classify.rs
  • crates/unicode/src/data.rs
  • crates/unicode/src/identifier.rs
  • crates/unicode/src/lib.rs
  • crates/unicode/src/normalize.rs
  • extra_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

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
crates/unicode/src/normalize.rs (1)

81-111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add NFKC/NFKD coverage to is_normalized tests.

Tests exercise Nfc/Nfd round-tripping and the surrogate-skip behavior, but no case exercises NormalizeForm::Nfkc/Nfkd branches of is_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

📥 Commits

Reviewing files that changed from the base of the PR and between 6018efb and 9207dd4.

📒 Files selected for processing (4)
  • crates/stdlib/src/unicodedata.rs
  • crates/unicode/src/data.rs
  • crates/unicode/src/normalize.rs
  • crates/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
@youknowone
youknowone merged commit 85e3ea8 into RustPython:main Jul 7, 2026
27 checks passed
@youknowone
youknowone deleted the unicode branch July 7, 2026 00:19
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.

rustpython-unicode

2 participants