Skip to content

Update locale from v3.14.3 - #7348

Merged
youknowone merged 1 commit into
RustPython:mainfrom
youknowone:upgrade/locale
Mar 5, 2026
Merged

youknowone merged 1 commit into
RustPython:mainfrom
youknowone:upgrade/locale

Conversation

@youknowone

@youknowone youknowone commented Mar 4, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Updated Lib/locale.py from CPython v3.14.3
    • Lazy re module import for faster startup (moved re import from module-level to format_string())
  • Updated Lib/test/test_locale.py from CPython v3.14.3
    • Added LazyImportTest (cpython_only, skipped in RustPython)
    • Replaced is_emscripten or is_wasi checks with linked_to_musl()
    • Modernized test_defaults_UTF8 to use EnvironmentVarGuard
  • Removed @expectedFailure from 2 tests in test_types.py that now pass thanks to the lazy re import fix:
    • test_float__format__locale
    • test_int__format__locale

Test plan

  • test_locale and test__locale pass
  • test_types passes (2 previously failing tests now pass)
  • Direct dependent tests pass: test_builtin, test_calendar, test_decimal, test_float, test_format, test_inspect, test_io, test_re, test_strftime, test_sys, test_types, test_gettext, test_argparse, test_getopt, test_optparse, test_site

Summary by CodeRabbit

  • New Features
    • Added support for locale-aware number formatting that respects your system's locale settings for thousands separators and decimal points.
    • Numbers (integers, floating-point, and complex) can now be formatted using locale-specific grouping patterns.

@coderabbitai

