diff --git a/crates/vm/src/anystr.rs b/crates/vm/src/anystr.rs index 5abb34ed6f9..ed32605e587 100644 --- a/crates/vm/src/anystr.rs +++ b/crates/vm/src/anystr.rs @@ -4,9 +4,8 @@ use crate::{ convert::TryFromBorrowedObject, function::OptionalOption, }; -use icu_properties::{ - CodePointSetData, - props::{Alphabetic, ChangesWhenLowercased, ChangesWhenUppercased}, +use icu_properties::props::{ + BinaryProperty, EnumeratedProperty, GeneralCategory, GeneralCategoryGroup, }; use num_traits::{cast::ToPrimitive, sign::Signed}; @@ -405,42 +404,64 @@ pub trait AnyStr { rustpython_common::str::zfill(self.as_bytes(), width) } - // Unified form of CPython functions: - // _Py_bytes_islower - // unicode_islower_impl + // _Py_bytes_islower fn py_islower(&self) -> bool { - let case_change = CodePointSetData::new::(); - let alphabetic = CodePointSetData::new::(); let mut lower = false; - for chunk in self.as_bytes().utf8_chunks().map(|c| c.valid()) { - if chunk.chars().any(|c| case_change.contains(c)) { + for byte in self + .as_bytes() + .iter() + .copied() + .filter(u8::is_ascii_alphabetic) + { + if byte.is_ascii_uppercase() { return false; } - - if !lower && chunk.chars().any(|c| alphabetic.contains(c)) { - lower = true; - } + lower = true; } lower } - // Unified form of CPython functions: - // Py_bytes_isupper - // unicode_isupper_impl + // Py_bytes_isupper fn py_isupper(&self) -> bool { - let case_change = CodePointSetData::new::(); - let alphabetic = CodePointSetData::new::(); let mut upper = false; - for chunk in self.as_bytes().utf8_chunks().map(|c| c.valid()) { - if chunk.chars().any(|c| case_change.contains(c)) { + for byte in self + .as_bytes() + .iter() + .copied() + .filter(u8::is_ascii_alphabetic) + { + if byte.is_ascii_lowercase() { return false; } + upper = true; + } + upper + } - if !upper && chunk.chars().any(|c| alphabetic.contains(c)) { - upper = true; + // Unified form of CPython functions: + // unicode_isupper_impl + // unicode_islower_impl + fn is_cased(&self) -> bool + where + VALID: BinaryProperty, + INVALID: BinaryProperty, + { + let mut all_cased = false; + for c in self + .as_bytes() + .utf8_chunks() + .flat_map(|c| c.valid().chars()) + { + if INVALID::for_char(c) + || GeneralCategoryGroup::TitlecaseLetter.contains(GeneralCategory::for_char(c)) + { + return false; + } + if !all_cased && VALID::for_char(c) { + all_cased = true; } } - upper + all_cased } } diff --git a/crates/vm/src/builtins/str.rs b/crates/vm/src/builtins/str.rs index af1c4a5ae92..b94893fe954 100644 --- a/crates/vm/src/builtins/str.rs +++ b/crates/vm/src/builtins/str.rs @@ -6,7 +6,6 @@ use super::{ builtins_iter, }, }; -use crate::common::lock::LazyLock; use crate::{ AsObject, Context, Py, PyExact, PyObject, PyObjectRef, PyPayload, PyRef, PyRefExact, PyResult, TryFromBorrowedObject, VirtualMachine, @@ -14,6 +13,7 @@ use crate::{ atomic_func, cformat::cformat_string, class::PyClassImpl, + common::lock::LazyLock, common::str::{PyKindStr, StrData, StrKind}, convert::{IntoPyException, ToPyException, ToPyObject, ToPyResult}, format::{format, format_map}, @@ -46,7 +46,7 @@ use rustpython_common::{ use icu_properties::props::{ BidiClass, BinaryProperty, EnumeratedProperty, GeneralCategory, GeneralCategoryGroup, - NumericType, XidContinue, XidStart, + Lowercase, NumericType, Uppercase, XidContinue, XidStart, }; use unicode_casing::CharExt; @@ -2330,6 +2330,14 @@ impl AnyStr for str { } splits } + + fn py_islower(&self) -> bool { + self.is_cased::() + } + + fn py_isupper(&self) -> bool { + self.is_cased::() + } } impl AnyStrContainer for Wtf8Buf { @@ -2442,6 +2450,14 @@ impl AnyStr for Wtf8 { } splits } + + fn py_islower(&self) -> bool { + self.is_cased::() + } + + fn py_isupper(&self) -> bool { + self.is_cased::() + } } impl AnyStrContainer for AsciiString { diff --git a/crates/vm/src/stdlib/_thread.rs b/crates/vm/src/stdlib/_thread.rs index faba2e552ea..73313c53e06 100644 --- a/crates/vm/src/stdlib/_thread.rs +++ b/crates/vm/src/stdlib/_thread.rs @@ -459,7 +459,7 @@ pub(crate) mod _thread { { // On Unix, use pthread ID from the handle use std::os::unix::thread::JoinHandleExt; - handle.as_pthread_t() as u64 + handle.as_pthread_t() as _ } #[cfg(not(unix))] { diff --git a/extra_tests/snippets/builtin_str.py b/extra_tests/snippets/builtin_str.py index 3899c04956e..8cfb4f82b47 100644 --- a/extra_tests/snippets/builtin_str.py +++ b/extra_tests/snippets/builtin_str.py @@ -1,3 +1,5 @@ +import sys + from testutils import AssertRaises, assert_raises, skip_if_unsupported assert "".__eq__(1) == NotImplemented @@ -250,7 +252,10 @@ assert not "\U0001f431".islower() assert "\U0001f431 CAT".isupper() assert "\U0001f431 cat".islower() -assert "\u0295".islower() +if sys.version_info >= (3, 15): + assert not "\u0295".islower() + assert not "\u0295".isupper() + assert not "\u0295".istitle() assert "\u1c89".isupper() assert "hello, my name is".partition("my ") == ("hello, ", "my ", "name is") assert "hello".partition("is") == ("hello", "", "") @@ -525,6 +530,8 @@ def try_mutate_str(): assert "1a".islower() assert "가나다a".islower() assert "가나다A".isupper() +assert not "ジョジョ".isupper() +assert not "ジョジョ".islower() # test str.format_map() # diff --git a/extra_tests/snippets/builtin_str_encode.py b/extra_tests/snippets/builtin_str_encode.py index 156a83e205e..fc72e6ad4d1 100644 --- a/extra_tests/snippets/builtin_str_encode.py +++ b/extra_tests/snippets/builtin_str_encode.py @@ -20,3 +20,15 @@ def round_trip(s, encoding="utf-8"): round_trip("👺♦ 𝐚Şđƒ ☆☝") round_trip("☢🐣 ᖇ𝓤𝕊тⓟ𝕐𝕥卄σ𝔫 ♬👣") round_trip("💀👌 ק𝔂tℍⓞ𝓷 3 🔥👤") + +# Bytes should not assume an encoding for isupper/islower +assert "Æ".isupper() +assert not "Æ".encode().isupper() +assert "æ".islower() +assert not "æ".encode().islower() + +# Invalid Unicode +assert not b"\x80\x80".islower() +assert not b"\x80\x80".isupper() +assert b"\x80cat\x80".islower() +assert b"\x80CAT\x80".isupper()