Fix remaining tests

This commit is contained in:
Noa committed 2025-03-25 19:05:12 -05:00
1 parent 5c22697344
commit a86126419c
19 files changed
+150 -106

No files matched your search

-2
View File
@@ -1066,8 +1066,6 @@ class CommonTest(BaseTest):
hash(b)
self.assertEqual(hash(a), hash(b))
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_capitalize_nonascii(self):
# check that titlecased chars are lowered correctly
# \u1ffc is the titlecased char
+1 -2
View File
@@ -574,8 +574,7 @@ class CmdLineTest(unittest.TestCase):
self.assertTrue(text[1].startswith(' File '))
self.assertTrue(text[3].startswith('NameError'))
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.expectedFailureIf(sys.platform == "linux", "TODO: RUSTPYTHON")
def test_non_ascii(self):
# Mac OS X denies the creation of a file with an invalid UTF-8 name.
# Windows allows creating a name with an arbitrary bytes name, but
+2
View File
@@ -1305,6 +1305,8 @@ class ImportTracebackTests(unittest.TestCase):
else:
importlib.SourceLoader.exec_module = old_exec_module
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.skipUnless(TESTFN_UNENCODABLE, 'need TESTFN_UNENCODABLE')
def test_unencodable_filename(self):
# Issue #11619: The Python parser and the import machinery must not
-6
View File
@@ -143,10 +143,4 @@ class TestScanstring:
class TestPyScanstring(TestScanstring, PyTest): pass
# TODO: RUSTPYTHON
class TestPyScanstring(TestScanstring, PyTest):
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_bad_escapes(self):
super().test_bad_escapes()
class TestCScanstring(TestScanstring, CTest): pass
-6
View File
@@ -1032,12 +1032,6 @@ class NtCommonTest(test_genericpath.CommonTest, unittest.TestCase):
pathmodule = ntpath
attributes = ['relpath']
# TODO: RUSTPYTHON
if sys.platform == "linux":
@unittest.expectedFailure
def test_nonascii_abspath(self):
super().test_nonascii_abspath()
# TODO: RUSTPYTHON
if sys.platform == "win32":
# TODO: RUSTPYTHON, ValueError: illegal environment variable name
+1
View File
@@ -2231,6 +2231,7 @@ class ReTests(unittest.TestCase):
with self.assertRaisesRegex(TypeError, "got 'type'"):
re.search("x*", type)
@unittest.skip("TODO: RUSTPYTHON: flaky, improve perf")
@requires_resource('cpu')
def test_search_anchor_at_beginning(self):
s = 'x'*10**7
+1
View File
@@ -1578,6 +1578,7 @@ class GeneralModuleTests(unittest.TestCase):
# only IP addresses are allowed
self.assertRaises(OSError, socket.getnameinfo, ('mail.python.org',0), 0)
@unittest.skip("TODO: RUSTPYTHON: flaky on CI?")
@unittest.skipUnless(support.is_resource_enabled('network'),
'network is not enabled')
def test_idna(self):
-2
View File
@@ -95,8 +95,6 @@ class SqliteTypeTests(unittest.TestCase):
row = self.cur.fetchone()
self.assertIsNone(row)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_string_with_surrogates(self):
for value in 0xd8ff, 0xdcff:
with self.assertRaises(UnicodeEncodeError):
-2
View File
@@ -102,8 +102,6 @@ class UnicodeNamesTest(unittest.TestCase):
self.checkletter("CJK UNIFIED IDEOGRAPH-2B81D", "\U0002B81D")
self.checkletter("CJK UNIFIED IDEOGRAPH-3134A", "\U0003134A")
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_bmp_characters(self):
for code in range(0x10000):
char = chr(code)
-2
View File
@@ -721,8 +721,6 @@ class UnicodeTest(string_tests.CommonTest,
'\U0001F40D', '\U0001F46F']:
self.assertFalse(ch.isspace(), '{!a} is not space.'.format(ch))
# TODO: RUSTPYTHON
@unittest.expectedFailure
@support.requires_resource('cpu')
def test_isspace_invariant(self):
for codepoint in range(sys.maxunicode + 1):
-6
View File
@@ -99,8 +99,6 @@ class UnicodeFunctionsTest(UnicodeDatabaseTest):
result = h.hexdigest()
self.assertEqual(result, self.expectedchecksum)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@requires_resource('cpu')
def test_name_inverse_lookup(self):
for i in range(sys.maxunicode + 1):
@@ -326,8 +324,6 @@ class UnicodeMiscTest(UnicodeDatabaseTest):
self.assertTrue("\u1d79".upper()=='\ua77d')
self.assertTrue(".".upper()=='.')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_bug_5828(self):
self.assertEqual("\u1d79".lower(), "\u1d79")
# Only U+0000 should have U+0000 as its upper/lower/titlecase variant
@@ -347,8 +343,6 @@ class UnicodeMiscTest(UnicodeDatabaseTest):
self.assertEqual("\u01c5".title(), "\u01c5")
self.assertEqual("\u01c6".title(), "\u01c5")
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_linebreak_7643(self):
for i in range(0x10000):
lines = (chr(i) + 'A').splitlines()
+19
View File
@@ -552,6 +552,12 @@ impl Extend<CodePoint> for Wtf8Buf {
}
}
impl Extend<char> for Wtf8Buf {
fn extend<T: IntoIterator<Item = char>>(&mut self, iter: T) {
self.extend(iter.into_iter().map(CodePoint::from))
}
}
impl<W: AsRef<Wtf8>> Extend<W> for Wtf8Buf {
fn extend<T: IntoIterator<Item = W>>(&mut self, iter: T) {
iter.into_iter()
@@ -1004,6 +1010,14 @@ impl Wtf8 {
memchr::memmem::rfind(self.as_bytes(), pat.as_bytes())
}
pub fn find_iter(&self, pat: &Wtf8) -> impl Iterator<Item = usize> {
memchr::memmem::find_iter(self.as_bytes(), pat.as_bytes())
}
pub fn rfind_iter(&self, pat: &Wtf8) -> impl Iterator<Item = usize> {
memchr::memmem::rfind_iter(self.as_bytes(), pat.as_bytes())
}
pub fn contains(&self, pat: &Wtf8) -> bool {
self.bytes.contains_str(pat)
}
@@ -1057,6 +1071,11 @@ impl Wtf8 {
let w = self.bytes.replace(from, to);
unsafe { Wtf8Buf::from_bytes_unchecked(w) }
}
pub fn replacen(&self, from: &Wtf8, to: &Wtf8, n: usize) -> Wtf8Buf {
let w = self.bytes.replacen(from, to, n);
unsafe { Wtf8Buf::from_bytes_unchecked(w) }
}
}
impl AsRef<Wtf8> for str {
+5 -2
View File
@@ -2929,9 +2929,12 @@ mod _sqlite {
}
fn str_to_ptr_len(s: &PyStr, vm: &VirtualMachine) -> PyResult<(*const libc::c_char, i32)> {
let len = c_int::try_from(s.byte_len())
let s = s
.to_str()
.ok_or_else(|| vm.new_unicode_encode_error("surrogates not allowed".to_owned()))?;
let len = c_int::try_from(s.len())
.map_err(|_| vm.new_overflow_error("TEXT longer than INT_MAX bytes".to_owned()))?;
let ptr = s.as_str().as_ptr().cast();
let ptr = s.as_ptr().cast();
Ok((ptr, len))
}
+2 -2
View File
@@ -403,8 +403,8 @@ mod _ssl {
.to_str()
.unwrap();
let (cert_file, cert_dir) = get_cert_file_dir();
let cert_file = OsPath::new_str(cert_file).filename(vm)?;
let cert_dir = OsPath::new_str(cert_dir).filename(vm)?;
let cert_file = OsPath::new_str(cert_file).filename(vm);
let cert_dir = OsPath::new_str(cert_dir).filename(vm);
Ok((cert_file_env, cert_file, cert_dir_env, cert_dir))
}
+88 -53
View File
@@ -38,7 +38,7 @@ use rustpython_common::{
str::DeduceStrKind,
wtf8::{CodePoint, Wtf8, Wtf8Buf, Wtf8Chunk},
};
use std::{char, fmt, ops::Range};
use std::{borrow::Cow, char, fmt, ops::Range};
use unic_ucd_bidi::BidiClass;
use unic_ucd_category::GeneralCategory;
use unic_ucd_ident::{is_xid_continue, is_xid_start};
@@ -420,6 +420,16 @@ impl PyStr {
self.data.as_str().expect("str has surrogates")
}
pub fn to_str(&self) -> Option<&str> {
self.data.as_str()
}
pub fn to_string_lossy(&self) -> Cow<'_, str> {
self.to_str()
.map(Cow::Borrowed)
.unwrap_or_else(|| self.as_wtf8().to_string_lossy())
}
pub fn kind(&self) -> StrKind {
self.data.kind()
}
@@ -629,16 +639,42 @@ impl PyStr {
}
#[pymethod]
fn capitalize(&self) -> String {
let mut chars = self.as_str().chars();
if let Some(first_char) = chars.next() {
format!(
"{}{}",
first_char.to_uppercase(),
&chars.as_str().to_lowercase(),
)
} else {
"".to_owned()
fn capitalize(&self) -> Wtf8Buf {
match self.as_str_kind() {
PyKindStr::Ascii(s) => {
let mut s = s.to_owned();
if let [first, rest @ ..] = s.as_mut_slice() {
first.make_ascii_uppercase();
ascii::AsciiStr::make_ascii_lowercase(rest.into());
}
s.into()
}
PyKindStr::Utf8(s) => {
let mut chars = s.chars();
let mut out = String::with_capacity(s.len());
if let Some(c) = chars.next() {
out.extend(c.to_titlecase());
out.push_str(&chars.as_str().to_lowercase());
}
out.into()
}
PyKindStr::Wtf8(s) => {
let mut out = Wtf8Buf::with_capacity(s.len());
let mut chars = s.code_points();
if let Some(ch) = chars.next() {
match ch.to_char() {
Some(ch) => out.extend(ch.to_titlecase()),
None => out.push(ch),
}
for chunk in chars.as_wtf8().chunks() {
match chunk {
Wtf8Chunk::Utf8(s) => out.push_str(&s.to_lowercase()),
Wtf8Chunk::Surrogate(ch) => out.push(ch),
}
}
}
out
}
}
}
@@ -844,14 +880,11 @@ impl PyStr {
#[pymethod]
fn isdigit(&self) -> bool {
// python's isdigit also checks if exponents are digits, these are the unicode codepoints for exponents
let valid_codepoints: [u16; 10] = [
0x2070, 0x00B9, 0x00B2, 0x00B3, 0x2074, 0x2075, 0x2076, 0x2077, 0x2078, 0x2079,
];
let s = self.as_str();
!s.is_empty()
&& s.chars()
.filter(|c| !c.is_ascii_digit())
.all(|c| valid_codepoints.contains(&(c as u16)))
!self.data.is_empty()
&& self.char_all(|c| {
c.is_ascii_digit()
|| matches!(c, '⁰' | '¹' | '²' | '³' | '⁴' | '⁵' | '⁶' | '⁷' | '⁸' | '⁹')
})
}
#[pymethod]
@@ -910,43 +943,45 @@ impl PyStr {
/// Return a titlecased version of the string where words start with an
/// uppercase character and the remaining characters are lowercase.
#[pymethod]
fn title(&self) -> String {
let mut title = String::with_capacity(self.data.len());
fn title(&self) -> Wtf8Buf {
let mut title = Wtf8Buf::with_capacity(self.data.len());
let mut previous_is_cased = false;
for c in self.as_str().chars() {
for c_orig in self.as_wtf8().code_points() {
let c = c_orig.to_char_lossy();
if c.is_lowercase() {
if !previous_is_cased {
title.extend(c.to_titlecase());
} else {
title.push(c);
title.push_char(c);
}
previous_is_cased = true;
} else if c.is_uppercase() || c.is_titlecase() {
if previous_is_cased {
title.extend(c.to_lowercase());
} else {
title.push(c);
title.push_char(c);
}
previous_is_cased = true;
} else {
previous_is_cased = false;
title.push(c);
title.push(c_orig);
}
}
title
}
#[pymethod]
fn swapcase(&self) -> String {
let mut swapped_str = String::with_capacity(self.data.len());
for c in self.as_str().chars() {
fn swapcase(&self) -> Wtf8Buf {
let mut swapped_str = Wtf8Buf::with_capacity(self.data.len());
for c_orig in self.as_wtf8().code_points() {
let c = c_orig.to_char_lossy();
// to_uppercase returns an iterator, to_ascii_uppercase returns the char
if c.is_lowercase() {
swapped_str.push(c.to_ascii_uppercase());
swapped_str.push_char(c.to_ascii_uppercase());
} else if c.is_uppercase() {
swapped_str.push(c.to_ascii_lowercase());
swapped_str.push_char(c.to_ascii_lowercase());
} else {
swapped_str.push(c);
swapped_str.push(c_orig);
}
}
swapped_str
@@ -958,20 +993,20 @@ impl PyStr {
}
#[pymethod]
fn replace(&self, old: PyStrRef, new: PyStrRef, count: OptionalArg<isize>) -> String {
let s = self.as_str();
fn replace(&self, old: PyStrRef, new: PyStrRef, count: OptionalArg<isize>) -> Wtf8Buf {
let s = self.as_wtf8();
match count {
OptionalArg::Present(max_count) if max_count >= 0 => {
if max_count == 0 || (s.is_empty() && !old.is_empty()) {
// nothing to do; return the original bytes
s.to_owned()
} else if s.is_empty() && old.is_empty() {
new.as_str().to_owned()
new.as_wtf8().to_owned()
} else {
s.replacen(old.as_str(), new.as_str(), max_count as usize)
s.replacen(old.as_wtf8(), new.as_wtf8(), max_count as usize)
}
}
_ => s.replace(old.as_str(), new.as_str()),
_ => s.replace(old.as_wtf8(), new.as_wtf8()),
}
}
@@ -1148,7 +1183,7 @@ impl PyStr {
if has_mid {
sep
} else {
vm.ctx.new_str(ascii!(""))
vm.ctx.empty_str.to_owned()
},
self.new_substr(back),
)
@@ -1165,7 +1200,7 @@ impl PyStr {
let mut cased = false;
let mut previous_is_cased = false;
for c in self.as_str().chars() {
for c in self.as_wtf8().code_points().map(CodePoint::to_char_lossy) {
if c.is_uppercase() || c.is_titlecase() {
if previous_is_cased {
return false;
@@ -1188,15 +1223,15 @@ impl PyStr {
#[pymethod]
fn count(&self, args: FindArgs) -> usize {
let (needle, range) = args.get_value(self.len());
self.as_str()
.py_count(needle.as_str(), range, |h, n| h.matches(n).count())
self.as_wtf8()
.py_count(needle.as_wtf8(), range, |h, n| h.find_iter(n).count())
}
#[pymethod]
fn zfill(&self, width: isize) -> String {
fn zfill(&self, width: isize) -> Wtf8Buf {
unsafe {
// SAFETY: this is safe-guaranteed because the original self.as_str() is valid utf8
String::from_utf8_unchecked(self.as_str().py_zfill(width))
// SAFETY: this is safe-guaranteed because the original self.as_wtf8() is valid wtf8
Wtf8Buf::from_bytes_unchecked(self.as_wtf8().py_zfill(width))
}
}
@@ -1205,20 +1240,20 @@ impl PyStr {
&self,
width: isize,
fillchar: OptionalArg<PyStrRef>,
pad: fn(&str, usize, char, usize) -> String,
pad: fn(&Wtf8, usize, CodePoint, usize) -> Wtf8Buf,
vm: &VirtualMachine,
) -> PyResult<String> {
let fillchar = fillchar.map_or(Ok(' '), |ref s| {
s.as_str().chars().exactly_one().map_err(|_| {
) -> PyResult<Wtf8Buf> {
let fillchar = fillchar.map_or(Ok(' '.into()), |ref s| {
s.as_wtf8().code_points().exactly_one().map_err(|_| {
vm.new_type_error(
"The fill character must be exactly one character long".to_owned(),
)
})
})?;
Ok(if self.len() as isize >= width {
String::from(self.as_str())
self.as_wtf8().to_owned()
} else {
pad(self.as_str(), width as usize, fillchar, self.len())
pad(self.as_wtf8(), width as usize, fillchar, self.len())
})
}
@@ -1228,7 +1263,7 @@ impl PyStr {
width: isize,
fillchar: OptionalArg<PyStrRef>,
vm: &VirtualMachine,
) -> PyResult<String> {
) -> PyResult<Wtf8Buf> {
self._pad(width, fillchar, AnyStr::py_center, vm)
}
@@ -1238,7 +1273,7 @@ impl PyStr {
width: isize,
fillchar: OptionalArg<PyStrRef>,
vm: &VirtualMachine,
) -> PyResult<String> {
) -> PyResult<Wtf8Buf> {
self._pad(width, fillchar, AnyStr::py_ljust, vm)
}
@@ -1248,7 +1283,7 @@ impl PyStr {
width: isize,
fillchar: OptionalArg<PyStrRef>,
vm: &VirtualMachine,
) -> PyResult<String> {
) -> PyResult<Wtf8Buf> {
self._pad(width, fillchar, AnyStr::py_rjust, vm)
}
@@ -2186,7 +2221,7 @@ mod tests {
("Greek ῼitlecases ...", "greek ῳitlecases ..."),
];
for (title, input) in tests {
assert_eq!(PyStr::from(input).title().as_str(), title);
assert_eq!(PyStr::from(input).title().as_str(), Ok(title));
}
}
+12 -10
View File
@@ -426,12 +426,13 @@ fn xmlcharrefreplace_errors(err: PyObjectRef, vm: &VirtualMachine) -> PyResult<(
}
let range = extract_unicode_error_range(&err, vm)?;
let s = PyStrRef::try_from_object(vm, err.get_attr("object", vm)?)?;
let s_after_start = crate::common::str::try_get_chars(s.as_str(), range.start..).unwrap_or("");
let s_after_start =
crate::common::str::try_get_codepoints(s.as_wtf8(), range.start..).unwrap_or_default();
let num_chars = range.len();
// capacity rough guess; assuming that the codepoints are 3 digits in decimal + the &#;
let mut out = String::with_capacity(num_chars * 6);
for c in s_after_start.chars().take(num_chars) {
write!(out, "&#{};", c as u32).unwrap()
for c in s_after_start.code_points().take(num_chars) {
write!(out, "&#{};", c.to_u32()).unwrap()
}
Ok((out, range.end))
}
@@ -450,12 +451,13 @@ fn backslashreplace_errors(err: PyObjectRef, vm: &VirtualMachine) -> PyResult<(S
}
let range = extract_unicode_error_range(&err, vm)?;
let s = PyStrRef::try_from_object(vm, err.get_attr("object", vm)?)?;
let s_after_start = crate::common::str::try_get_chars(s.as_str(), range.start..).unwrap_or("");
let s_after_start =
crate::common::str::try_get_codepoints(s.as_wtf8(), range.start..).unwrap_or_default();
let num_chars = range.len();
// minimum 4 output bytes per char: \xNN
let mut out = String::with_capacity(num_chars * 4);
for c in s_after_start.chars().take(num_chars) {
let c = c as u32;
for c in s_after_start.code_points().take(num_chars) {
let c = c.to_u32();
if c >= 0x10000 {
write!(out, "\\U{c:08x}").unwrap();
} else if c >= 0x100 {
@@ -472,12 +474,12 @@ fn namereplace_errors(err: PyObjectRef, vm: &VirtualMachine) -> PyResult<(String
let range = extract_unicode_error_range(&err, vm)?;
let s = PyStrRef::try_from_object(vm, err.get_attr("object", vm)?)?;
let s_after_start =
crate::common::str::try_get_chars(s.as_str(), range.start..).unwrap_or("");
crate::common::str::try_get_codepoints(s.as_wtf8(), range.start..).unwrap_or_default();
let num_chars = range.len();
let mut out = String::with_capacity(num_chars * 4);
for c in s_after_start.chars().take(num_chars) {
let c_u32 = c as u32;
if let Some(c_name) = unicode_names2::name(c) {
for c in s_after_start.code_points().take(num_chars) {
let c_u32 = c.to_u32();
if let Some(c_name) = unicode_names2::name(c.to_char_lossy()) {
write!(out, "\\N{{{c_name}}}").unwrap();
} else if c_u32 >= 0x10000 {
write!(out, "\\U{c_u32:08x}").unwrap();
+3 -3
View File
@@ -74,10 +74,10 @@ impl FsPath {
}
}
pub fn as_str(&self) -> &str {
pub fn to_string_lossy(&self) -> Cow<'_, str> {
match self {
FsPath::Bytes(b) => std::str::from_utf8(b).unwrap(),
FsPath::Str(s) => s.as_str(),
FsPath::Str(s) => s.to_string_lossy(),
FsPath::Bytes(s) => String::from_utf8_lossy(s),
}
}
+2 -2
View File
@@ -155,7 +155,7 @@ mod builtins {
return ast::compile(
vm,
args.source,
args.filename.as_str(),
&args.filename.to_string_lossy(),
mode,
Some(optimize),
);
@@ -204,7 +204,7 @@ mod builtins {
.compile_with_opts(
source,
mode,
args.filename.as_str().to_owned(),
args.filename.to_string_lossy().into_owned(),
opts,
)
.map_err(|err| (err, Some(source)).to_pyexception(vm))?;
+14 -6
View File
@@ -916,12 +916,20 @@ impl VirtualMachine {
}
pub fn fsdecode(&self, s: impl Into<OsString>) -> PyStrRef {
let bytes = self.ctx.new_bytes(s.into().into_encoded_bytes());
let errors = self.fs_encode_errors().to_owned();
self.state
.codec_registry
.decode_text(bytes.into(), "utf-8", Some(errors), self)
.unwrap() // this should never fail, since fsdecode should be lossless from the fs encoding
match s.into().into_string() {
Ok(s) => self.ctx.new_str(s),
Err(s) => {
let bytes = self.ctx.new_bytes(s.into_encoded_bytes());
let errors = self.fs_encode_errors().to_owned();
let res = self.state.codec_registry.decode_text(
bytes.into(),
"utf-8",
Some(errors),
self,
);
self.expect_pyresult(res, "fsdecode should be lossless and never fail")
}
}
}
pub fn fsencode<'a>(&self, s: &'a Py<PyStr>) -> PyResult<Cow<'a, OsStr>> {