Skip to content

Fix int unicode decimal digits - #8521

Open
zzarbttoo wants to merge 3 commits into
RustPython:mainfrom
zzarbttoo:fix-int-unicode-decimal-digits
Open

Fix int unicode decimal digits#8521
zzarbttoo wants to merge 3 commits into
RustPython:mainfrom
zzarbttoo:fix-int-unicode-decimal-digits

Conversation

@zzarbttoo

@zzarbttoo zzarbttoo commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

CPython runs the string argument of every numeric constructor through
_PyUnicode_TransformDecimalAndSpaceToASCII before parsing, so digits from any
script — and any Unicode whitespace — are accepted:

>>> int("١٢٣٤٥٦٧٨٩٠")
1234567890
>>> int("१२३४५६७८९०1234567890")
12345678901234567890

Changes

  • Add rustpython_common::str::transform_decimal_and_space_to_ascii, a port of CPython's transform. Unicode decimal digits fold to ASCII, Unicode whitespace folds to a plain space, and ASCII input is returned borrowed without allocating. Any other non-ASCII character can never appear in a numeric literal, so it is replaced with ? and the rest of the string is dropped — ? is rejected by every parser at every base, which leaves the error message to the caller that knows the base and owns the original string.
  • Add protocol::numeric_literal_from_str, the shared trim + transform step, and route int() (both try_int_radix and try_int), float() and complex() through it. This is the only step the three constructors share — only int takes a base, and only int and float accept bytes-like input — so each keeps its own entry point around it. Strings holding surrogates fold to an empty (and therefore invalid) literal, as before.
  • float()'s inline mapping is replaced by the shared helper; its previous version left non-digit non-ASCII characters in place, which the new one rejects up front.
  • Decimal() inherits the fix through its int() call in _pydecimal.

Summary by CodeRabbit

  • New Features
    • Numeric string parsing now recognizes Unicode decimal digits and whitespace by converting them to standard ASCII equivalents.
    • Integer, floating-point, and complex-number conversions consistently apply the same normalization rules.
  • Bug Fixes
    • Improved handling of strings containing unsupported characters or surrogate values, treating them as invalid numeric input instead of producing inconsistent results.
    • Preserved efficient handling for already-ASCII numeric strings.

zzarbttoo and others added 3 commits August 9, 2026 16:29
CPython runs a string argument through
_PyUnicode_TransformDecimalAndSpaceToASCII before parsing it, so decimal
digits from any script are accepted:

    int('١٢٣')          # 123
    int('0x١f', 16)     # 31
    Decimal('١٢٣')      # Decimal('123')
    complex('1+2j')    # (1+2j)

RustPython only did this for float(), which had the transform inlined.
int() handed the raw UTF-8 bytes to bytes_to_int(), whose digit check is
is_ascii_alphanumeric(), so every non-ASCII digit was rejected — even
though float() accepted the same string.

Lift the inlined transform out of float_from_string() into
common::str::transform_decimal_and_space_to_ascii() and apply it to the
str paths of int() and complex() too. The result is always ASCII: as in
CPython, a character that is neither ASCII, whitespace nor a decimal
digit becomes '?' and truncates the string, which no parser accepts at
any base, leaving the caller to raise the error from the original string.

Bytes-like input keeps going straight to the parser, matching CPython's
split between PyLong_FromUnicodeObject and PyLong_FromString.

This unmarks two expectedFailure tests: test_int.test_unicode and
test_decimal.test_unicode_digits.
All three constructors need the same thing from a str argument: trim it,
fold Unicode decimal digits and whitespace to ASCII, and give up on a
string holding surrogates. Each expressed that last part differently —
float matched PyKindStr and returned b"", complex leaned on to_str()
returning None, int returned an empty Cow — so the rule lived in three
places at once.

Move it into protocol::numeric_literal_from_str() and have all three call
it. CPython repeats this per type because its wrapper is three lines over
a single PyUnicode representation; ours has to match over Ascii/Utf8/Wtf8,
which is worth writing once.

Only the shared step moves. int keeps its base handling, int and float
keep accepting bytes-like input, complex keeps rejecting it, and each
keeps raising its own error, because none of that is shared.

No behavior change: the CPython differential suite is byte-identical
before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9b5d29f4-ac69-439f-b5a3-22155b9bb594

