Skip to content

Commit 1d38d7d

Browse files
committed
Merge remote-tracking branch 'upstream/main' into bytecode-consti-newtype
2 parents fc7d2b7 + 04cf5da commit 1d38d7d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1512
-537
lines changed

.cspell.dict/rust-more.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ bindgen
66
bitand
77
bitflags
88
bitor
9+
bitvec
910
bitxor
1011
bstr
1112
byteorder
@@ -58,6 +59,7 @@ powi
5859
prepended
5960
punct
6061
replacen
62+
retag
6163
rmatch
6264
rposition
6365
rsplitn
@@ -89,5 +91,3 @@ widestring
8991
winapi
9092
winresource
9193
winsock
92-
bitvec
93-
Bitvec

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_dataclasses/__init__.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3672,7 +3672,6 @@ class A:
36723672
self.assertEqual(obj.a, 'a')
36733673
self.assertEqual(obj.b, 'b')
36743674

3675-
@unittest.expectedFailure # TODO: RUSTPYTHON
36763675
def test_slots_no_weakref(self):
36773676
@dataclass(slots=True)
36783677
class A:
@@ -3687,7 +3686,6 @@ class A:
36873686
with self.assertRaises(AttributeError):
36883687
a.__weakref__
36893688

3690-
@unittest.expectedFailure # TODO: RUSTPYTHON
36913689
def test_slots_weakref(self):
36923690
@dataclass(slots=True, weakref_slot=True)
36933691
class A:
@@ -3748,7 +3746,6 @@ def test_weakref_slot_make_dataclass(self):
37483746
"weakref_slot is True but slots is False"):
37493747
B = make_dataclass('B', [('a', int),], weakref_slot=True)
37503748

3751-
@unittest.expectedFailure # TODO: RUSTPYTHON
37523749
def test_weakref_slot_subclass_weakref_slot(self):
37533750
@dataclass(slots=True, weakref_slot=True)
37543751
class Base:
@@ -3767,7 +3764,6 @@ class A(Base):
37673764
a_ref = weakref.ref(a)
37683765
self.assertIs(a.__weakref__, a_ref)
37693766

3770-
@unittest.expectedFailure # TODO: RUSTPYTHON
37713767
def test_weakref_slot_subclass_no_weakref_slot(self):
37723768
@dataclass(slots=True, weakref_slot=True)
37733769
class Base:
@@ -3785,7 +3781,6 @@ class A(Base):
37853781
a_ref = weakref.ref(a)
37863782
self.assertIs(a.__weakref__, a_ref)
37873783

3788-
@unittest.expectedFailure # TODO: RUSTPYTHON
37893784
def test_weakref_slot_normal_base_weakref_slot(self):
37903785
class Base:
37913786
__slots__ = ('__weakref__',)
@@ -3830,7 +3825,6 @@ class B[T2]:
38303825
self.assertTrue(B.__weakref__)
38313826
B()
38323827

3833-
@unittest.expectedFailure # TODO: RUSTPYTHON
38343828
def test_dataclass_derived_generic_from_base(self):
38353829
T = typing.TypeVar('T')
38363830

Lib/test/test_descr.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,7 +1321,6 @@ class X(object):
13211321
with self.assertRaisesRegex(AttributeError, "'X' object has no attribute 'a'"):
13221322
X().a
13231323

1324-
@unittest.expectedFailure # TODO: RUSTPYTHON
13251324
def test_slots_special(self):
13261325
# Testing __dict__ and __weakref__ in __slots__...
13271326
class D(object):
@@ -2294,7 +2293,6 @@ def __contains__(self, value):
22942293
self.assertIn(i, p10)
22952294
self.assertNotIn(10, p10)
22962295

2297-
@unittest.expectedFailure # TODO: RUSTPYTHON
22982296
def test_weakrefs(self):
22992297
# Testing weak references...
23002298
import weakref
@@ -3976,7 +3974,6 @@ def __init__(self, x):
39763974
o = trash(o)
39773975
del o
39783976

3979-
@unittest.expectedFailure # TODO: RUSTPYTHON
39803977
def test_slots_multiple_inheritance(self):
39813978
# SF bug 575229, multiple inheritance w/ slots dumps core
39823979
class A(object):

Lib/test/test_format.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,6 @@ def test_non_ascii(self):
423423
self.assertEqual(format(1+2j, "\u2007^8"), "\u2007(1+2j)\u2007")
424424
self.assertEqual(format(0j, "\u2007^4"), "\u20070j\u2007")
425425

426-
@unittest.skip("TODO: RUSTPYTHON; formatting does not support locales. See https://github.com/RustPython/RustPython/issues/5181")
427426
def test_locale(self):
428427
try:
429428
oldloc = locale.setlocale(locale.LC_ALL)

Lib/test/test_types.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,6 @@ def test(i, format_spec, result):
431431
test(123456, "1=20", '11111111111111123456')
432432
test(123456, "*=20", '**************123456')
433433

