Skip to content

Commit e1416f5

Browse files
Merge pull request RustPython#967 from skinny121/str_xwith_tuple_arg
Accept tuple for first arg in str.startswith and str.endswith
2 parents 5273bdc + 90711f1 commit e1416f5

File tree

4 files changed

+89
-36
lines changed

4 files changed

+89
-36
lines changed

tests/snippets/strings.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,15 @@
4242
assert a.upper() == 'HALLO'
4343
assert a.split('al') == ['H', 'lo']
4444
assert a.startswith('H')
45+
assert a.startswith(('H', 1))
46+
assert a.startswith(('A', 'H'))
4547
assert not a.startswith('f')
48+
assert not a.startswith(('A', 'f'))
4649
assert a.endswith('llo')
50+
assert a.endswith(('lo', 1))
51+
assert a.endswith(('A', 'lo'))
4752
assert not a.endswith('on')
53+
assert not a.endswith(('A', 'll'))
4854
assert a.zfill(8) == '000Hallo'
4955
assert a.isalnum()
5056
assert not a.isdigit()
@@ -144,6 +150,7 @@
144150
assert '___a__'.find('a', 4, 3) == -1
145151

146152
assert 'abcd'.startswith('b', 1)
153+
assert 'abcd'.startswith(('b', 'z'), 1)
147154
assert not 'abcd'.startswith('b', -4)
148155
assert 'abcd'.startswith('b', -3)
149156

vm/src/builtins.rs

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@ use crate::obj::objdict::PyDictRef;
1717
use crate::obj::objint::{self, PyIntRef};
1818
use crate::obj::objiter;
1919
use crate::obj::objstr::{self, PyString, PyStringRef};
20-
use crate::obj::objtuple::PyTuple;
21-
use crate::obj::objtype::{self, PyClass, PyClassRef};
20+
use crate::obj::objtype::{self, PyClassRef};
2221