📥 Commits

Reviewing files that changed from the base of the PR and between d04318e and 0d81b08.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_decimal.py is excluded by !Lib/**
  • Lib/test/test_int.py is excluded by !Lib/**
📒 Files selected for processing (6)
  • crates/common/src/str.rs
  • crates/vm/src/builtins/complex.rs
  • crates/vm/src/builtins/float.rs
  • crates/vm/src/builtins/int.rs
  • crates/vm/src/protocol/mod.rs
  • crates/vm/src/protocol/number.rs

📝 Walkthrough

Walkthrough

The change adds shared normalization for Unicode decimal digits and whitespace. int(), float(), and complex() string parsing now use numeric_literal_from_str, including invalid-character handling and ASCII-preserving behavior.

Changes

Numeric string normalization

Layer / File(s) Summary
ASCII transformation and tests
crates/common/src/str.rs
Adds transform_decimal_and_space_to_ascii, which preserves ASCII input, converts Unicode decimal digits and whitespace, and truncates unsupported characters. Tests cover these behaviors.
Numeric literal normalization
crates/vm/src/protocol/number.rs, crates/vm/src/protocol/mod.rs, crates/vm/src/builtins/int.rs
Adds and re-exports numeric_literal_from_str. Integer string parsing uses the normalized literal.
Builtin numeric parsing integration
crates/vm/src/builtins/complex.rs, crates/vm/src/builtins/float.rs, crates/vm/src/builtins/int.rs
complex(), float(), and integer radix parsing use numeric_literal_from_str for string inputs.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: 🔵 Low · up to 0d81b

The PR broadens numeric parsing to Unicode decimal digits and whitespace across int(), float(), complex(), and Decimal(); it is mergeable with explicit follow-up to run the required Rust checks and verify constructor-level compatibility, since localized parser integration or build-quality regressions could otherwise go unnoticed.

Sequence Diagram(s)

sequenceDiagram
  participant NumericBuiltin
  participant numeric_literal_from_str
  participant transform_decimal_and_space_to_ascii
  participant LiteralParser
  NumericBuiltin->>numeric_literal_from_str: normalize string input
  numeric_literal_from_str->>transform_decimal_and_space_to_ascii: convert digits and whitespace
  transform_decimal_and_space_to_ascii-->>numeric_literal_from_str: normalized text
  numeric_literal_from_str-->>NumericBuiltin: trimmed numeric literal
  NumericBuiltin->>LiteralParser: parse normalized literal
Loading

Suggested reviewers: hyojongpark

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the Unicode decimal digit fix for int(), which is a real but narrower part of the broader numeric constructor changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

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

[x] lib: cpython/Lib/code.py
[x] test: cpython/Lib/test/test_code_module.py (TODO: 3)

dependencies:

  • code

dependent tests: (2 tests)
- [x] pdb: test_pdb
- [ ] sqlite3.main: test_sqlite3

[x] test: cpython/Lib/test/test_structseq.py (TODO: 7)

dependencies:

dependent tests: (no tests depend on structseq)

[x] lib: cpython/Lib/decimal.py
[x] lib: cpython/Lib/_pydecimal.py
[ ] test: cpython/Lib/test/test_decimal.py

dependencies:

  • decimal

dependent tests: (75 tests)

  • decimal: test_asyncio test_buffer test_builtin test_compare test_configparser test_decimal test_fractions test_fstring test_itertools test_json test_locale test_math test_numeric_tower test_operator test_os test_statistics test_time test_tokenize test_tomllib test_xmlrpc
    • fractions: test_float test_random test_string
      • statistics: test_signal
    • statistics:
      • random: test_asyncio test_bisect test_bz2 test_collections test_complex test_context test_dbm_dumb test_deque test_descr test_devpoll test_dict test_dummy_thread test_email test_functools test_grp test_heapq test_hmac test_importlib test_int test_io test_logging test_long test_lzma test_mmap test_ordered_dict test_poll test_posixpath test_pow test_pprint test_pwd test_queue test_regrtest test_richcmp test_selectors test_set test_shutil test_socket test_sort test_strtod test_struct test_sys test_tarfile test_thread test_threading test_traceback test_unparse test_uuid test_weakref test_zipfile test_zlib test_zstd

[ ] test: cpython/Lib/test/test_set.py (TODO: 4)

dependencies:

dependent tests: (no tests depend on set)

[x] lib: cpython/Lib/ssl.py
[ ] test: cpython/Lib/test/test_ssl.py (TODO: 10)

dependencies:

  • ssl

dependent tests: (53 tests)

  • ssl: test_asyncio test_ftplib test_httplib test_httpservers test_imaplib test_logging test_poplib test_ssl test_urllib test_urllib2_localnet test_venv test_xmlrpc
    • asyncio.selector_events: test_asyncio
    • ftplib: test_urllib2
      • urllib.request: test_http_cookiejar test_pathlib test_pydoc test_sax test_site test_urllib2net test_urllibnet
    • http.client: test_docxmlrpc test_hashlib test_ucn test_unicodedata test_wsgiref
      • logging.handlers: test_concurrent_futures test_pkgutil
    • http.server: test_robotparser
      • pydoc: test_enum
    • smtplib: test_smtplib test_smtpnet
    • urllib.request:
      • pathlib: test_ast test_dbm_sqlite3 test_ensurepip test_importlib test_json test_launcher test_pathlib test_peg_generator test_pyrepl test_runpy test_tarfile test_tempfile test_tomllib test_tools test_traceback test_unparse test_winapi test_zipapp test_zipfile test_zoneinfo test_zstd

[ ] lib: cpython/Lib/typing.py
[ ] test: cpython/Lib/test/test_typing.py (TODO: 2)
[x] test: cpython/Lib/test/test_type_aliases.py
[x] test: cpython/Lib/test/test_type_annotations.py (TODO: 1)
[ ] test: cpython/Lib/test/test_type_params.py (TODO: 1)
[x] test: cpython/Lib/test/test_genericalias.py

dependencies:

  • typing (native: _typing, collections.abc, sys)
    • collections (native: _collections, _weakref, itertools, sys)
    • inspect (native: builtins, collections.abc, importlib.machinery, itertools, sys)
    • warnings (native: _contextvars, _thread, _warnings, builtins, sys)
    • abc, annotationlib, contextlib, copyreg, functools, operator, re, types

dependent tests: (19 tests)

  • typing: test_annotationlib test_builtin test_copy test_enum test_fractions test_funcattrs test_functools test_genericalias test_grammar test_inspect test_isinstance test_patma test_peg_generator test_pydoc test_pyrepl test_type_aliases test_type_params test_types test_typing

[x] lib: cpython/Lib/_pylong.py
[ ] test: cpython/Lib/test/test_int.py (TODO: 3)
[x] test: cpython/Lib/test/test_long.py (TODO: 4)
[x] test: cpython/Lib/test/test_int_literal.py

dependencies:

  • int

dependent tests: (no tests depend on int)

[x] test: cpython/Lib/test/test_itertools.py (TODO: 6)

dependencies:

dependent tests: (56 tests)

  • itertools: test_annotationlib test_ast test_asyncio test_bdb test_buffer test_builtin test_call test_codeccallbacks test_collections test_compile test_concurrent_futures test_csv test_ctypes test_descr test_dis test_email test_exceptions test_functools test_genericalias test_hashlib test_heapq test_httplib test_importlib test_inspect test_io test_iterlen test_itertools test_launcher test_logging test_math test_memoryview test_mmap test_os test_peepholer test_platform test_pprint test_pyrepl test_queue test_range test_set test_shlex test_slice test_socket test_sort test_statistics test_str test_struct test_subprocess test_tokenize test_tuple test_typing test_unittest test_uuid test_winreg test_xml_etree test_zipfile

[x] test: cpython/Lib/test/test_marshal.py (TODO: 15)

dependencies:

dependent tests: (25 tests)

  • marshal: test_bool test_exceptions test_importlib test_inspect test_marshal test_zipimport
    • importlib._bootstrap_external: test_importlib test_unittest
      • modulefinder: test_importlib test_modulefinder
      • py_compile: test_argparse test_cmd_line_script test_compileall test_importlib test_multiprocessing_main_handling test_py_compile test_pydoc test_runpy
      • pydoc: test_enum
    • pkgutil: test_pkgutil test_pyrepl
    • profile: test_profile
    • pstats: test_pstats
    • zipimport: test_importlib test_zipimport_support

Legend:

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

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.

1 participant