434-
@unittest.expectedFailure # TODO: RUSTPYTHON; + 1234.57
435434
@run_with_locale('LC_NUMERIC', 'en_US.UTF8', '')
436435
def test_float__format__locale(self):
437436
# test locale support for __format__ code 'n'
@@ -441,7 +440,6 @@ def test_float__format__locale(self):
441440
self.assertEqual(locale.format_string('%g', x, grouping=True), format(x, 'n'))
442441
self.assertEqual(locale.format_string('%.10g', x, grouping=True), format(x, '.10n'))
443442

444-
@unittest.expectedFailure # TODO: RUSTPYTHON; + 123456789012345678901234567890
445443
@run_with_locale('LC_NUMERIC', 'en_US.UTF8', '')
446444
def test_int__format__locale(self):
447445
# test locale support for __format__ code 'n' for integers

Lib/test/test_weakref.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1862,7 +1862,6 @@ def test_weak_valued_delitem(self):
18621862
self.assertEqual(len(d), 1)
18631863
self.assertEqual(list(d.items()), [('something else', o2)])
18641864

1865-
@unittest.expectedFailure # TODO: RUSTPYTHON
18661865
def test_weak_keyed_bad_delitem(self):
18671866
d = weakref.WeakKeyDictionary()
18681867
o = Object('1')

crates/common/src/format.rs

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ use rustpython_literal::format::Case;
1212

1313
use crate::wtf8::{CodePoint, Wtf8, Wtf8Buf};
1414

15+
/// Locale information for 'n' format specifier.
16+
/// Contains thousands separator, decimal point, and grouping pattern
17+
/// from the C library's `localeconv()`.
18+
#[derive(Clone, Debug)]
19+
pub struct LocaleInfo {
20+
pub thousands_sep: String,
21+
pub decimal_point: String,
22+
/// Grouping pattern from `lconv.grouping`.
23+
/// Each element is a group size. The last non-zero element repeats.
24+
/// e.g. `[3, 0]` means groups of 3 repeating forever.
25+
pub grouping: Vec<u8>,
26+
}
27+
1528
trait FormatParse {
1629
fn parse(text: &Wtf8) -> (Option<Self>, &Wtf8)
1730
where
@@ -460,6 +473,189 @@ impl FormatSpec {
460473
}
461474
}
462475

