Skip to content

Commit daaf906

Browse files
authored
Implement Python 3.9 style string functions: removeprefix and removesuffix PEP616
* implement and test Py39 string operations removeprefix and removesuffix. * Added test snippets for it using an also contained extension of testutils
1 parent 2b5597d commit daaf906

File tree

8 files changed

+264
-2
lines changed

8 files changed

+264
-2
lines changed

Lib/test/string_tests.py

+39
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,45 @@ def test_replace_overflow(self):
681681
self.checkraises(OverflowError, A2_16, "replace", "A", A2_16)
682682
self.checkraises(OverflowError, A2_16, "replace", "AA", A2_16+A2_16)
683683

684+
685+
# Python 3.9
686+
def test_removeprefix(self):
687+
self.checkequal('am', 'spam', 'removeprefix', 'sp')
688+
self.checkequal('spamspam', 'spamspamspam', 'removeprefix', 'spam')
689+
self.checkequal('spam', 'spam', 'removeprefix', 'python')
690+
self.checkequal('spam', 'spam', 'removeprefix', 'spider')
691+
self.checkequal('spam', 'spam', 'removeprefix', 'spam and eggs')
692+
693+
self.checkequal('', '', 'removeprefix', '')
694+
self.checkequal('', '', 'removeprefix', 'abcde')
695+
self.checkequal('abcde', 'abcde', 'removeprefix', '')
696+
self.checkequal('', 'abcde', 'removeprefix', 'abcde')
697+
698+
self.checkraises(TypeError, 'hello', 'removeprefix')
699+
self.checkraises(TypeError, 'hello', 'removeprefix', 42)
700+
self.checkraises(TypeError, 'hello', 'removeprefix', 42, 'h')
701+
self.checkraises(TypeError, 'hello', 'removeprefix', 'h', 42)
702+
self.checkraises(TypeError, 'hello', 'removeprefix', ("he", "l"))
703+
704+
# Python 3.9
705+
def test_removesuffix(self):
706+
self.checkequal('sp', 'spam', 'removesuffix', 'am')
707+
self.checkequal('spamspam', 'spamspamspam', 'removesuffix', 'spam')
708+
self.checkequal('spam', 'spam', 'removesuffix', 'python')
709+
self.checkequal('spam', 'spam', 'removesuffix', 'blam')
710+
self.checkequal('spam', 'spam', 'removesuffix', 'eggs and spam')
711+
712+
self.checkequal('', '', 'removesuffix', '')
713+
self.checkequal('', '', 'removesuffix', 'abcde')
714+
self.checkequal('abcde', 'abcde', 'removesuffix', '')
715+
self.checkequal('', 'abcde', 'removesuffix', 'abcde')
716+
717+
self.checkraises(TypeError, 'hello', 'removesuffix')
718+
self.checkraises(TypeError, 'hello', 'removesuffix', 42)
719+
self.checkraises(TypeError, 'hello', 'removesuffix', 42, 'h')
720+
self.checkraises(TypeError, 'hello', 'removesuffix', 'h', 42)
721+
self.checkraises(TypeError, 'hello', 'removesuffix', ("lo", "l"))
722+
684723
def test_capitalize(self):
685724
self.checkequal(' hello ', ' hello ', 'capitalize')
686725
self.checkequal('Hello ', 'Hello ','capitalize')

tests/snippets/strings.py

+96-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from testutils import assert_raises, AssertRaises
1+
from testutils import assert_raises, AssertRaises, skip_if_unsupported
22

