diff --git a/Lib/test/test_locale.py b/Lib/test/test_locale.py index 196dc6e3a1..fff605d9b6 100644 --- a/Lib/test/test_locale.py +++ b/Lib/test/test_locale.py @@ -381,6 +381,8 @@ def setUp(self): is_emscripten or is_wasi, "musl libc issue on Emscripten/WASI, bpo-46390" ) + # TODO: RUSTPYTHON", strcoll has not been implemented + @unittest.expectedFailure def test_strcoll_with_diacritic(self): self.assertLess(locale.strcoll('à', 'b'), 0) @@ -390,6 +392,8 @@ def test_strcoll_with_diacritic(self): is_emscripten or is_wasi, "musl libc issue on Emscripten/WASI, bpo-46390" ) + # TODO: RUSTPYTHON", strxfrm has not been implemented + @unittest.expectedFailure def test_strxfrm_with_diacritic(self): self.assertLess(locale.strxfrm('à'), locale.strxfrm('b')) @@ -506,8 +510,6 @@ def test_japanese(self): class TestMiscellaneous(unittest.TestCase): - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_defaults_UTF8(self): # Issue #18378: on (at least) macOS setting LC_CTYPE to "UTF-8" is # valid. Furthermore LC_CTYPE=UTF is used by the UTF-8 locale coercing @@ -544,6 +546,10 @@ def test_defaults_UTF8(self): if orig_getlocale is not None: _locale._getdefaultlocale = orig_getlocale + + # TODO: RUSTPYTHON + if sys.platform == "win32": + test_defaults_UTF8 = unittest.expectedFailure(test_defaults_UTF8) def test_getencoding(self): # Invoke getencoding to make sure it does not cause exceptions. @@ -565,8 +571,6 @@ def test_strcoll_3303(self): self.assertRaises(TypeError, locale.strcoll, "a", None) self.assertRaises(TypeError, locale.strcoll, b"a", None) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_setlocale_category(self): locale.setlocale(locale.LC_ALL) locale.setlocale(locale.LC_TIME) @@ -577,6 +581,10 @@ def test_setlocale_category(self): # crasher from bug #7419 self.assertRaises(locale.Error, locale.setlocale, 12345) + + # TODO: RUSTPYTHON + if sys.platform == "win32": + test_setlocale_category = unittest.expectedFailure(test_setlocale_category) def test_getsetlocale_issue1813(self): # Issue #1813: setting and getting the locale under a Turkish locale diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 339c370088..2212d2323d 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -403,6 +403,7 @@ def test_float__format__locale(self): self.assertEqual(locale.format_string('%g', x, grouping=True), format(x, 'n')) self.assertEqual(locale.format_string('%.10g', x, grouping=True), format(x, '.10n')) + @unittest.skip("TODO: RustPython format code n is not integrated with locale") @run_with_locale('LC_NUMERIC', 'en_US.UTF8') def test_int__format__locale(self): # test locale support for __format__ code 'n' for integers diff --git a/stdlib/src/lib.rs b/stdlib/src/lib.rs index a4bbabf5ed..f3a35ace64 100644 --- a/stdlib/src/lib.rs +++ b/stdlib/src/lib.rs @@ -15,6 +15,7 @@ mod dis; mod gc; mod hashlib; mod json; +mod locale; mod math; #[cfg(unix)] mod mmap; @@ -159,5 +160,9 @@ pub fn get_module_inits() -> impl Iterator, StdlibInit { "_uuid" => uuid::make_module, } + #[cfg(all(unix, not(any(target_os = "ios", target_os = "android"))))] + { + "_locale" => locale::make_module, + } } } diff --git a/stdlib/src/locale.rs b/stdlib/src/locale.rs new file mode 100644 index 0000000000..631fda1e08 --- /dev/null +++ b/stdlib/src/locale.rs @@ -0,0 +1,151 @@ +#[cfg(all(unix, not(any(target_os = "ios", target_os = "android"))))] +pub(crate) use _locale::make_module; + +#[cfg(all(unix, not(any(target_os = "ios", target_os = "android"))))] +#[pymodule] +mod _locale { + use rustpython_vm::{ + builtins::{PyDictRef, PyIntRef, PyListRef, PyStrRef, PyTypeRef}, + convert::ToPyException, + function::OptionalArg, + PyObjectRef, PyResult, VirtualMachine, + }; + use std::{ + ffi::{CStr, CString}, + ptr, + }; + + #[pyattr] + use libc::{ + ABDAY_1, ABDAY_2, ABDAY_3, ABDAY_4, ABDAY_5, ABDAY_6, ABDAY_7, ABMON_1, ABMON_10, ABMON_11, + ABMON_12, ABMON_2, ABMON_3, ABMON_4, ABMON_5, ABMON_6, ABMON_7, ABMON_8, ABMON_9, + ALT_DIGITS, AM_STR, CODESET, CRNCYSTR, DAY_1, DAY_2, DAY_3, DAY_4, DAY_5, DAY_6, DAY_7, + D_FMT, D_T_FMT, ERA, ERA_D_FMT, ERA_D_T_FMT, ERA_T_FMT, LC_ALL, LC_COLLATE, LC_CTYPE, + LC_MESSAGES, LC_MONETARY, LC_NUMERIC, LC_TIME, MON_1, MON_10, MON_11, MON_12, MON_2, MON_3, + MON_4, MON_5, MON_6, MON_7, MON_8, MON_9, NOEXPR, PM_STR, RADIXCHAR, THOUSEP, T_FMT, + T_FMT_AMPM, YESEXPR, + }; + + #[pyattr(name = "CHAR_MAX")] + fn char_max(vm: &VirtualMachine) -> PyIntRef { + vm.ctx.new_int(libc::c_char::MAX) + } + + unsafe fn copy_grouping(group: *const libc::c_char, vm: &VirtualMachine) -> PyListRef { + let mut group_vec: Vec = Vec::new(); + if group.is_null() { + return vm.ctx.new_list(group_vec); + } + + let mut ptr = group; + while ![0_i8, libc::c_char::MAX].contains(&*ptr) { + let val = vm.ctx.new_int(*ptr); + group_vec.push(val.into()); + ptr = ptr.add(1); + } + // https://github.com/python/cpython/blob/677320348728ce058fa3579017e985af74a236d4/Modules/_localemodule.c#L80 + if !group_vec.is_empty() { + group_vec.push(vm.ctx.new_int(0).into()); + } + vm.ctx.new_list(group_vec) + } + + unsafe fn pystr_from_raw_cstr(vm: &VirtualMachine, raw_ptr: *const libc::c_char) -> PyResult { + let slice = unsafe { CStr::from_ptr(raw_ptr) }; + let string = slice + .to_str() + .expect("localeconv always return decodable string"); + Ok(vm.new_pyobj(string)) + } + + #[pyattr(name = "Error", once)] + fn error(vm: &VirtualMachine) -> PyTypeRef { + vm.ctx.new_exception_type( + "locale", + "Error", + Some(vec![vm.ctx.exceptions.exception_type.to_owned()]), + ) + } + + #[pyfunction] + fn localeconv(vm: &VirtualMachine) -> PyResult { + let result = vm.ctx.new_dict(); + + unsafe { + let lc = libc::localeconv(); + + macro_rules! set_string_field { + ($field:ident) => {{ + result.set_item( + stringify!($field), + pystr_from_raw_cstr(vm, (*lc).$field)?, + vm, + )? + }}; + } + + macro_rules! set_int_field { + ($field:ident) => {{ + result.set_item(stringify!($field), vm.new_pyobj((*lc).$field), vm)? + }}; + } + + macro_rules! set_group_field { + ($field:ident) => {{ + result.set_item( + stringify!($field), + copy_grouping((*lc).$field, vm).into(), + vm, + )? + }}; + } + + set_group_field!(mon_grouping); + set_group_field!(grouping); + set_int_field!(int_frac_digits); + set_int_field!(frac_digits); + set_int_field!(p_cs_precedes); + set_int_field!(p_sep_by_space); + set_int_field!(n_cs_precedes); + set_int_field!(p_sign_posn); + set_int_field!(n_sign_posn); + set_string_field!(decimal_point); + set_string_field!(thousands_sep); + set_string_field!(int_curr_symbol); + set_string_field!(currency_symbol); + set_string_field!(mon_decimal_point); + set_string_field!(mon_thousands_sep); + set_int_field!(n_sep_by_space); + set_string_field!(positive_sign); + set_string_field!(negative_sign); + } + Ok(result) + } + + #[derive(FromArgs)] + struct LocaleArgs { + #[pyarg(any)] + category: i32, + #[pyarg(any, optional)] + locale: OptionalArg>, + } + + #[pyfunction] + fn setlocale(args: LocaleArgs, vm: &VirtualMachine) -> PyResult { + unsafe { + let result = match args.locale.flatten() { + None => libc::setlocale(args.category, ptr::null()), + Some(locale) => { + let c_locale: CString = + CString::new(locale.as_str()).map_err(|e| e.to_pyexception(vm))?; + libc::setlocale(args.category, c_locale.as_ptr()) + } + }; + if result.is_null() { + let error = error(vm); + return Err(vm.new_exception_msg(error, String::from("unsupported locale setting"))); + } + pystr_from_raw_cstr(vm, result) + } + } +}