476+
/// Returns true if this format spec uses the locale-aware 'n' format type.
477+
pub fn has_locale_format(&self) -> bool {
478+
matches!(self.format_type, Some(FormatType::Number(Case::Lower)))
479+
}
480+
481+
/// Insert locale-aware thousands separators into an integer string.
482+
/// Follows CPython's GroupGenerator logic for variable-width grouping.
483+
fn insert_locale_grouping(int_part: &str, locale: &LocaleInfo) -> String {
484+
if locale.grouping.is_empty() || locale.thousands_sep.is_empty() || int_part.len() <= 1 {
485+
return int_part.to_string();
486+
}
487+
488+
let mut group_idx = 0;
489+
let mut group_size = locale.grouping[0] as usize;
490+
491+
if group_size == 0 {
492+
return int_part.to_string();
493+
}
494+
495+
// Collect groups of digits from right to left
496+
let len = int_part.len();
497+
let mut groups: Vec<&str> = Vec::new();
498+
let mut pos = len;
499+
500+
loop {
501+
if pos <= group_size {
502+
groups.push(&int_part[..pos]);
503+
break;
504+
}
505+
506+
groups.push(&int_part[pos - group_size..pos]);
507+
pos -= group_size;
508+
509+
// Advance to next group size
510+
if group_idx + 1 < locale.grouping.len() {
511+
let next = locale.grouping[group_idx + 1] as usize;
512+
if next != 0 {
513+
group_size = next;
514+
group_idx += 1;
515+
}
516+
// 0 means repeat previous group size forever
517+
}
518+
}
519+
520+
// Groups were collected right-to-left, reverse to get left-to-right
521+
groups.reverse();
522+
groups.join(&locale.thousands_sep)
523+
}
524+
525+
/// Apply locale-aware grouping and decimal point replacement to a formatted number.
526+
fn apply_locale_formatting(magnitude_str: String, locale: &LocaleInfo) -> String {
527+
let mut parts = magnitude_str.splitn(2, '.');
528+
let int_part = parts.next().unwrap();
529+
let grouped = Self::insert_locale_grouping(int_part, locale);
530+
531+
if let Some(frac_part) = parts.next() {
532+
format!("{grouped}{}{frac_part}", locale.decimal_point)
533+
} else {
534+
grouped
535+
}
536+
}
537+
538+
/// Format an integer with locale-aware 'n' format.
539+
pub fn format_int_locale(
540+
&self,
541+
num: &BigInt,
542+
locale: &LocaleInfo,
543+
) -> Result<String, FormatSpecError> {
544+
self.validate_format(FormatType::Decimal)?;
545+
let magnitude = num.abs();
546+
547+
let raw_magnitude_str = match self.format_type {
548+
Some(FormatType::Number(Case::Lower)) => self.format_int_radix(magnitude, 10),
549+
_ => return self.format_int(num),
550+
}?;
551+
552+
let magnitude_str = Self::apply_locale_formatting(raw_magnitude_str, locale);
553+
554+
let format_sign = self.sign.unwrap_or(FormatSign::Minus);
555+
let sign_str = match num.sign() {
556+
Sign::Minus => "-",
557+
_ => match format_sign {
558+
FormatSign::Plus => "+",
559+
FormatSign::Minus => "",
560+
FormatSign::MinusOrSpace => " ",
561+
},
562+
};
563+
564+
self.format_sign_and_align(&AsciiStr::new(&magnitude_str), sign_str, FormatAlign::Right)
565+
}
566+
567+
/// Format a float with locale-aware 'n' format.
568+
pub fn format_float_locale(
569+
&self,
570+
num: f64,
571+
locale: &LocaleInfo,
572+
) -> Result<String, FormatSpecError> {
573+
self.validate_format(FormatType::FixedPoint(Case::Lower))?;
574+
let precision = self.precision.unwrap_or(6);
575+
let magnitude = num.abs();
576+
577+
let raw_magnitude_str = match &self.format_type {
578+
Some(FormatType::Number(case)) => {
579+
let precision = if precision == 0 { 1 } else { precision };
580+
Ok(float::format_general(
581+
precision,
582+
magnitude,
583+
*case,
584+
self.alternate_form,
585+
false,
586+
))
587+
}
588+
_ => return self.format_float(num),
589+
}?;
590+
591+
let magnitude_str = Self::apply_locale_formatting(raw_magnitude_str, locale);
592+
593+
let format_sign = self.sign.unwrap_or(FormatSign::Minus);
594+
let sign_str = if num.is_sign_negative() && !num.is_nan() {
595+
"-"
596+
} else {
597+
match format_sign {
598+
FormatSign::Plus => "+",
599+
FormatSign::Minus => "",
600+
FormatSign::MinusOrSpace => " ",
601+
}
602+
};
603+
604+
self.format_sign_and_align(&AsciiStr::new(&magnitude_str), sign_str, FormatAlign::Right)
605+
}
606+
607+
/// Format a complex number with locale-aware 'n' format.
608+
pub fn format_complex_locale(
609+
&self,
610+
num: &Complex64,
611+
locale: &LocaleInfo,
612+
) -> Result<String, FormatSpecError> {
613+
// Reuse format_complex_re_im with 'g' type to get the base formatted parts,
614+
// then apply locale grouping. This matches CPython's format_complex_internal:
615+
// 'n' → 'g', add_parens=0, skip_re=0.
616+
let locale_spec = FormatSpec {
617+
format_type: Some(FormatType::GeneralFormat(Case::Lower)),
618+
..*self
619+
};
620+
let (formatted_re, formatted_im) = locale_spec.format_complex_re_im(num)?;
621+
622+
// Apply locale grouping to both parts
623+
let grouped_re = if formatted_re.is_empty() {
624+
formatted_re
625+
} else {
626+
// Split sign from magnitude, apply grouping, recombine
627+
let (sign, mag) = if formatted_re.starts_with('-')
628+
|| formatted_re.starts_with('+')
629+
|| formatted_re.starts_with(' ')
630+
{
631+
formatted_re.split_at(1)
632+
} else {
633+
("", formatted_re.as_str())
634+
};
635+
format!(
636+
"{sign}{}",
637+
Self::apply_locale_formatting(mag.to_string(), locale)
638+
)
639+
};
640+
641+
// formatted_im is like "+1234j" or "-1234j" or "1234j"
642+
// Split sign, magnitude, and 'j' suffix
643+
let im_str = &formatted_im;
644+
let (im_sign, im_rest) = if im_str.starts_with('+') || im_str.starts_with('-') {
645+
im_str.split_at(1)
646+
} else {
647+
("", im_str.as_str())
648+
};
649+
let im_mag = im_rest.strip_suffix('j').unwrap_or(im_rest);
650+
let im_grouped = Self::apply_locale_formatting(im_mag.to_string(), locale);
651+
let grouped_im = format!("{im_sign}{im_grouped}j");
652+
653+
// No parentheses for 'n' format (CPython: add_parens=0)
654+
let magnitude_str = format!("{grouped_re}{grouped_im}");
655+
656+
self.format_sign_and_align(&AsciiStr::new(&magnitude_str), "", FormatAlign::Right)
657+
}
658+
463659
pub fn format_bool(&self, input: bool) -> Result<String, FormatSpecError> {
464660
let x = u8::from(input);
465661
match &self.format_type {

0 commit comments

Comments
 (0)