33
assert "".__eq__(1) == NotImplemented
44
assert "a" == 'a'
@@ -471,3 +471,98 @@ def try_mutate_str():
471471
assert '{:e}'.format(float('inf')) == 'inf'
472472
assert '{:e}'.format(float('-inf')) == '-inf'
473473
assert '{:E}'.format(float('inf')) == 'INF'
474+
475+
476+
# remove*fix test
477+
def test_removeprefix():
478+
s = 'foobarfoo'
479+
s_ref='foobarfoo'
480+
assert s.removeprefix('f') == s_ref[1:]
481+
assert s.removeprefix('fo') == s_ref[2:]
482+
assert s.removeprefix('foo') == s_ref[3:]
483+
484+
assert s.removeprefix('') == s_ref
485+
assert s.removeprefix('bar') == s_ref
486+
assert s.removeprefix('lol') == s_ref
487+
assert s.removeprefix('_foo') == s_ref
488+
assert s.removeprefix('-foo') == s_ref
489+
assert s.removeprefix('afoo') == s_ref
490+
assert s.removeprefix('*foo') == s_ref
491+
492+
assert s==s_ref, 'undefined test fail'
493+
494+
s_uc = '😱foobarfoo🖖'
495+
s_ref_uc = '😱foobarfoo🖖'
496+
assert s_uc.removeprefix('😱') == s_ref_uc[1:]
497+
assert s_uc.removeprefix('😱fo') == s_ref_uc[3:]
498+
assert s_uc.removeprefix('😱foo') == s_ref_uc[4:]
499+
500+
assert s_uc.removeprefix('🖖') == s_ref_uc
501+
assert s_uc.removeprefix('foo') == s_ref_uc
502+
assert s_uc.removeprefix(' ') == s_ref_uc
503+
assert s_uc.removeprefix('_😱') == s_ref_uc
504+
assert s_uc.removeprefix(' 😱') == s_ref_uc
505+
assert s_uc.removeprefix('-😱') == s_ref_uc
506+
assert s_uc.removeprefix('#😱') == s_ref_uc
507+
508+
def test_removeprefix_types():
509+
s='0123456'
510+
s_ref='0123456'
511+
others=[0,['012']]
512+
found=False
513+
for o in others:
514+
try:
515+
s.removeprefix(o)
516+
except:
517+
found=True
518+
519+
assert found, f'Removeprefix accepts other type: {type(o)}: {o=}'
520+
521+
def test_removesuffix():
522+
s='foobarfoo'
523+
s_ref='foobarfoo'
524+
assert s.removesuffix('o') == s_ref[:-1]
525+
assert s.removesuffix('oo') == s_ref[:-2]
526+
assert s.removesuffix('foo') == s_ref[:-3]
527+
528+
assert s.removesuffix('') == s_ref
529+
assert s.removesuffix('bar') == s_ref
530+
assert s.removesuffix('lol') == s_ref
531+
assert s.removesuffix('foo_') == s_ref
532+
assert s.removesuffix('foo-') == s_ref
533+
assert s.removesuffix('foo*') == s_ref
534+
assert s.removesuffix('fooa') == s_ref
535+
536+
assert s==s_ref, 'undefined test fail'
537+
538+
s_uc = '😱foobarfoo🖖'
539+
s_ref_uc = '😱foobarfoo🖖'
540+
assert s_uc.removesuffix('🖖') == s_ref_uc[:-1]
541+
assert s_uc.removesuffix('oo🖖') == s_ref_uc[:-3]
542+
assert s_uc.removesuffix('foo🖖') == s_ref_uc[:-4]
543+
544+
assert s_uc.removesuffix('😱') == s_ref_uc
545+
assert s_uc.removesuffix('foo') == s_ref_uc
546+
assert s_uc.removesuffix(' ') == s_ref_uc
547+
assert s_uc.removesuffix('🖖_') == s_ref_uc
548+
assert s_uc.removesuffix('🖖 ') == s_ref_uc
549+
assert s_uc.removesuffix('🖖-') == s_ref_uc
550+
assert s_uc.removesuffix('🖖#') == s_ref_uc
551+
552+
def test_removesuffix_types():
553+
s='0123456'
554+
s_ref='0123456'
555+
others=[0,6,['6']]
556+
found=False
557+
for o in others:
558+
try:
559+
s.removesuffix(o)
560+
except:
561+
found=True
562+
563+
assert found, f'Removesuffix accepts other type: {type(o)}: {o=}'
564+
565+
skip_if_unsupported(3,9,test_removeprefix)
566+
skip_if_unsupported(3,9,test_removeprefix_types)
567+
skip_if_unsupported(3,9,test_removesuffix)
568+
skip_if_unsupported(3,9,test_removesuffix_types)

tests/snippets/testutils.py

-1
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,3 @@ def exec():
9292
exec()
9393
else:
9494
assert False, f'Test cannot performed on this python version. {platform.python_implementation()} {platform.python_version()}'
95-

vm/src/obj/objbytearray.rs

+24
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,30 @@ impl PyByteArray {
387387
self.borrow_value().rstrip(chars).into()
388388
}
389389

