Replaces: #9166
unicode_names2 fixed a bug with its aliases. Ironically, the bug
caused a test in test_snippets.py to pass. Bumping unicode_names2 to
4.0.0 fixes the bug and thus the extra 3.2.0 test fails. I fixed the
logic for name lookup for 3.2.0. Now the test correctly passes.
* Implement native Unicode 17 grapheme iteration
Add the CPython extended grapheme state machine, full-codepoint properties, and traced Segment/iterator types with positional index handling. Refresh canonical unicodedata tests through update_lib while retaining pending markers. Include a losslessly range-compressed exact-final property oracle and differential test.
Source-reviewed implementation; candidate runtime validation remains pending. Reference property generation completed against the pinned CPython Unicode extension.
Assisted-by: OpenAI Codex:model version unavailable
* Bind grapheme Segment methods to the Python object
Use the required Py<Segment> receiver for native descriptors; preserve payload ownership and representation behavior.
Assisted-by: OpenAI Codex:model version unavailable
* Use explicit object receivers for Segment descriptors
Keep the inherent impl on the local payload and receive &Py<Self>, as required for stdlib classes outside the VM crate.
Assisted-by: OpenAI Codex:model version unavailable
* Mark grapheme property lookups as must-use
Satisfy strict Clippy for the three pure property functions without changing their behavior.
Assisted-by: OpenAI Codex:model version unavailable
* Enable validated Unicode grapheme canonical tests
Remove seven stale expected-failure markers through update_lib metadata after native property parity, canonical methods, full Unicode 17 grapheme corpus, and reference-cycle GC validation.
Assisted-by: OpenAI Codex:model version unavailable
* Accept platform line endings in grapheme oracle header
Compare the complete provenance line without its line ending so the same exact Unicode 17 reference is accepted after Windows checkout conversion. Keep the complete code-point property sweep unchanged.
Assisted-by: OpenAI Codex:model version unavailable
* Keep grapheme property regression in test module
Move the existing complete Unicode 17 property regression into the existing cfg(test) module to satisfy strict Clippy. Preserve the fixture, assertions, code-point coverage, and platform-independent provenance line check unchanged.
Assisted-by: OpenAI Codex:model version unavailable
* Qualify grapheme iterator return type for generated bindings
Assisted-by: OpenAI Codex:model version unavailable
* Fix deprecated PyO3 code input import in benchmarks
Use the canonical code module path introduced in PyO3 0.30 without suppressing deprecation warnings.
Assisted-by: OpenAI Codex:model version unavailable
* Reuse helpers where appropriate
* Implement a helper for data that doesn't follow the pattern of the
rest of the Unicode files
* Use BTreeMap, which is deduped by key and sorted instead of sorting
a Vec
* ucd: Parse Unihan data for 3.2.0; unmask test
Python's 3.2.0 path uses the Unihan data for supplementary numeric
values. The Unihan data isn't bundled with the "full" data which means
we have to parse it ourselves. The full data is large, so I compressed
it with zlib.
AI disclosure: I asked an LLM to elaborate on why the full Unicode 3.2.0
data precludes \u5793. It explained that the code point is part of
Unihan. I wrote the parser myself. I linted the zlib snippet with AI.
Assisted-by: GPT-5.6: Luna
* Use spare_capacity_mut + set_len over transmute
* unicodedata: resolve name aliases and named sequences
Vendor Unicode 17 NameAliases.txt and NamedSequences.txt into
rustpython-unicode so unicodedata.lookup matches CPython: formal
aliases and named sequences on the modern view, none on ucd_3_2_0.
Tangut derived-name lookup is case-insensitive. \N{} still rejects
named sequences because they are not a single character.
Assisted-by: Claude
* test_ucn: drop expectedFailure on test_aliases
The name-alias tables make the test pass; leaving the marker
fails CI as an unexpected success.
Assisted-by: Claude
* unicode: reject whitespace in Tangut derived names
parse_hex_code does not skip spaces around the hex digits.
Assisted-by: Claude
This commit fixes most of UCD's names. The two main fixes are:
* Handling chars missing in 3.2.0 by checking for membership #8548
* Handling names not present in unicodenames
AI disclosure: I used AI for the initial research of this patch to
determine why the Tangut characters are missing. AI also assisted with a
bug fix where I neglected to parse the Tangut u32 as hex. I wrote all of
the code.
Assisted-by: GPT 5.6 Luna
Related: #8599
My original parser was monolithic and inflexible. It worked as intended
for the derived data files, but anything more complicated required hacky
code. For example, the original parser always expected to build a vector
of values, yet sometimes we required other data structures such as
BTreeMaps.
I split up the parser into helper functions that are both cleaner and
more flexible. The actual parser should still yield the same results -
this doesn't "fix" anything yet. However, this is preliminary work for
fixing more of UCD since we will need a more flexible parser for fixes
like the linked PR.
* Fix %-formatting of bytes precision, width and %c errors
`format_bytes` subtracted the byte length from the field width without
saturating, so `b'%1s' % (b'abcde',)` aborted with a capacity overflow.
A precision written as a bare dot now truncates to nothing, and `%r`/`%a`
on bytes now honour the precision and width like the other conversions.
`%c` with a bytes, bytearray or str argument that is not one unit long
now names the argument by its length instead of by its type.
Assisted-by: Claude Code:claude-opus-5
* Fix format spec handling of grouping, bool, 'c' and complex
A grouping option next to an unrecognised presentation type now reports
"Cannot specify ',' with 'a'." instead of falling through to the type
check. `format(True, spec)` now validates the spec the way an int does.
The 'c' type now pads and aligns its character, and an integer outside
the C long range raises OverflowError rather than reporting a code point
out of range.
A complex formatted with the 'n'/'g' types or with a precision and no
type now goes through the general format without forcing a fraction.
Assisted-by: Claude Code:claude-opus-5
* Round floats at a negative ndigits from the exact decimal digits
Dividing by a power of ten rounded twice: once when the quotient was
stored and again at the tie, so e.g. `round(2.5e2, -2)` came out as 300.0.
The digits now come from the exact decimal expansion of the value.
Assisted-by: Claude Code:claude-opus-5
* Report the requested base in int literal errors and reject a bare sign
An invalid literal parsed with base 0 named the base that the prefix
turned out to mean rather than 0. A string of nothing but a sign was
taken for the old octal form and returned 0 instead of raising.
Assisted-by: Claude Code:claude-opus-5
* Split, strip and expand tabs over the Py_UNICODE_ISSPACE set
`str.split`, `rsplit`, `strip`, `lstrip` and `rstrip` with no separator
used Rust's whitespace predicate, which differs from the 29 code points
`Py_UNICODE_ISSPACE` names: U+001C-U+001F and U+0085 were skipped and
U+200B was treated as a separator.
`expandtabs` went through `str`, so it dropped the lone surrogates of a
WTF-8 string; it now works on `Wtf8` throughout.
Indexing a str out of range now says "string index out of range".
Assisted-by: Claude Code:claude-opus-5
* Read Unicode numeric values as exact rationals
`DerivedNumericValues.txt` field 1 carries a rounded decimal, so
`unicodedata.numeric('⅐')` and its neighbours came back short of the
value CPython reports. The generator now reads the rational in field 3,
and takes the 3.2 values from field 8 of `UnicodeData-3.2.0.txt`.
Assisted-by: Claude Code:claude-opus-5
* Return a mappingproxy from Pattern.groupindex
`pattern_groupindex` wraps the group index in a dict proxy, and hands
back a plain dict only when the pattern has no named group.
Assisted-by: Claude Code:claude-opus-5
* Fix the utf-16/32/7 codecs' error handling
The utf-32 decoder dropped a code point above U+10FFFF instead of calling
the error handler, and both the utf-16 and utf-32 handlers were told the
encoding was "utf-16"/"utf-32" whichever byte order the data was in.
The encoders now take a bytes replacement from the handler as it stands,
rejecting one that does not fill whole code units, rather than iterating
over it as text; a str replacement from the utf-16 encoder now splits an
astral code point into a surrogate pair.
The utf-7 decoder names its encoding "utf7", as its codec does.
Assisted-by: Claude Code:claude-opus-5
* Rework complex multiplication, division and power
Division now divides by whichever part of the divisor is larger, the way
`_Py_c_quot` does, and both it and multiplication recover the infinities
that the plain formulas turn into nan, following C11 Annex G. An operand
that is a real number is now multiplied and divided component-wise, which
keeps the sign of a zero and the other part of an infinity; a real
numerator has its own quotient so that its zero keeps a sign too.
An integer exponent no greater than 100 in magnitude is now reached by
repeated multiplication rather than through the polar form, so e.g.
`(1+2j) ** 2` is exact. A zero exponent gives 1, and an exponent with no
imaginary part no longer reaches for a logarithm that turns an infinite
magnitude into a nan. A result that came out infinite raises
OverflowError.
`complex() arg is a malformed string` is replaced by `could not convert
string to complex: ...` when the string carries a misplaced underscore,
`complex division by zero` by `division by zero`, and the message for
zero raised to a negative or complex power no longer names the base.
Assisted-by: Claude Code:claude-opus-5
* Intern the string constants that read as names
A code object's string constants were interned by length, so a short
string that is not a name was interned and a long name was not. They are
now picked out the way `all_name_chars` does. The one-character latin-1
strings are interned as well, so that every route to one of them lands on
the same object.
Assisted-by: Claude Code:claude-opus-5
* Honour the marshal version and separate its EOF errors
Versions below 2 now write a float or complex as its digits under
TYPE_FLOAT/TYPE_COMPLEX, versions from 4 on write a tuple of fewer than
256 items under TYPE_SMALL_TUPLE and a long ascii string under
TYPE_ASCII, and a string is marked interned only when it is.
`marshal.loads` now tells apart the three ways its data can run out:
"EOF read where object expected" where an object was to start, "EOF read
where not expected" where a single byte was, and "marshal data too short"
where a run of bytes was cut short.
Assisted-by: Claude Code:claude-opus-5
We can save space by calculating the diffs between modern Unicode and
3.2.0 and storing membership information where valid. This avoids
storing entire tables for 3.2.0, and is also more correct in the long
run since it handles absence from 3.2.0 correctly. I switched over Bidi
to this new method which partially fixed the test. Unfortunately, our
Unicode data is more up to date than Python 3.14 so the test fails for
modern Unicode. I updated the test to 3.15 as per review, and now our
bidi passes the test.
case::{title,capitalize}_wtf8 pass a single first-of-word code point to
TitlecaseMapper::titlecase_segment. Under the default Auto leading
adjustment the mapper looks for a Letter/Number/Symbol/Private_Use head
and skips anything else, dropping the titlecase mapping of cased marks
such as U+0345 (COMBINING GREEK YPOGEGRAMMENI) -> U+0399: 'ͅ'.title()
returned 'ͅ' instead of 'Ι'. Use LeadingAdjustment::None so the
given code point is titlecased directly, matching CPython str.title and
str.capitalize.
Assisted-by: Claude
* Move str casing into rustpython-unicode::case and drop icu from vm
Add code-point simple mappings (simple_lowercase/uppercase/titlecase/fold),
casing predicates (is_lowercase/is_uppercase/is_titlecase/is_cased/
is_case_ignorable), and the string-level capitalize/title/swapcase/casefold
helpers to crates/unicode/src/case.rs, moving the titlecase segmentation and
final-sigma logic out of vm/builtins/str.rs verbatim. str.rs and anystr.rs now
call through the crate; the is_cased kernel takes plain fn(char)->bool
predicates instead of icu BinaryProperty generics.
Remove icu_casemap/icu_locale/icu_properties/writeable from crates/vm and the
now-unused VecFmtWriter helper. crates/unicode is the only workspace member
with a direct icu dependency. str.lower/upper keep using Wtf8::to_lowercase/
to_uppercase.
Assisted-by: Claude
* Route sre is_uni_space through unicode::classify::is_space
SRE_UNI_IS_SPACE is Py_UNICODE_ISSPACE. Replace the hand-rolled BMP code-point
list with classify::is_space, which is differential-tested against CPython. A
full-range sweep confirms the two agree on every code point.
Assisted-by: Claude
* Use simple case mappings and the Cased property for sre IGNORECASE
lower_unicode/upper_unicode took the first char of the full case mapping, so
code points with a full mapping but no simple one were miscased (e.g.
upper_unicode('ß') returned 'S'). Route them through case::simple_lowercase/
simple_uppercase, matching Py_UNICODE_TOLOWER/TOUPPER.
_sre.unicode_iscased derived casedness from those mappings, which only held
while the full mapping was used; with simple mappings a cased code point that
maps to itself (e.g. the ſt/st ligatures, U+FB05/06) read as uncased and lost its
_casefix equivalence. Query the Cased property directly via case::is_cased.
Assisted-by: Claude
* Sweep casing predicates and simple lowercase mapping against CPython
Extend the differential harness to cover is_lowercase/is_uppercase/is_titlecase/
is_cased over the full scalar range, sourced from str.islower/isupper, the Lt
category, and _sre.unicode_iscased. Add a parallel sweep of the simple lowercase
mapping (Py_UNICODE_TOLOWER via _sre.unicode_tolower) with its own version-skew
allow-list; CPython exposes no simple-uppercase oracle, so toupper stays on the
SRE unit tests.
Record the U+0295 Ll->Lo recategorization (Unicode 16.0.0 -> 17.0.0) as a known
reverse-direction divergence so the skew regenerator still rejects genuine
regressions.
Assisted-by: Claude
* Extract unicodedata core into rustpython-unicode crate
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
* Route str predicates, casefold, and printable through rustpython-unicode
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
* Route sre_engine char classes through rustpython-unicode
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
* Route \N{} name lookups through rustpython-unicode
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
* Add differential Unicode sweep and shared-crate snippet test
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
* Match regex \d to Unicode decimal digits
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
* Replace shallow Unicode wrappers with re-exports
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
* no-std
* Address review feedback: test module, dead variant, WTF-8 normalization
- 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