2322
use crate::frame::Scope;
24-
use crate::function::{Args, KwArgs, OptionalArg, PyFuncArgs};
23+
use crate::function::{single_or_tuple_any, Args, KwArgs, OptionalArg, PyFuncArgs};
2524
use crate::pyobject::{
2625
IdProtocol, IntoPyObject, ItemProtocol, PyIterable, PyObjectRef, PyResult, PyValue,
2726
TryFromObject, TypeProtocol,
@@ -331,37 +330,36 @@ fn builtin_id(vm: &VirtualMachine, args: PyFuncArgs) -> PyResult {
331330

332331
// builtin_input
333332

334-
fn type_test(
335-
vm: &VirtualMachine,
336-
typ: PyObjectRef,
337-
test: impl Fn(&PyClassRef) -> PyResult<bool>,
338-
test_name: &str,
339-
) -> PyResult<bool> {
340-
match_class!(typ,
341-
cls @ PyClass => test(&cls),
342-
tuple @ PyTuple => {
343-
for cls_obj in tuple.elements.borrow().iter() {
344-
let cls = PyClassRef::try_from_object(vm, cls_obj.clone())?;
345-
if test(&cls)? {
346-
return Ok(true);
347-
}
348-
}
349-
Ok(false)
333+
fn builtin_isinstance(obj: PyObjectRef, typ: PyObjectRef, vm: &VirtualMachine) -> PyResult<bool> {
334+
single_or_tuple_any(
335+
typ,
336+
|cls: PyClassRef| vm.isinstance(&obj, &cls),
337+
|o| {
338+
format!(
339+
"isinstance() arg 2 must be a type or tuple of types, not {}",
340+
o.class()
341+
)
350342
},
351-
_ => Err(vm.new_type_error(format!("{}() arg 2 must be a type or tuple of types", test_name)))
343+
vm,
352344
)
353345
}
354346

355-
fn builtin_isinstance(obj: PyObjectRef, typ: PyObjectRef, vm: &VirtualMachine) -> PyResult<bool> {
356-
type_test(vm, typ, |cls| vm.isinstance(&obj, cls), "isinstance")
357-
}
358-
359347
fn builtin_issubclass(
360348
subclass: PyClassRef,
361349
typ: PyObjectRef,
362350
vm: &VirtualMachine,
363351
) -> PyResult<bool> {
364-
type_test(vm, typ, |cls| vm.issubclass(&subclass, cls), "issubclass")
352+
single_or_tuple_any(
353+
typ,
354+
|cls: PyClassRef| vm.issubclass(&subclass, &cls),
355+
|o| {
356+
format!(
357+
"issubclass() arg 2 must be a class or tuple of classes, not {}",
358+
o.class()
359+
)
360+
},
361+
vm,
362+
)
365363
}
366364

367365
fn builtin_iter(vm: &VirtualMachine, args: PyFuncArgs) -> PyResult {

vm/src/function.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::collections::HashMap;
22
use std::mem;
33
use std::ops::RangeInclusive;
44

5+
use crate::obj::objtuple::PyTuple;
56
use crate::obj::objtype::{isinstance, PyClassRef};
67
use crate::pyobject::{
78
IntoPyObject, PyObjectRef, PyRef, PyResult, PyValue, TryFromObject, TypeProtocol,
@@ -523,3 +524,28 @@ into_py_native_func_tuple!((a, A), (b, B));
523524
into_py_native_func_tuple!((a, A), (b, B), (c, C));
524525
into_py_native_func_tuple!((a, A), (b, B), (c, C), (d, D));
525526
into_py_native_func_tuple!((a, A), (b, B), (c, C), (d, D), (e, E));
527+
528+
/// Tests that the predicate is True on a single value, or if the value is a tuple a tuple, then
529+
/// test that any of the values contained within the tuples satisfies the predicate. Type parameter
530+
/// T specifies the type that is expected, if the input value is not of that type or a tuple of
531+
/// values of that type, then a TypeError is raised.
532+
pub fn single_or_tuple_any<T: PyValue, F: Fn(PyRef<T>) -> PyResult<bool>>(
533+
obj: PyObjectRef,
534+
predicate: F,
535+
message: fn(&PyObjectRef) -> String,
536+
vm: &VirtualMachine,
537+
) -> PyResult<bool> {
538+
match_class!(obj,
539+
obj @ T => predicate(obj),
540+
tuple @ PyTuple => {
541+
for obj in tuple.elements.borrow().iter() {
542+
let inner_val = PyRef::<T>::try_from_object(vm, obj.clone())?;
543+
if predicate(inner_val)? {
544+
return Ok(true);
545+
}
546+
}
547+
Ok(false)
548+
},
549+
obj => Err(vm.new_type_error(message(&obj)))
550+
)
551+
}

vm/src/obj/objstr.rs

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use unicode_segmentation::UnicodeSegmentation;
1212
use unicode_xid::UnicodeXID;
1313

1414
use crate::format::{FormatParseError, FormatPart, FormatString};
15-
use crate::function::{OptionalArg, PyFuncArgs};
15+
use crate::function::{single_or_tuple_any, OptionalArg, PyFuncArgs};
1616
use crate::pyobject::{
1717
IdProtocol, IntoPyObject, ItemProtocol, PyClassImpl, PyContext, PyIterable, PyObjectRef, PyRef,
1818
PyResult, PyValue, TryFromObject, TryIntoRef, TypeProtocol,
@@ -344,30 +344,52 @@ impl PyString {
344344
#[pymethod]
345345
fn endswith(
346346
&self,
347-
suffix: PyStringRef,
347+
suffix: PyObjectRef,
348348
start: OptionalArg<isize>,
349349
end: OptionalArg<isize>,
350-
_vm: &VirtualMachine,
351-
) -> bool {
350+
vm: &VirtualMachine,
351+
) -> PyResult<bool> {
352352
if let Some((start, end)) = adjust_indices(start, end, self.value.len()) {
353-
self.value[start..end].ends_with(&suffix.value)
353+
let value = &self.value[start..end];
354+
single_or_tuple_any(
355+
suffix,
356+
|s: PyStringRef| Ok(value.ends_with(&s.value)),
357+
|o| {
358+
format!(
359+
"endswith first arg must be str or a tuple of str, not {}",
360+
o.class(),
361+
)
362+
},
363+
vm,
364+
)
354365
} else {
355-
false
366+
Ok(false)
356367
}
357368
}
358369

359370
#[pymethod]
360371
fn startswith(
361372
&self,
362-
prefix: PyStringRef,
373+
prefix: PyObjectRef,
363374
start: OptionalArg<isize>,
364375
end: OptionalArg<isize>,
365-
_vm: &VirtualMachine,
366-
) -> bool {
376+
vm: &VirtualMachine,
377+
) -> PyResult<bool> {
367378
if let Some((start, end)) = adjust_indices(start, end, self.value.len()) {
368-
self.value[start..end].starts_with(&prefix.value)
379+
let value = &self.value[start..end];
380+
single_or_tuple_any(
381+
prefix,
382+
|s: PyStringRef| Ok(value.starts_with(&s.value)),
383+
|o| {
384+
format!(
385+
"startswith first arg must be str or a tuple of str, not {}",
386+
o.class(),
387+
)
388+
},
389+
vm,
390+
)
369391
} else {
370-
false
392+
Ok(false)
371393
}
372394
}
373395

0 commit comments

Comments
 (0)