390+
/// removeprefix($self, prefix, /)
391+
///
392+
///
393+
/// Return a bytearray object with the given prefix string removed if present.
394+
///
395+
/// If the bytearray starts with the prefix string, return string[len(prefix):]
396+
/// Otherwise, return a copy of the original bytearray.
397+
#[pymethod(name = "removeprefix")]
398+
fn removeprefix(&self, prefix: PyByteInner) -> PyByteArray {
399+
self.borrow_value().removeprefix(prefix).into()
400+
}
401+
402+
/// removesuffix(self, prefix, /)
403+
///
404+
///
405+
/// Return a bytearray object with the given suffix string removed if present.
406+
///
407+
/// If the bytearray ends with the suffix string, return string[:len(suffix)]
408+
/// Otherwise, return a copy of the original bytearray.
409+
#[pymethod(name = "removesuffix")]
410+
fn removesuffix(&self, suffix: PyByteInner) -> PyByteArray {
411+
self.borrow_value().removesuffix(suffix).to_vec().into()
412+
}
413+
390414
#[pymethod(name = "split")]
391415
fn split(&self, options: ByteInnerSplitOptions, vm: &VirtualMachine) -> PyResult {
392416
self.borrow_value()

vm/src/obj/objbyteinner.rs

+18
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,24 @@ impl PyByteInner {
840840
.to_vec()
841841
}
842842

843+
// new in Python 3.9
844+
pub fn removeprefix(&self, prefix: PyByteInner) -> Vec<u8> {
845+
self.elements
846+
.py_removeprefix(&prefix.elements, prefix.elements.len(), |s, p| {
847+
s.starts_with(p)
848+
})
849+
.to_vec()
850+
}
851+
852+
// new in Python 3.9
853+
pub fn removesuffix(&self, suffix: PyByteInner) -> Vec<u8> {
854+
self.elements
855+
.py_removesuffix(&suffix.elements, suffix.elements.len(), |s, p| {
856+
s.ends_with(p)
857+
})
858+
.to_vec()
859+
}
860+
843861
pub fn split<F>(
844862
&self,
845863
options: ByteInnerSplitOptions,

vm/src/obj/objbytes.rs

+24
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,30 @@ impl PyBytes {
345345
self.inner.rstrip(chars).into()
346346
}
347347

348+
/// removeprefix($self, prefix, /)
349+
///
350+
///
351+
/// Return a bytes object with the given prefix string removed if present.
352+
///
353+
/// If the bytes starts with the prefix string, return string[len(prefix):]
354+
/// Otherwise, return a copy of the original bytes.
355+
#[pymethod(name = "removeprefix")]
356+
fn removeprefix(&self, prefix: PyByteInner) -> PyBytes {
357+
self.inner.removeprefix(prefix).into()
358+
}
359+
360+
/// removesuffix(self, prefix, /)
361+
///
362+
///
363+
/// Return a bytes object with the given suffix string removed if present.
364+
///
365+
/// If the bytes ends with the suffix string, return string[:len(suffix)]
366+
/// Otherwise, return a copy of the original bytes.
367+
#[pymethod(name = "removesuffix")]
368+
fn removesuffix(&self, suffix: PyByteInner) -> PyBytes {
369+
self.inner.removesuffix(suffix).into()
370+
}
371+
348372
#[pymethod(name = "split")]
349373
fn split(&self, options: ByteInnerSplitOptions, vm: &VirtualMachine) -> PyResult {
350374
self.inner

vm/src/obj/objstr.rs

+30
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,36 @@ impl PyString {
529529
)
530530
}
531531

532+
/// removeprefix($self, prefix, /)
533+
///
534+
///
535+
/// Return a str with the given prefix string removed if present.
536+
///
537+
/// If the string starts with the prefix string, return string[len(prefix):]
538+
/// Otherwise, return a copy of the original string.
539+
#[pymethod]
540+
fn removeprefix(&self, pref: PyStringRef) -> String {
541+
self.value
542+
.as_str()
543+
.py_removeprefix(&pref.value, pref.value.len(), |s, p| s.starts_with(p))
544+
.to_string()
545+
}
546+
547+
/// removesuffix(self, prefix, /)
548+
///
549+
///
550+
/// Return a str with the given suffix string removed if present.
551+
///
552+
/// If the string ends with the suffix string, return string[:len(suffix)]
553+
/// Otherwise, return a copy of the original string.
554+
#[pymethod]
555+
fn removesuffix(&self, suff: PyStringRef) -> String {
556+
self.value
557+
.as_str()
558+
.py_removesuffix(&suff.value, suff.value.len(), |s, p| s.ends_with(p))
559+
.to_string()
560+
}
561+
532562
#[pymethod]
533563
fn isalnum(&self) -> bool {
534564
!self.value.is_empty() && self.value.chars().all(char::is_alphanumeric)

vm/src/obj/pystr.rs

+33
Original file line numberDiff line numberDiff line change
@@ -264,4 +264,37 @@ pub trait PyCommonString<E> {
264264
fn py_rjust(&self, width: usize, fillchar: E) -> Self::Container {
265265
self.py_pad(width - self.chars_len(), 0, fillchar)
266266
}
267+
268+
fn py_removeprefix<FC>(
269+
&self,
270+
prefix: &Self::Container,
271+
prefix_len: usize,
272+
is_prefix: FC,
273+
) -> &Self
274+
where
275+
FC: Fn(&Self, &Self::Container) -> bool,
276+
{
277+
//if self.py_starts_with(prefix) {
278+
if is_prefix(&self, &prefix) {
279+
self.get_bytes(prefix_len..self.bytes_len())
280+
} else {
281+
&self
282+
}
283+
}
284+
285+
fn py_removesuffix<FC>(
286+
&self,
287+
suffix: &Self::Container,
288+
suffix_len: usize,
289+
is_suffix: FC,
290+
) -> &Self
291+
where
292+
FC: Fn(&Self, &Self::Container) -> bool,
293+
{
294+
if is_suffix(&self, &suffix) {
295+
self.get_bytes(0..self.bytes_len() - suffix_len)
296+
} else {
297+
&self
298+
}
299+
}
267300
}

0 commit comments

Comments
 (0)