coderabbitai Bot commented Mar 4, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (2)
  • Lib/locale.py is excluded by !Lib/**
  • Lib/test/test_locale.py is excluded by !Lib/**

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: dff35012-841f-4138-86ea-32996fdcf22b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces locale-aware Python number formatting support by adding locale detection infrastructure, extending FormatSpec with locale-specific formatting methods, and integrating locale-aware paths into int, float, and complex __format__ implementations.

Changes

Cohort / File(s) Summary
Core Locale-Aware Formatting
crates/common/src/format.rs
Adds LocaleInfo struct to hold thousands separator, decimal point, and grouping patterns. Introduces has_locale_format() detection and three public locale formatting methods (format_int_locale, format_float_locale, format_complex_locale). Includes helper functions for locale-aware grouping and decimal point substitution based on CPython rules.
Locale Information Retrieval
crates/vm/src/format.rs
Adds get_locale_info() function that retrieves locale settings via C's localeconv() on Unix platforms and provides safe defaults on non-Unix systems. Includes unsafe helper parse_grouping() for parsing C grouping arrays.
Builtin Type Format Integration
crates/vm/src/builtins/int.rs, crates/vm/src/builtins/float.rs, crates/vm/src/builtins/complex.rs
Updates __format__ implementations for int, float, and complex types to: (1) convert format spec parse errors to Python exceptions, (2) detect locale formatting via has_locale_format(), (3) route to locale-aware formatters when appropriate, and (4) preserve existing non-locale paths.

Sequence Diagram

sequenceDiagram
    actor User
    participant PyBuiltin as PyInt/PyFloat/PyComplex
    participant FormatSpec
    participant LocaleLogic as Locale Detection &<br/>Retrieval
    participant Formatter as Locale-Aware<br/>Formatter
    participant Result as Formatted String

    User->>PyBuiltin: __format__(spec_str)
    PyBuiltin->>FormatSpec: parse(spec_str)
    FormatSpec-->>PyBuiltin: parsed spec
    PyBuiltin->>FormatSpec: has_locale_format()
    alt Locale Formatting Detected
        FormatSpec-->>PyBuiltin: true
        PyBuiltin->>LocaleLogic: get_locale_info()
        LocaleLogic-->>PyBuiltin: LocaleInfo (thousands_sep, decimal_point, grouping)
        PyBuiltin->>Formatter: format_*_locale(num, locale_info)
        Formatter->>Formatter: apply grouping & substitution
        Formatter-->>Result: locale-formatted string
    else Standard Formatting
        FormatSpec-->>PyBuiltin: false
        PyBuiltin->>Formatter: format_*(num)
        Formatter-->>Result: standard formatted string
    end
    Result-->>User: formatted output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 With whiskers twitched and paws aglow,
This rabbit brings locale to flow!
Numbers now wear their regional dress—
Commas and dots in formatted finesse! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title 'Update locale from v3.14.3' is overly vague and does not clearly convey the main technical changes. The PR actually implements locale-aware formatting support for the 'n' format specifier across int, float, and complex types, plus updates to locale module and tests. The title fails to reflect the primary engineering work. Revise the title to better reflect the main change, such as: 'Implement locale-aware n format specifier and update locale to v3.14.3' or 'Add locale-aware number formatting and update locale module from v3.14.3'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

@github-actions

github-actions Bot commented Mar 4, 2026 •

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

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

[x] lib: cpython/Lib/locale.py
[x] test: cpython/Lib/test/test_locale.py
[x] test: cpython/Lib/test/test__locale.py

dependencies:

  • locale

dependent tests: (95 tests)

  • locale: test__locale test_builtin test_c_locale_coercion test_calendar test_decimal test_float test_format test_inspect test_io test_locale test_os test_re test_regrtest test_strftime test_sys test_types test_utf8_mode
    • calendar: test_imaplib
      • http.cookiejar: test_http_cookiejar test_urllib2
      • mailbox: test_genericalias test_mailbox
      • ssl: test_asyncio test_ftplib test_httplib test_httpservers test_logging test_ssl test_urllib test_urllib2_localnet test_venv test_xmlrpc
    • gettext: test_gettext test_tools
      • argparse: test_argparse
      • getopt: test_getopt
      • optparse: test_optparse
    • site: test_site
    • subprocess: test_android test_asyncio test_atexit test_audit test_bz2 test_cmd_line test_cmd_line_script test_ctypes test_dtrace test_faulthandler test_file_eintr test_gc test_gzip test_json test_launcher test_msvcrt test_ntpath test_osx_env test_platform test_plistlib test_poll test_py_compile test_quopri test_repl test_runpy test_script_helper test_select test_shutil test_signal test_sqlite3 test_subprocess test_support test_sysconfig test_tempfile test_threading test_traceback test_unittest test_wait3 test_webbrowser test_zipfile
      • ctypes.util: test_ctypes
      • ensurepip: test_ensurepip
      • multiprocessing.util: test_asyncio test_compileall test_concurrent_futures
      • platform: test__osx_support test_asyncio test_baseexception test_cmath test_fcntl test_math test_mimetypes test_posix test_socket test_time test_winreg test_wsgiref

Legend:

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

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Code has been automatically formatted

The code in this PR has been formatted using:

  • cargo fmt --all
    Please pull the latest changes before pushing again:
git pull origin upgrade/locale

@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

🧹 Nitpick comments (1)
crates/vm/src/format.rs (1)

57-62: Potential portability issue with c_char::MAX check.

On platforms where c_char is signed (e.g., x86 Linux), c_char::MAX is 127. On platforms where it's unsigned (e.g., ARM), it's 255. The C standard specifies that CHAR_MAX in the grouping array terminates grouping, but this value differs by platform. The current check should work correctly since you're comparing against libc::c_char::MAX, but verify this matches CPython's behavior across platforms.

Additionally, the inner unsafe block on line 57 is redundant since the outer function body is already within an unsafe block.

Remove redundant unsafe block
 unsafe fn parse_grouping(grouping: *const libc::c_char) -> Vec<u8> {
     let mut result = Vec::new();
     if grouping.is_null() {
         return result;
     }
-    unsafe {
-        let mut ptr = grouping;
-        while ![0, libc::c_char::MAX].contains(&*ptr) {
-            result.push(*ptr as u8);
-            ptr = ptr.add(1);
-        }
+    let mut ptr = grouping;
+    while ![0, libc::c_char::MAX].contains(&*ptr) {
+        result.push(*ptr as u8);
+        ptr = ptr.add(1);
     }
     if !result.is_empty() {
         result.push(0);
     }
     result
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/format.rs` around lines 57 - 62, The loop currently uses an
inner unsafe and compares against libc::c_char::MAX which can be
platform-dependent; remove the redundant inner "unsafe" block (the function is
already unsafe) and change the termination check to match CPython's behavior by
comparing *ptr against 0 and the C CHAR_MAX sentinel (use libc::CHAR_MAX rather
than libc::c_char::MAX), keeping the existing pointer iteration (grouping, ptr)
and result.push(*ptr as u8) logic intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/vm/src/format.rs`:
- Around line 12-38: get_locale_info currently calls the non-thread-safe C
localeconv() directly; change it to use a per-thread cache via thread_local! so
each thread calls localeconv() once and stores a copied LocaleInfo (strings and
grouping) to avoid races. Specifically, introduce a thread_local static (e.g.,
CACHED_LOCALE) holding Option<LocaleInfo>, update get_locale_info to check that
cache and return a clone if present, otherwise call localeconv() inside the
unsafe block, build the LocaleInfo using parse_grouping and owned strings, store
it in the thread-local cache, and return it; alternatively on glibc you can
fallback to using localeconv_l with a per-thread locale object if available.
- Around line 25-30: The code dereferences (*lc).thousands_sep and
(*lc).decimal_point without null checks; add checks before calling
CStr::from_ptr so you don't call it on a null pointer. For example, inspect
(*lc).thousands_sep and (*lc).decimal_point for .is_null() and if null assign a
safe default String (e.g., empty string or a sensible default like "." for
decimal_point) otherwise call
CStr::from_ptr(...).to_string_lossy().into_owned(); update the two lines using
thousands_sep and decimal_point accordingly (keeping the existing variable
names).

---

Nitpick comments:
In `@crates/vm/src/format.rs`:
- Around line 57-62: The loop currently uses an inner unsafe and compares
against libc::c_char::MAX which can be platform-dependent; remove the redundant
inner "unsafe" block (the function is already unsafe) and change the termination
check to match CPython's behavior by comparing *ptr against 0 and the C CHAR_MAX
sentinel (use libc::CHAR_MAX rather than libc::c_char::MAX), keeping the
existing pointer iteration (grouping, ptr) and result.push(*ptr as u8) logic
intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 9f9d8854-834d-48c8-9e30-ab3832f04f71

📥 Commits

Reviewing files that changed from the base of the PR and between 9a0511b and 3142f99.

⛔ Files ignored due to path filters (4)
  • Lib/locale.py is excluded by !Lib/**
  • Lib/test/test_format.py is excluded by !Lib/**
  • Lib/test/test_locale.py is excluded by !Lib/**
  • Lib/test/test_types.py is excluded by !Lib/**
📒 Files selected for processing (5)
  • crates/common/src/format.rs
  • crates/vm/src/builtins/complex.rs
  • crates/vm/src/builtins/float.rs
  • crates/vm/src/builtins/int.rs
  • crates/vm/src/format.rs

Comment thread crates/vm/src/format.rs
Comment on lines +12 to +38
/// Get locale information from C `localeconv()` for the 'n' format specifier.
#[cfg(unix)]
pub(crate) fn get_locale_info() -> LocaleInfo {
use core::ffi::CStr;
unsafe {
let lc = libc::localeconv();
if lc.is_null() {
return LocaleInfo {
thousands_sep: String::new(),
decimal_point: ".".to_string(),
grouping: vec![],
};
}
let thousands_sep = CStr::from_ptr((*lc).thousands_sep)
.to_string_lossy()
.into_owned();
let decimal_point = CStr::from_ptr((*lc).decimal_point)
.to_string_lossy()
.into_owned();
let grouping = parse_grouping((*lc).grouping);
LocaleInfo {
thousands_sep,
decimal_point,
grouping,
}
}
}

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.

⚠️ Potential issue | 🟠 Major

Thread safety: localeconv() is not thread-safe.

The C localeconv() function is not required to be thread-safe by POSIX. It returns a pointer to static storage that can be overwritten by subsequent calls to localeconv() or setlocale(). Since get_locale_info() is called from __format__ on int/float/complex without synchronization (as shown in the context snippets), concurrent formatting operations may cause data races or corrupt locale data.

Consider one of these mitigations:

  1. Cache the locale info at startup or per-thread using thread_local!
  2. Use a mutex to serialize access to localeconv()
  3. On glibc, use localeconv_l() with a per-thread locale object
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/format.rs` around lines 12 - 38, get_locale_info currently
calls the non-thread-safe C localeconv() directly; change it to use a per-thread
cache via thread_local! so each thread calls localeconv() once and stores a
copied LocaleInfo (strings and grouping) to avoid races. Specifically, introduce
a thread_local static (e.g., CACHED_LOCALE) holding Option<LocaleInfo>, update
get_locale_info to check that cache and return a clone if present, otherwise
call localeconv() inside the unsafe block, build the LocaleInfo using
parse_grouping and owned strings, store it in the thread-local cache, and return
it; alternatively on glibc you can fallback to using localeconv_l with a
per-thread locale object if available.

Comment thread crates/vm/src/format.rs
Comment on lines +25 to +30
let thousands_sep = CStr::from_ptr((*lc).thousands_sep)
.to_string_lossy()
.into_owned();
let decimal_point = CStr::from_ptr((*lc).decimal_point)
.to_string_lossy()
.into_owned();

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.

⚠️ Potential issue | 🟡 Minor

Missing null checks for thousands_sep and decimal_point pointers.

While you check if lc is null, the thousands_sep and decimal_point fields within the lconv struct could also be null pointers on some platforms or in edge cases. Dereferencing them with CStr::from_ptr() would cause undefined behavior.

Add null checks for string fields
         let lc = libc::localeconv();
         if lc.is_null() {
             return LocaleInfo {
                 thousands_sep: String::new(),
                 decimal_point: ".".to_string(),
                 grouping: vec![],
             };
         }
-        let thousands_sep = CStr::from_ptr((*lc).thousands_sep)
-            .to_string_lossy()
-            .into_owned();
-        let decimal_point = CStr::from_ptr((*lc).decimal_point)
-            .to_string_lossy()
-            .into_owned();
+        let thousands_sep = if (*lc).thousands_sep.is_null() {
+            String::new()
+        } else {
+            CStr::from_ptr((*lc).thousands_sep)
+                .to_string_lossy()
+                .into_owned()
+        };
+        let decimal_point = if (*lc).decimal_point.is_null() {
+            ".".to_string()
+        } else {
+            CStr::from_ptr((*lc).decimal_point)
+                .to_string_lossy()
+                .into_owned()
+        };
         let grouping = parse_grouping((*lc).grouping);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let thousands_sep = CStr::from_ptr((*lc).thousands_sep)
.to_string_lossy()
.into_owned();
let decimal_point = CStr::from_ptr((*lc).decimal_point)
.to_string_lossy()
.into_owned();
let lc = libc::localeconv();
if lc.is_null() {
return LocaleInfo {
thousands_sep: String::new(),
decimal_point: ".".to_string(),
grouping: vec![],
};
}
let thousands_sep = if (*lc).thousands_sep.is_null() {
String::new()
} else {
CStr::from_ptr((*lc).thousands_sep)
.to_string_lossy()
.into_owned()
};
let decimal_point = if (*lc).decimal_point.is_null() {
".".to_string()
} else {
CStr::from_ptr((*lc).decimal_point)
.to_string_lossy()
.into_owned()
};
let grouping = parse_grouping((*lc).grouping);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/format.rs` around lines 25 - 30, The code dereferences
(*lc).thousands_sep and (*lc).decimal_point without null checks; add checks
before calling CStr::from_ptr so you don't call it on a null pointer. For
example, inspect (*lc).thousands_sep and (*lc).decimal_point for .is_null() and
if null assign a safe default String (e.g., empty string or a sensible default
like "." for decimal_point) otherwise call
CStr::from_ptr(...).to_string_lossy().into_owned(); update the two lines using
thousands_sep and decimal_point accordingly (keeping the existing variable
names).

@youknowone
youknowone merged commit a27d812 into RustPython:main Mar 5, 2026
20 of 25 checks passed
@youknowone
youknowone deleted the upgrade/locale branch March 5, 2026 21:00
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