mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
1709 lines
52 KiB
Rust
1709 lines
52 KiB
Rust
use super::{
|
|
int::{PyInt, PyIntRef},
|
|
iter::IterStatus::{self, Exhausted},
|
|
PositionIterInternal, PyBytesRef, PyDict, PyTupleRef, PyTypeRef,
|
|
};
|
|
use crate::{
|
|
anystr::{self, adjust_indices, AnyStr, AnyStrContainer, AnyStrWrapper},
|
|
class::PyClassImpl,
|
|
convert::{ToPyException, ToPyObject},
|
|
format::{FormatSpec, FormatString, FromTemplate},
|
|
function::{ArgIterable, FuncArgs, OptionalArg, OptionalOption, PyComparisonValue},
|
|
protocol::{PyIterReturn, PyMappingMethods, PySequenceMethods},
|
|
sequence::SequenceOp,
|
|
sliceable::{SequenceIndex, SliceableSequenceOp},
|
|
types::{
|
|
AsMapping, AsSequence, Comparable, Constructor, Hashable, IterNext, IterNextIterable,
|
|
Iterable, PyComparisonOp, Unconstructible,
|
|
},
|
|
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult,
|
|
TryFromBorrowedObject, VirtualMachine,
|
|
};
|
|
use ascii::{AsciiStr, AsciiString};
|
|
use bstr::ByteSlice;
|
|
use itertools::Itertools;
|
|
use num_traits::ToPrimitive;
|
|
use rustpython_common::{
|
|
ascii,
|
|
atomic::{self, PyAtomic, Radium},
|
|
hash,
|
|
lock::PyMutex,
|
|
};
|
|
use std::{char, fmt, ops::Range, string::ToString};
|
|
use unic_ucd_bidi::BidiClass;
|
|
use unic_ucd_category::GeneralCategory;
|
|
use unic_ucd_ident::{is_xid_continue, is_xid_start};
|
|
use unicode_casing::CharExt;
|
|
|
|
/// Utf8 + state.ascii (+ PyUnicode_Kind in future)
|
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
|
pub(crate) enum PyStrKind {
|
|
Ascii,
|
|
Utf8,
|
|
}
|
|
|
|
impl std::ops::BitOr for PyStrKind {
|
|
type Output = Self;
|
|
fn bitor(self, other: Self) -> Self {
|
|
match (self, other) {
|
|
(Self::Ascii, Self::Ascii) => Self::Ascii,
|
|
_ => Self::Utf8,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PyStrKind {
|
|
fn new_data(self) -> PyStrKindData {
|
|
match self {
|
|
PyStrKind::Ascii => PyStrKindData::Ascii,
|
|
PyStrKind::Utf8 => PyStrKindData::Utf8(Radium::new(usize::MAX)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum PyStrKindData {
|
|
Ascii,
|
|
// uses usize::MAX as a sentinel for "uncomputed"
|
|
Utf8(PyAtomic<usize>),
|
|
}
|
|
|
|
impl PyStrKindData {
|
|
fn kind(&self) -> PyStrKind {
|
|
match self {
|
|
PyStrKindData::Ascii => PyStrKind::Ascii,
|
|
PyStrKindData::Utf8(_) => PyStrKind::Utf8,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFromBorrowedObject for String {
|
|
fn try_from_borrowed_object(vm: &VirtualMachine, obj: &PyObject) -> PyResult<Self> {
|
|
obj.try_value_with(|pystr: &PyStr| Ok(pystr.as_str().to_owned()), vm)
|
|
}
|
|
}
|
|
|
|
/// str(object='') -> str
|
|
/// str(bytes_or_buffer[, encoding[, errors]]) -> str
|
|
///
|
|
/// Create a new string object from the given object. If encoding or
|
|
/// errors is specified, then the object must expose a data buffer
|
|
/// that will be decoded using the given encoding and error handler.
|
|
/// Otherwise, returns the result of object.__str__() (if defined)
|
|
/// or repr(object).
|
|
/// encoding defaults to sys.getdefaultencoding().
|
|
/// errors defaults to 'strict'."
|
|
#[pyclass(module = false, name = "str")]
|
|
pub struct PyStr {
|
|
bytes: Box<[u8]>,
|
|
kind: PyStrKindData,
|
|
hash: PyAtomic<hash::PyHash>,
|
|
}
|
|
|
|
impl fmt::Debug for PyStr {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
f.debug_struct("PyStr")
|
|
.field("value", &self.as_str())
|
|
.field("kind", &self.kind)
|
|
.field("hash", &self.hash)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl AsRef<str> for PyStr {
|
|
fn as_ref(&self) -> &str {
|
|
self.as_str()
|
|
}
|
|
}
|
|
|
|
impl AsRef<str> for PyStrRef {
|
|
fn as_ref(&self) -> &str {
|
|
self.as_str()
|
|
}
|
|
}
|
|
|
|
impl<'a> From<&'a AsciiStr> for PyStr {
|
|
fn from(s: &'a AsciiStr) -> Self {
|
|
s.to_owned().into()
|
|
}
|
|
}
|
|
|
|
impl From<AsciiString> for PyStr {
|
|
fn from(s: AsciiString) -> Self {
|
|
unsafe { Self::new_ascii_unchecked(s.into()) }
|
|
}
|
|
}
|
|
|
|
impl<'a> From<&'a str> for PyStr {
|
|
fn from(s: &'a str) -> Self {
|
|
s.to_owned().into()
|
|
}
|
|
}
|
|
|
|
impl From<String> for PyStr {
|
|
fn from(s: String) -> Self {
|
|
s.into_boxed_str().into()
|
|
}
|
|
}
|
|
|
|
impl<'a> From<std::borrow::Cow<'a, str>> for PyStr {
|
|
fn from(s: std::borrow::Cow<'a, str>) -> Self {
|
|
s.into_owned().into()
|
|
}
|
|
}
|
|
|
|
impl From<Box<str>> for PyStr {
|
|
#[inline]
|
|
fn from(value: Box<str>) -> Self {
|
|
// doing the check is ~10x faster for ascii, and is actually only 2% slower worst case for
|
|
// non-ascii; see https://github.com/RustPython/RustPython/pull/2586#issuecomment-844611532
|
|
let is_ascii = value.is_ascii();
|
|
let bytes = value.into_boxed_bytes();
|
|
let kind = if is_ascii {
|
|
PyStrKind::Ascii
|
|
} else {
|
|
PyStrKind::Utf8
|
|
}
|
|
.new_data();
|
|
Self {
|
|
bytes,
|
|
kind,
|
|
hash: Radium::new(hash::SENTINEL),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub type PyStrRef = PyRef<PyStr>;
|
|
|
|
impl fmt::Display for PyStr {
|
|
#[inline]
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
fmt::Display::fmt(self.as_str(), f)
|
|
}
|
|
}
|
|
|
|
pub trait IntoPyStrRef {
|
|
fn into_pystr_ref(self, vm: &VirtualMachine) -> PyStrRef;
|
|
}
|
|
|
|
impl IntoPyStrRef for PyStrRef {
|
|
#[inline]
|
|
fn into_pystr_ref(self, _vm: &VirtualMachine) -> PyRef<PyStr> {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl IntoPyStrRef for PyStr {
|
|
#[inline]
|
|
fn into_pystr_ref(self, vm: &VirtualMachine) -> PyRef<PyStr> {
|
|
self.into_ref(vm)
|
|
}
|
|
}
|
|
|
|
impl IntoPyStrRef for AsciiString {
|
|
#[inline]
|
|
fn into_pystr_ref(self, vm: &VirtualMachine) -> PyRef<PyStr> {
|
|
PyStr::from(self).into_ref(vm)
|
|
}
|
|
}
|
|
|
|
impl IntoPyStrRef for String {
|
|
#[inline]
|
|
fn into_pystr_ref(self, vm: &VirtualMachine) -> PyRef<PyStr> {
|
|
PyStr::from(self).into_ref(vm)
|
|
}
|
|
}
|
|
|
|
impl IntoPyStrRef for &str {
|
|
#[inline]
|
|
fn into_pystr_ref(self, vm: &VirtualMachine) -> PyRef<PyStr> {
|
|
PyStr::from(self).into_ref(vm)
|
|
}
|
|
}
|
|
|
|
#[pyclass(module = false, name = "str_iterator")]
|
|
#[derive(Debug)]
|
|
pub struct PyStrIterator {
|
|
internal: PyMutex<(PositionIterInternal<PyStrRef>, usize)>,
|
|
}
|
|
|
|
impl PyPayload for PyStrIterator {
|
|
fn class(vm: &VirtualMachine) -> &PyTypeRef {
|
|
&vm.ctx.types.str_iterator_type
|
|
}
|
|
}
|
|
|
|
#[pyimpl(with(Constructor, IterNext))]
|
|
impl PyStrIterator {
|
|
#[pymethod(magic)]
|
|
fn length_hint(&self) -> usize {
|
|
self.internal.lock().0.length_hint(|obj| obj.char_len())
|
|
}
|
|
|
|
#[pymethod(magic)]
|
|
fn setstate(&self, state: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
|
|
let mut internal = self.internal.lock();
|
|
internal.1 = usize::MAX;
|
|
internal
|
|
.0
|
|
.set_state(state, |obj, pos| pos.min(obj.char_len()), vm)
|
|
}
|
|
|
|
#[pymethod(magic)]
|
|
fn reduce(&self, vm: &VirtualMachine) -> PyTupleRef {
|
|
self.internal
|
|
.lock()
|
|
.0
|
|
.builtins_iter_reduce(|x| x.clone().into(), vm)
|
|
}
|
|
}
|
|
impl Unconstructible for PyStrIterator {}
|
|
|
|
impl IterNextIterable for PyStrIterator {}
|
|
impl IterNext for PyStrIterator {
|
|
fn next(zelf: &crate::Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
|
|
let mut internal = zelf.internal.lock();
|
|
|
|
if let IterStatus::Active(s) = &internal.0.status {
|
|
let value = s.as_str();
|
|
|
|
if internal.1 == usize::MAX {
|
|
if let Some((offset, ch)) = value.char_indices().nth(internal.0.position) {
|
|
internal.0.position += 1;
|
|
internal.1 = offset + ch.len_utf8();
|
|
return Ok(PyIterReturn::Return(ch.to_pyobject(vm)));
|
|
}
|
|
} else if let Some(value) = value.get(internal.1..) {
|
|
if let Some(ch) = value.chars().next() {
|
|
internal.0.position += 1;
|
|
internal.1 += ch.len_utf8();
|
|
return Ok(PyIterReturn::Return(ch.to_pyobject(vm)));
|
|
}
|
|
}
|
|
internal.0.status = Exhausted;
|
|
}
|
|
Ok(PyIterReturn::StopIteration(None))
|
|
}
|
|
}
|
|
|
|
#[derive(FromArgs)]
|
|
pub struct StrArgs {
|
|
#[pyarg(any, optional)]
|
|
object: OptionalArg<PyObjectRef>,
|
|
#[pyarg(any, optional)]
|
|
encoding: OptionalArg<PyStrRef>,
|
|
#[pyarg(any, optional)]
|
|
errors: OptionalArg<PyStrRef>,
|
|
}
|
|
|
|
impl Constructor for PyStr {
|
|
type Args = StrArgs;
|
|
|
|
fn py_new(cls: PyTypeRef, args: Self::Args, vm: &VirtualMachine) -> PyResult {
|
|
let string: PyStrRef = match args.object {
|
|
OptionalArg::Present(input) => {
|
|
if let OptionalArg::Present(enc) = args.encoding {
|
|
vm.state.codec_registry.decode_text(
|
|
input,
|
|
enc.as_str(),
|
|
args.errors.into_option(),
|
|
vm,
|
|
)?
|
|
} else {
|
|
input.str(vm)?
|
|
}
|
|
}
|
|
OptionalArg::Missing => {
|
|
PyStr::from(String::new()).into_ref_with_type(vm, cls.clone())?
|
|
}
|
|
};
|
|
if string.class().is(&cls) {
|
|
Ok(string.into())
|
|
} else {
|
|
PyStr::from(string.as_str())
|
|
.into_ref_with_type(vm, cls)
|
|
.map(Into::into)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PyStr {
|
|
/// # Safety: Given `bytes` must be valid data for given `kind`
|
|
pub(crate) unsafe fn new_str_unchecked(bytes: Vec<u8>, kind: PyStrKind) -> Self {
|
|
let s = Self {
|
|
bytes: bytes.into_boxed_slice(),
|
|
kind: kind.new_data(),
|
|
hash: Radium::new(hash::SENTINEL),
|
|
};
|
|
debug_assert!(matches!(s.kind, PyStrKindData::Ascii) || !s.as_str().is_ascii());
|
|
s
|
|
}
|
|
|
|
/// # Safety
|
|
/// Given `bytes` must be ascii
|
|
pub unsafe fn new_ascii_unchecked(bytes: Vec<u8>) -> Self {
|
|
Self::new_str_unchecked(bytes, PyStrKind::Ascii)
|
|
}
|
|
|
|
pub fn new_ref(s: impl Into<Self>, ctx: &Context) -> PyRef<Self> {
|
|
PyRef::new_ref(s.into(), ctx.types.str_type.clone(), None)
|
|
}
|
|
|
|
fn new_substr(&self, s: String) -> Self {
|
|
let kind = if self.kind.kind() == PyStrKind::Ascii || s.is_ascii() {
|
|
PyStrKind::Ascii
|
|
} else {
|
|
PyStrKind::Utf8
|
|
};
|
|
unsafe {
|
|
// SAFETY: kind is properly decided for substring
|
|
Self::new_str_unchecked(s.into_bytes(), kind)
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn as_str(&self) -> &str {
|
|
unsafe {
|
|
// SAFETY: Both PyStrKind::{Ascii, Utf8} are valid utf8 string
|
|
std::str::from_utf8_unchecked(&self.bytes)
|
|
}
|
|
}
|
|
|
|
fn char_all<F>(&self, test: F) -> bool
|
|
where
|
|
F: Fn(char) -> bool,
|
|
{
|
|
match self.kind.kind() {
|
|
PyStrKind::Ascii => self.bytes.iter().all(|&x| test(char::from(x))),
|
|
PyStrKind::Utf8 => self.as_str().chars().all(test),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[pyimpl(
|
|
flags(BASETYPE),
|
|
with(AsMapping, AsSequence, Hashable, Comparable, Iterable, Constructor)
|
|
)]
|
|
impl PyStr {
|
|
#[pymethod(magic)]
|
|
fn add(zelf: PyRef<Self>, other: PyObjectRef, vm: &VirtualMachine) -> PyResult {
|
|
if let Some(other) = other.payload::<PyStr>() {
|
|
let bytes = zelf.as_str().py_add(other.as_ref());
|
|
Ok(unsafe {
|
|
// SAFETY: `kind` is safely decided
|
|
let kind = zelf.kind.kind() | other.kind.kind();
|
|
Self::new_str_unchecked(bytes.into_bytes(), kind)
|
|
}
|
|
.to_pyobject(vm))
|
|
} else if let Some(radd) = vm.get_method(other.clone(), "__radd__") {
|
|
// hack to get around not distinguishing number add from seq concat
|
|
vm.invoke(&radd?, (zelf,))
|
|
} else {
|
|
Err(vm.new_type_error(format!(
|
|
"can only concatenate str (not \"{}\") to str",
|
|
other.class().name()
|
|
)))
|
|
}
|
|
}
|
|
|
|
#[pymethod(magic)]
|
|
fn bool(&self) -> bool {
|
|
!self.bytes.is_empty()
|
|
}
|
|
|
|
fn _contains(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult<bool> {
|
|
if let Some(needle) = needle.payload::<Self>() {
|
|
Ok(self.as_str().contains(needle.as_str()))
|
|
} else {
|
|
Err(vm.new_type_error(format!(
|
|
"'in <string>' requires string as left operand, not {}",
|
|
needle.class().name()
|
|
)))
|
|
}
|
|
}
|
|
|
|
#[pymethod(magic)]
|
|
fn contains(&self, needle: PyObjectRef, vm: &VirtualMachine) -> PyResult<bool> {
|
|
self._contains(&needle, vm)
|
|
}
|
|
|
|
fn _getitem(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult {
|
|
match SequenceIndex::try_from_borrowed_object(vm, needle)? {
|
|
SequenceIndex::Int(i) => self.get_item_by_index(vm, i).map(|x| x.to_string()),
|
|
SequenceIndex::Slice(slice) => self.get_item_by_slice(vm, slice),
|
|
}
|
|
.map(|x| self.new_substr(x).into_ref(vm).into())
|
|
}
|
|
|
|
#[pymethod(magic)]
|
|
fn getitem(&self, needle: PyObjectRef, vm: &VirtualMachine) -> PyResult {
|
|
self._getitem(&needle, vm)
|
|
}
|
|
|
|
#[inline]
|
|
pub(crate) fn hash(&self, vm: &VirtualMachine) -> hash::PyHash {
|
|
match self.hash.load(atomic::Ordering::Relaxed) {
|
|
hash::SENTINEL => self._compute_hash(vm),
|
|
hash => hash,
|
|
}
|
|
}
|
|
#[cold]
|
|
fn _compute_hash(&self, vm: &VirtualMachine) -> hash::PyHash {
|
|
let hash_val = vm.state.hash_secret.hash_str(self.as_str());
|
|
debug_assert_ne!(hash_val, hash::SENTINEL);
|
|
// like with char_len, we don't need a cmpxchg loop, since it'll always be the same value
|
|
self.hash.store(hash_val, atomic::Ordering::Relaxed);
|
|
hash_val
|
|
}
|
|
|
|
#[inline]
|
|
pub fn byte_len(&self) -> usize {
|
|
self.bytes.len()
|
|
}
|
|
#[inline]
|
|
pub fn is_empty(&self) -> bool {
|
|
self.bytes.is_empty()
|
|
}
|
|
|
|
#[pymethod(name = "__len__")]
|
|
#[inline]
|
|
pub fn char_len(&self) -> usize {
|
|
match self.kind {
|
|
PyStrKindData::Ascii => self.bytes.len(),
|
|
PyStrKindData::Utf8(ref len) => match len.load(atomic::Ordering::Relaxed) {
|
|
usize::MAX => self._compute_char_len(),
|
|
len => len,
|
|
},
|
|
}
|
|
}
|
|
#[cold]
|
|
fn _compute_char_len(&self) -> usize {
|
|
match self.kind {
|
|
PyStrKindData::Utf8(ref char_len) => {
|
|
let len = self.as_str().chars().count();
|
|
// len cannot be usize::MAX, since vec.capacity() < sys.maxsize
|
|
char_len.store(len, atomic::Ordering::Relaxed);
|
|
len
|
|
}
|
|
_ => unsafe {
|
|
debug_assert!(false); // invalid for non-utf8 strings
|
|
std::hint::unreachable_unchecked()
|
|
},
|
|
}
|
|
}
|
|
|
|
#[pymethod(name = "isascii")]
|
|
#[inline(always)]
|
|
pub fn is_ascii(&self) -> bool {
|
|
match self.kind {
|
|
PyStrKindData::Ascii => true,
|
|
PyStrKindData::Utf8(_) => false,
|
|
}
|
|
}
|
|
|
|
#[pymethod(magic)]
|
|
fn sizeof(&self) -> usize {
|
|
std::mem::size_of::<Self>() + self.byte_len() * std::mem::size_of::<u8>()
|
|
}
|
|
|
|
#[pymethod(name = "__rmul__")]
|
|
#[pymethod(magic)]
|
|
fn mul(zelf: PyRef<Self>, value: isize, vm: &VirtualMachine) -> PyResult<PyRef<Self>> {
|
|
if value == 1 && zelf.class().is(&vm.ctx.types.str_type) {
|
|
// Special case: when some `str` is multiplied by `1`,
|
|
// nothing really happens, we need to return an object itself
|
|
// with the same `id()` to be compatible with CPython.
|
|
// This only works for `str` itself, not its subclasses.
|
|
return Ok(zelf);
|
|
}
|
|
zelf.as_str()
|
|
.as_bytes()
|
|
.mul(vm, value)
|
|
.map(|x| Self::from(unsafe { String::from_utf8_unchecked(x) }).into_ref(vm))
|
|
}
|
|
|
|
#[pymethod(magic)]
|
|
fn str(zelf: PyRef<Self>) -> PyStrRef {
|
|
zelf
|
|
}
|
|
|
|
#[pymethod(magic)]
|
|
pub(crate) fn repr(&self, vm: &VirtualMachine) -> PyResult<String> {
|
|
rustpython_common::str::repr(self.as_str())
|
|
.to_string_checked()
|
|
.map_err(|err| vm.new_overflow_error(err.to_string()))
|
|
}
|
|
|
|
#[pymethod]
|
|
fn lower(&self) -> String {
|
|
match self.kind.kind() {
|
|
PyStrKind::Ascii => self.as_str().to_ascii_lowercase(),
|
|
PyStrKind::Utf8 => self.as_str().to_lowercase(),
|
|
}
|
|
}
|
|
|
|
// casefold is much more aggressive than lower
|
|
#[pymethod]
|
|
fn casefold(&self) -> String {
|
|
caseless::default_case_fold_str(self.as_str())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn upper(&self) -> String {
|
|
match self.kind.kind() {
|
|
PyStrKind::Ascii => self.as_str().to_ascii_uppercase(),
|
|
PyStrKind::Utf8 => self.as_str().to_uppercase(),
|
|
}
|
|
}
|
|
|
|
#[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()
|
|
}
|
|
}
|
|
|
|
#[pymethod]
|
|
fn split(&self, args: SplitArgs, vm: &VirtualMachine) -> PyResult<Vec<PyObjectRef>> {
|
|
let elements = match self.kind.kind() {
|
|
PyStrKind::Ascii => self.as_str().py_split(
|
|
args,
|
|
vm,
|
|
|v, s, vm| {
|
|
v.as_bytes()
|
|
.split_str(s)
|
|
.map(|s| {
|
|
unsafe { PyStr::new_ascii_unchecked(s.to_owned()) }.to_pyobject(vm)
|
|
})
|
|
.collect()
|
|
},
|
|
|v, s, n, vm| {
|
|
v.as_bytes()
|
|
.splitn_str(n, s)
|
|
.map(|s| {
|
|
unsafe { PyStr::new_ascii_unchecked(s.to_owned()) }.to_pyobject(vm)
|
|
})
|
|
.collect()
|
|
},
|
|
|v, n, vm| {
|
|
v.as_bytes().py_split_whitespace(n, |s| {
|
|
unsafe { PyStr::new_ascii_unchecked(s.to_owned()) }.to_pyobject(vm)
|
|
})
|
|
},
|
|
),
|
|
PyStrKind::Utf8 => self.as_str().py_split(
|
|
args,
|
|
vm,
|
|
|v, s, vm| v.split(s).map(|s| vm.ctx.new_str(s).into()).collect(),
|
|
|v, s, n, vm| v.splitn(n, s).map(|s| vm.ctx.new_str(s).into()).collect(),
|
|
|v, n, vm| v.py_split_whitespace(n, |s| vm.ctx.new_str(s).into()),
|
|
),
|
|
}?;
|
|
Ok(elements)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn rsplit(&self, args: SplitArgs, vm: &VirtualMachine) -> PyResult<Vec<PyObjectRef>> {
|
|
let mut elements = self.as_str().py_split(
|
|
args,
|
|
vm,
|
|
|v, s, vm| v.rsplit(s).map(|s| vm.ctx.new_str(s).into()).collect(),
|
|
|v, s, n, vm| v.rsplitn(n, s).map(|s| vm.ctx.new_str(s).into()).collect(),
|
|
|v, n, vm| v.py_rsplit_whitespace(n, |s| vm.ctx.new_str(s).into()),
|
|
)?;
|
|
// Unlike Python rsplit, Rust rsplitn returns an iterator that
|
|
// starts from the end of the string.
|
|
elements.reverse();
|
|
Ok(elements)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn strip(&self, chars: OptionalOption<PyStrRef>) -> String {
|
|
self.as_str()
|
|
.py_strip(
|
|
chars,
|
|
|s, chars| s.trim_matches(|c| chars.contains(c)),
|
|
|s| s.trim(),
|
|
)
|
|
.to_owned()
|
|
}
|
|
|
|
#[pymethod]
|
|
fn lstrip(&self, chars: OptionalOption<PyStrRef>) -> String {
|
|
self.as_str()
|
|
.py_strip(
|
|
chars,
|
|
|s, chars| s.trim_start_matches(|c| chars.contains(c)),
|
|
|s| s.trim_start(),
|
|
)
|
|
.to_owned()
|
|
}
|
|
|
|
#[pymethod]
|
|
fn rstrip(&self, chars: OptionalOption<PyStrRef>) -> String {
|
|
self.as_str()
|
|
.py_strip(
|
|
chars,
|
|
|s, chars| s.trim_end_matches(|c| chars.contains(c)),
|
|
|s| s.trim_end(),
|
|
)
|
|
.to_owned()
|
|
}
|
|
|
|
#[pymethod]
|
|
fn endswith(&self, options: anystr::StartsEndsWithArgs, vm: &VirtualMachine) -> PyResult<bool> {
|
|
let (affix, substr) =
|
|
match options.prepare(self.as_str(), self.len(), |s, r| s.get_chars(r)) {
|
|
Some(x) => x,
|
|
None => return Ok(false),
|
|
};
|
|
substr.py_startsendswith(
|
|
affix,
|
|
"endswith",
|
|
"str",
|
|
|s, x: &PyStrRef| s.ends_with(x.as_str()),
|
|
vm,
|
|
)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn startswith(
|
|
&self,
|
|
options: anystr::StartsEndsWithArgs,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<bool> {
|
|
let (affix, substr) =
|
|
match options.prepare(self.as_str(), self.len(), |s, r| s.get_chars(r)) {
|
|
Some(x) => x,
|
|
None => return Ok(false),
|
|
};
|
|
substr.py_startsendswith(
|
|
affix,
|
|
"startswith",
|
|
"str",
|
|
|s, x: &PyStrRef| s.starts_with(x.as_str()),
|
|
vm,
|
|
)
|
|
}
|
|
|
|
/// Return a str with the given prefix string removed if present.
|
|
///
|
|
/// If the string starts with the prefix string, return string[len(prefix):]
|
|
/// Otherwise, return a copy of the original string.
|
|
#[pymethod]
|
|
fn removeprefix(&self, pref: PyStrRef) -> String {
|
|
self.as_str()
|
|
.py_removeprefix(pref.as_str(), pref.byte_len(), |s, p| s.starts_with(p))
|
|
.to_owned()
|
|
}
|
|
|
|
/// Return a str with the given suffix string removed if present.
|
|
///
|
|
/// If the string ends with the suffix string, return string[:len(suffix)]
|
|
/// Otherwise, return a copy of the original string.
|
|
#[pymethod]
|
|
fn removesuffix(&self, suff: PyStrRef) -> String {
|
|
self.as_str()
|
|
.py_removesuffix(suff.as_str(), suff.byte_len(), |s, p| s.ends_with(p))
|
|
.to_owned()
|
|
}
|
|
|
|
#[pymethod]
|
|
fn isalnum(&self) -> bool {
|
|
!self.bytes.is_empty() && self.char_all(char::is_alphanumeric)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn isnumeric(&self) -> bool {
|
|
!self.bytes.is_empty() && self.char_all(char::is_numeric)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn isdigit(&self) -> bool {
|
|
// python's isdigit also checks if exponents are digits, these are the unicodes for exponents
|
|
let valid_unicodes: [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_unicodes.contains(&(c as u16)))
|
|
}
|
|
|
|
#[pymethod]
|
|
fn isdecimal(&self) -> bool {
|
|
!self.bytes.is_empty()
|
|
&& self.char_all(|c| GeneralCategory::of(c) == GeneralCategory::DecimalNumber)
|
|
}
|
|
|
|
#[pymethod(name = "__mod__")]
|
|
fn modulo(&self, values: PyObjectRef, vm: &VirtualMachine) -> PyResult<String> {
|
|
let formatted = self.as_str().py_cformat(values, vm)?;
|
|
Ok(formatted)
|
|
}
|
|
|
|
#[pymethod(magic)]
|
|
fn rmod(&self, _values: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
|
|
vm.ctx.not_implemented()
|
|
}
|
|
|
|
#[pymethod]
|
|
fn format(&self, args: FuncArgs, vm: &VirtualMachine) -> PyResult<String> {
|
|
let format_string =
|
|
FormatString::from_str(self.as_str()).map_err(|e| e.to_pyexception(vm))?;
|
|
format_string.format(&args, vm)
|
|
}
|
|
|
|
/// S.format_map(mapping) -> str
|
|
///
|
|
/// Return a formatted version of S, using substitutions from mapping.
|
|
/// The substitutions are identified by braces ('{' and '}').
|
|
#[pymethod]
|
|
fn format_map(&self, mapping: PyObjectRef, vm: &VirtualMachine) -> PyResult<String> {
|
|
match FormatString::from_str(self.as_str()) {
|
|
Ok(format_string) => format_string.format_map(&mapping, vm),
|
|
Err(err) => Err(err.to_pyexception(vm)),
|
|
}
|
|
}
|
|
|
|
#[pymethod(name = "__format__")]
|
|
fn format_str(&self, spec: PyStrRef, vm: &VirtualMachine) -> PyResult<String> {
|
|
match FormatSpec::parse(spec.as_str())
|
|
.and_then(|format_spec| format_spec.format_string(self.as_str()))
|
|
{
|
|
Ok(string) => Ok(string),
|
|
Err(err) => Err(vm.new_value_error(err.to_string())),
|
|
}
|
|
}
|
|
|
|
/// 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.bytes.len());
|
|
let mut previous_is_cased = false;
|
|
for c in self.as_str().chars() {
|
|
if c.is_lowercase() {
|
|
if !previous_is_cased {
|
|
title.extend(c.to_titlecase());
|
|
} else {
|
|
title.push(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);
|
|
}
|
|
previous_is_cased = true;
|
|
} else {
|
|
previous_is_cased = false;
|
|
title.push(c);
|
|
}
|
|
}
|
|
title
|
|
}
|
|
|
|
#[pymethod]
|
|
fn swapcase(&self) -> String {
|
|
let mut swapped_str = String::with_capacity(self.bytes.len());
|
|
for c in self.as_str().chars() {
|
|
// to_uppercase returns an iterator, to_ascii_uppercase returns the char
|
|
if c.is_lowercase() {
|
|
swapped_str.push(c.to_ascii_uppercase());
|
|
} else if c.is_uppercase() {
|
|
swapped_str.push(c.to_ascii_lowercase());
|
|
} else {
|
|
swapped_str.push(c);
|
|
}
|
|
}
|
|
swapped_str
|
|
}
|
|
|
|
#[pymethod]
|
|
fn isalpha(&self) -> bool {
|
|
!self.bytes.is_empty() && self.char_all(char::is_alphabetic)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn replace(&self, old: PyStrRef, new: PyStrRef, count: OptionalArg<isize>) -> String {
|
|
let s = self.as_str();
|
|
match count {
|
|
OptionalArg::Present(maxcount) if maxcount >= 0 => {
|
|
if maxcount == 0 || s.is_empty() {
|
|
// nothing to do; return the original bytes
|
|
s.into()
|
|
} else {
|
|
s.replacen(old.as_str(), new.as_str(), maxcount as usize)
|
|
}
|
|
}
|
|
_ => s.replace(old.as_str(), new.as_str()),
|
|
}
|
|
}
|
|
|
|
/// Return true if all characters in the string are printable or the string is empty,
|
|
/// false otherwise. Nonprintable characters are those characters defined in the
|
|
/// Unicode character database as `Other` or `Separator`,
|
|
/// excepting the ASCII space (0x20) which is considered printable.
|
|
///
|
|
/// All characters except those characters defined in the Unicode character
|
|
/// database as following categories are considered printable.
|
|
/// * Cc (Other, Control)
|
|
/// * Cf (Other, Format)
|
|
/// * Cs (Other, Surrogate)
|
|
/// * Co (Other, Private Use)
|
|
/// * Cn (Other, Not Assigned)
|
|
/// * Zl Separator, Line ('\u2028', LINE SEPARATOR)
|
|
/// * Zp Separator, Paragraph ('\u2029', PARAGRAPH SEPARATOR)
|
|
/// * Zs (Separator, Space) other than ASCII space('\x20').
|
|
#[pymethod]
|
|
fn isprintable(&self) -> bool {
|
|
self.char_all(|c| c == '\u{0020}' || rustpython_common::char::is_printable(c))
|
|
}
|
|
|
|
#[pymethod]
|
|
fn isspace(&self) -> bool {
|
|
use unic_ucd_bidi::bidi_class::abbr_names::*;
|
|
!self.bytes.is_empty()
|
|
&& self.char_all(|c| {
|
|
GeneralCategory::of(c) == GeneralCategory::SpaceSeparator
|
|
|| matches!(BidiClass::of(c), WS | B | S)
|
|
})
|
|
}
|
|
|
|
// Return true if all cased characters in the string are lowercase and there is at least one cased character, false otherwise.
|
|
#[pymethod]
|
|
fn islower(&self) -> bool {
|
|
match self.kind.kind() {
|
|
PyStrKind::Ascii => self.bytes.py_iscase(char::is_lowercase, char::is_uppercase),
|
|
PyStrKind::Utf8 => self
|
|
.as_str()
|
|
.py_iscase(char::is_lowercase, char::is_uppercase),
|
|
}
|
|
}
|
|
|
|
// Return true if all cased characters in the string are uppercase and there is at least one cased character, false otherwise.
|
|
#[pymethod]
|
|
fn isupper(&self) -> bool {
|
|
match self.kind.kind() {
|
|
PyStrKind::Ascii => self.bytes.py_iscase(char::is_uppercase, char::is_lowercase),
|
|
PyStrKind::Utf8 => self
|
|
.as_str()
|
|
.py_iscase(char::is_uppercase, char::is_lowercase),
|
|
}
|
|
}
|
|
|
|
#[pymethod]
|
|
fn splitlines(&self, args: anystr::SplitLinesArgs, vm: &VirtualMachine) -> Vec<PyObjectRef> {
|
|
self.as_str()
|
|
.py_splitlines(args, |s| self.new_substr(s.to_owned()).to_pyobject(vm))
|
|
}
|
|
|
|
#[pymethod]
|
|
fn join(&self, iterable: ArgIterable<PyStrRef>, vm: &VirtualMachine) -> PyResult<String> {
|
|
let iter = iterable.iter(vm)?;
|
|
self.as_str().py_join(iter)
|
|
}
|
|
|
|
// FIXME: two traversals of str is expensive
|
|
#[inline]
|
|
fn _to_char_idx(r: &str, byte_idx: usize) -> usize {
|
|
r[..byte_idx].chars().count()
|
|
}
|
|
|
|
#[inline]
|
|
fn _find<F>(&self, args: FindArgs, find: F) -> Option<usize>
|
|
where
|
|
F: Fn(&str, &str) -> Option<usize>,
|
|
{
|
|
let (sub, range) = args.get_value(self.len());
|
|
self.as_str().py_find(sub.as_str(), range, find)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn find(&self, args: FindArgs) -> isize {
|
|
self._find(args, |r, s| Some(Self::_to_char_idx(r, r.find(s)?)))
|
|
.map_or(-1, |v| v as isize)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn rfind(&self, args: FindArgs) -> isize {
|
|
self._find(args, |r, s| Some(Self::_to_char_idx(r, r.rfind(s)?)))
|
|
.map_or(-1, |v| v as isize)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn index(&self, args: FindArgs, vm: &VirtualMachine) -> PyResult<usize> {
|
|
self._find(args, |r, s| Some(Self::_to_char_idx(r, r.find(s)?)))
|
|
.ok_or_else(|| vm.new_value_error("substring not found".to_owned()))
|
|
}
|
|
|
|
#[pymethod]
|
|
fn rindex(&self, args: FindArgs, vm: &VirtualMachine) -> PyResult<usize> {
|
|
self._find(args, |r, s| Some(Self::_to_char_idx(r, r.rfind(s)?)))
|
|
.ok_or_else(|| vm.new_value_error("substring not found".to_owned()))
|
|
}
|
|
|
|
#[pymethod]
|
|
fn partition(&self, sep: PyStrRef, vm: &VirtualMachine) -> PyResult {
|
|
let (front, has_mid, back) = self.as_str().py_partition(
|
|
sep.as_str(),
|
|
|| self.as_str().splitn(2, sep.as_str()),
|
|
vm,
|
|
)?;
|
|
let partition = (
|
|
self.new_substr(front),
|
|
if has_mid {
|
|
sep
|
|
} else {
|
|
vm.ctx.new_str(ascii!(""))
|
|
},
|
|
self.new_substr(back),
|
|
);
|
|
Ok(partition.to_pyobject(vm))
|
|
}
|
|
|
|
#[pymethod]
|
|
fn rpartition(&self, sep: PyStrRef, vm: &VirtualMachine) -> PyResult {
|
|
let (back, has_mid, front) = self.as_str().py_partition(
|
|
sep.as_str(),
|
|
|| self.as_str().rsplitn(2, sep.as_str()),
|
|
vm,
|
|
)?;
|
|
Ok((
|
|
self.new_substr(front),
|
|
if has_mid {
|
|
sep
|
|
} else {
|
|
vm.ctx.new_str(ascii!(""))
|
|
},
|
|
self.new_substr(back),
|
|
)
|
|
.to_pyobject(vm))
|
|
}
|
|
|
|
/// Return `true` if the sequence is ASCII titlecase and the sequence is not
|
|
/// empty, `false` otherwise.
|
|
#[pymethod]
|
|
fn istitle(&self) -> bool {
|
|
if self.bytes.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
let mut cased = false;
|
|
let mut previous_is_cased = false;
|
|
for c in self.as_str().chars() {
|
|
if c.is_uppercase() || c.is_titlecase() {
|
|
if previous_is_cased {
|
|
return false;
|
|
}
|
|
previous_is_cased = true;
|
|
cased = true;
|
|
} else if c.is_lowercase() {
|
|
if !previous_is_cased {
|
|
return false;
|
|
}
|
|
previous_is_cased = true;
|
|
cased = true;
|
|
} else {
|
|
previous_is_cased = false;
|
|
}
|
|
}
|
|
cased
|
|
}
|
|
|
|
#[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())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn zfill(&self, width: isize) -> String {
|
|
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))
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn _pad(
|
|
&self,
|
|
width: isize,
|
|
fillchar: OptionalArg<PyStrRef>,
|
|
pad: fn(&str, usize, char, usize) -> String,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<String> {
|
|
let fillchar = fillchar.map_or(Ok(' '), |ref s| {
|
|
s.as_str().chars().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())
|
|
} else {
|
|
pad(self.as_str(), width as usize, fillchar, self.len())
|
|
})
|
|
}
|
|
|
|
#[pymethod]
|
|
fn center(
|
|
&self,
|
|
width: isize,
|
|
fillchar: OptionalArg<PyStrRef>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<String> {
|
|
self._pad(width, fillchar, AnyStr::py_center, vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn ljust(
|
|
&self,
|
|
width: isize,
|
|
fillchar: OptionalArg<PyStrRef>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<String> {
|
|
self._pad(width, fillchar, AnyStr::py_ljust, vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn rjust(
|
|
&self,
|
|
width: isize,
|
|
fillchar: OptionalArg<PyStrRef>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<String> {
|
|
self._pad(width, fillchar, AnyStr::py_rjust, vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn expandtabs(&self, args: anystr::ExpandTabsArgs) -> String {
|
|
let tab_stop = args.tabsize();
|
|
let mut expanded_str = String::with_capacity(self.byte_len());
|
|
let mut tab_size = tab_stop;
|
|
let mut col_count = 0usize;
|
|
for ch in self.as_str().chars() {
|
|
match ch {
|
|
'\t' => {
|
|
let num_spaces = tab_size - col_count;
|
|
col_count += num_spaces;
|
|
let expand = " ".repeat(num_spaces);
|
|
expanded_str.push_str(&expand);
|
|
}
|
|
'\r' | '\n' => {
|
|
expanded_str.push(ch);
|
|
col_count = 0;
|
|
tab_size = 0;
|
|
}
|
|
_ => {
|
|
expanded_str.push(ch);
|
|
col_count += 1;
|
|
}
|
|
}
|
|
if col_count >= tab_size {
|
|
tab_size += tab_stop;
|
|
}
|
|
}
|
|
expanded_str
|
|
}
|
|
|
|
#[pymethod]
|
|
fn isidentifier(&self) -> bool {
|
|
let mut chars = self.as_str().chars();
|
|
let is_identifier_start = chars.next().map_or(false, |c| c == '_' || is_xid_start(c));
|
|
// a string is not an identifier if it has whitespace or starts with a number
|
|
is_identifier_start && chars.all(is_xid_continue)
|
|
}
|
|
|
|
// https://docs.python.org/3/library/stdtypes.html#str.translate
|
|
#[pymethod]
|
|
fn translate(&self, table: PyObjectRef, vm: &VirtualMachine) -> PyResult<String> {
|
|
vm.get_method_or_type_error(table.clone(), "__getitem__", || {
|
|
format!("'{}' object is not subscriptable", table.class().name())
|
|
})?;
|
|
|
|
let mut translated = String::new();
|
|
for c in self.as_str().chars() {
|
|
match table.get_item((c as u32).to_pyobject(vm), vm) {
|
|
Ok(value) => {
|
|
if let Some(text) = value.payload::<PyStr>() {
|
|
translated.push_str(text.as_str());
|
|
} else if let Some(bigint) = value.payload::<PyInt>() {
|
|
let ch = bigint
|
|
.as_bigint()
|
|
.to_u32()
|
|
.and_then(std::char::from_u32)
|
|
.ok_or_else(|| {
|
|
vm.new_value_error(
|
|
"character mapping must be in range(0x110000)".to_owned(),
|
|
)
|
|
})?;
|
|
translated.push(ch as char);
|
|
} else if !vm.is_none(&value) {
|
|
return Err(vm.new_type_error(
|
|
"character mapping must return integer, None or str".to_owned(),
|
|
));
|
|
}
|
|
}
|
|
_ => translated.push(c),
|
|
}
|
|
}
|
|
Ok(translated)
|
|
}
|
|
|
|
#[pystaticmethod]
|
|
fn maketrans(
|
|
dict_or_str: PyObjectRef,
|
|
to_str: OptionalArg<PyStrRef>,
|
|
none_str: OptionalArg<PyStrRef>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult {
|
|
let new_dict = vm.ctx.new_dict();
|
|
if let OptionalArg::Present(to_str) = to_str {
|
|
match dict_or_str.downcast::<PyStr>() {
|
|
Ok(from_str) => {
|
|
if to_str.len() == from_str.len() {
|
|
for (c1, c2) in from_str.as_str().chars().zip(to_str.as_str().chars()) {
|
|
new_dict.set_item(
|
|
vm.new_pyobj(c1 as u32),
|
|
vm.new_pyobj(c2 as u32),
|
|
vm,
|
|
)?;
|
|
}
|
|
if let OptionalArg::Present(none_str) = none_str {
|
|
for c in none_str.as_str().chars() {
|
|
new_dict.set_item(vm.new_pyobj(c as u32), vm.ctx.none(), vm)?;
|
|
}
|
|
}
|
|
Ok(new_dict.to_pyobject(vm))
|
|
} else {
|
|
Err(vm.new_value_error(
|
|
"the first two maketrans arguments must have equal length".to_owned(),
|
|
))
|
|
}
|
|
}
|
|
_ => Err(vm.new_type_error(
|
|
"first maketrans argument must be a string if there is a second argument"
|
|
.to_owned(),
|
|
)),
|
|
}
|
|
} else {
|
|
// dict_str must be a dict
|
|
match dict_or_str.downcast::<PyDict>() {
|
|
Ok(dict) => {
|
|
for (key, val) in dict {
|
|
if let Some(num) = key.payload::<PyInt>() {
|
|
new_dict.set_item(num.as_bigint().to_i32().to_pyobject(vm), val, vm)?;
|
|
} else if let Some(string) = key.payload::<PyStr>() {
|
|
if string.len() == 1 {
|
|
let num_value = string.as_str().chars().next().unwrap() as u32;
|
|
new_dict.set_item(num_value.to_pyobject(vm), val, vm)?;
|
|
} else {
|
|
return Err(vm.new_value_error(
|
|
"string keys in translate table must be of length 1".to_owned(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
Ok(new_dict.to_pyobject(vm))
|
|
}
|
|
_ => Err(vm.new_value_error(
|
|
"if you give only one argument to maketrans it must be a dict".to_owned(),
|
|
)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[pymethod]
|
|
fn encode(zelf: PyRef<Self>, args: EncodeArgs, vm: &VirtualMachine) -> PyResult<PyBytesRef> {
|
|
encode_string(zelf, args.encoding, args.errors, vm)
|
|
}
|
|
}
|
|
|
|
impl PyStrRef {
|
|
pub fn concat_in_place(&mut self, other: &str, vm: &VirtualMachine) {
|
|
// TODO: call [A]Rc::get_mut on the str to try to mutate the data in place
|
|
if other.is_empty() {
|
|
return;
|
|
}
|
|
let mut s = String::with_capacity(self.byte_len() + other.len());
|
|
s.push_str(self.as_ref());
|
|
s.push_str(other);
|
|
*self = PyStr::from(s).into_ref(vm);
|
|
}
|
|
}
|
|
|
|
impl Hashable for PyStr {
|
|
#[inline]
|
|
fn hash(zelf: &crate::Py<Self>, vm: &VirtualMachine) -> PyResult<hash::PyHash> {
|
|
Ok(zelf.hash(vm))
|
|
}
|
|
}
|
|
|
|
impl Comparable for PyStr {
|
|
fn cmp(
|
|
zelf: &crate::Py<Self>,
|
|
other: &PyObject,
|
|
op: PyComparisonOp,
|
|
_vm: &VirtualMachine,
|
|
) -> PyResult<PyComparisonValue> {
|
|
if let Some(res) = op.identical_optimization(zelf, other) {
|
|
return Ok(res.into());
|
|
}
|
|
let other = class_or_notimplemented!(Self, other);
|
|
Ok(op.eval_ord(zelf.as_str().cmp(other.as_str())).into())
|
|
}
|
|
}
|
|
|
|
impl Iterable for PyStr {
|
|
fn iter(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult {
|
|
Ok(PyStrIterator {
|
|
internal: PyMutex::new((PositionIterInternal::new(zelf, 0), 0)),
|
|
}
|
|
.into_pyobject(vm))
|
|
}
|
|
}
|
|
|
|
impl AsMapping for PyStr {
|
|
fn as_mapping(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyMappingMethods {
|
|
Self::MAPPING_METHODS
|
|
}
|
|
}
|
|
|
|
impl PyStr {
|
|
const MAPPING_METHODS: PyMappingMethods = PyMappingMethods {
|
|
length: Some(|mapping, _vm| Ok(Self::mapping_downcast(mapping).len())),
|
|
subscript: Some(|mapping, needle, vm| Self::mapping_downcast(mapping)._getitem(needle, vm)),
|
|
ass_subscript: None,
|
|
};
|
|
}
|
|
|
|
impl AsSequence for PyStr {
|
|
fn as_sequence(
|
|
_zelf: &Py<Self>,
|
|
_vm: &VirtualMachine,
|
|
) -> std::borrow::Cow<'static, PySequenceMethods> {
|
|
std::borrow::Cow::Borrowed(&Self::SEQUENCE_METHDOS)
|
|
}
|
|
}
|
|
|
|
impl PyStr {
|
|
const SEQUENCE_METHDOS: PySequenceMethods = PySequenceMethods {
|
|
length: Some(|seq, _vm| Ok(Self::sequence_downcast(seq).len())),
|
|
concat: Some(|seq, other, vm| {
|
|
let zelf = Self::sequence_downcast(seq);
|
|
Self::add(zelf.to_owned(), other.to_owned(), vm)
|
|
}),
|
|
repeat: Some(|seq, n, vm| {
|
|
let zelf = Self::sequence_downcast(seq);
|
|
Self::mul(zelf.to_owned(), n as isize, vm).map(|x| x.into())
|
|
}),
|
|
item: Some(|seq, i, vm| {
|
|
let zelf = Self::sequence_downcast(seq);
|
|
zelf.get_item_by_index(vm, i)
|
|
.map(|x| zelf.new_substr(x.to_string()).into_ref(vm).into())
|
|
}),
|
|
contains: Some(|seq, needle, vm| Self::sequence_downcast(seq)._contains(needle, vm)),
|
|
..*PySequenceMethods::not_implemented()
|
|
};
|
|
}
|
|
|
|
#[derive(FromArgs)]
|
|
struct EncodeArgs {
|
|
#[pyarg(any, default)]
|
|
encoding: Option<PyStrRef>,
|
|
#[pyarg(any, default)]
|
|
errors: Option<PyStrRef>,
|
|
}
|
|
|
|
pub(crate) fn encode_string(
|
|
s: PyStrRef,
|
|
encoding: Option<PyStrRef>,
|
|
errors: Option<PyStrRef>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<PyBytesRef> {
|
|
let encoding = encoding
|
|
.as_ref()
|
|
.map_or(crate::codecs::DEFAULT_ENCODING, |s| s.as_str());
|
|
vm.state.codec_registry.encode_text(s, encoding, errors, vm)
|
|
}
|
|
|
|
impl PyPayload for PyStr {
|
|
fn class(vm: &VirtualMachine) -> &PyTypeRef {
|
|
&vm.ctx.types.str_type
|
|
}
|
|
}
|
|
|
|
impl ToPyObject for String {
|
|
fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef {
|
|
vm.ctx.new_str(self).into()
|
|
}
|
|
}
|
|
|
|
impl ToPyObject for char {
|
|
fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef {
|
|
vm.ctx.new_str(self.to_string()).into()
|
|
}
|
|
}
|
|
|
|
impl ToPyObject for &str {
|
|
fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef {
|
|
vm.ctx.new_str(self).into()
|
|
}
|
|
}
|
|
|
|
impl ToPyObject for &String {
|
|
fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef {
|
|
vm.ctx.new_str(self.clone()).into()
|
|
}
|
|
}
|
|
|
|
impl ToPyObject for &AsciiStr {
|
|
fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef {
|
|
vm.ctx.new_str(self).into()
|
|
}
|
|
}
|
|
|
|
impl ToPyObject for AsciiString {
|
|
fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef {
|
|
vm.ctx.new_str(self).into()
|
|
}
|
|
}
|
|
|
|
type SplitArgs<'a> = anystr::SplitArgs<'a, PyStrRef>;
|
|
|
|
#[derive(FromArgs)]
|
|
pub struct FindArgs {
|
|
#[pyarg(positional)]
|
|
sub: PyStrRef,
|
|
#[pyarg(positional, default)]
|
|
start: Option<PyIntRef>,
|
|
#[pyarg(positional, default)]
|
|
end: Option<PyIntRef>,
|
|
}
|
|
|
|
impl FindArgs {
|
|
fn get_value(self, len: usize) -> (PyStrRef, std::ops::Range<usize>) {
|
|
let range = adjust_indices(self.start, self.end, len);
|
|
(self.sub, range)
|
|
}
|
|
}
|
|
|
|
pub fn init(ctx: &Context) {
|
|
PyStr::extend_class(ctx, &ctx.types.str_type);
|
|
|
|
PyStrIterator::extend_class(ctx, &ctx.types.str_iterator_type);
|
|
}
|
|
|
|
impl SliceableSequenceOp for PyStr {
|
|
type Item = char;
|
|
type Sliced = String;
|
|
|
|
fn do_get(&self, index: usize) -> Self::Item {
|
|
if self.is_ascii() {
|
|
self.bytes[index] as char
|
|
} else {
|
|
self.as_str().chars().nth(index).unwrap()
|
|
}
|
|
}
|
|
|
|
fn do_slice(&self, range: Range<usize>) -> Self::Sliced {
|
|
let value = self.as_str();
|
|
if self.is_ascii() {
|
|
value[range].to_owned()
|
|
} else {
|
|
rustpython_common::str::get_chars(value, range).to_owned()
|
|
}
|
|
}
|
|
|
|
fn do_slice_reverse(&self, range: Range<usize>) -> Self::Sliced {
|
|
if self.is_ascii() {
|
|
// this is an ascii string
|
|
let mut v = self.bytes[range].to_vec();
|
|
v.reverse();
|
|
unsafe {
|
|
// SAFETY: an ascii string is always utf8
|
|
String::from_utf8_unchecked(v)
|
|
}
|
|
} else {
|
|
let mut s = String::with_capacity(self.bytes.len());
|
|
s.extend(
|
|
self.as_str()
|
|
.chars()
|
|
.rev()
|
|
.skip(self.char_len() - range.end)
|
|
.take(range.end - range.start),
|
|
);
|
|
s
|
|
}
|
|
}
|
|
|
|
fn do_stepped_slice(&self, range: Range<usize>, step: usize) -> Self::Sliced {
|
|
if self.is_ascii() {
|
|
let v = self.bytes[range].iter().copied().step_by(step).collect();
|
|
unsafe {
|
|
// SAFETY: Any subset of ascii string is a valid utf8 string
|
|
String::from_utf8_unchecked(v)
|
|
}
|
|
} else {
|
|
let mut s = String::with_capacity(2 * ((range.len() / step) + 1));
|
|
s.extend(
|
|
self.as_str()
|
|
.chars()
|
|
.skip(range.start)
|
|
.take(range.end - range.start)
|
|
.step_by(step),
|
|
);
|
|
s
|
|
}
|
|
}
|
|
|
|
fn do_stepped_slice_reverse(&self, range: Range<usize>, step: usize) -> Self::Sliced {
|
|
if self.is_ascii() {
|
|
// this is an ascii string
|
|
let v: Vec<u8> = self.bytes[range]
|
|
.iter()
|
|
.rev()
|
|
.copied()
|
|
.step_by(step)
|
|
.collect();
|
|
// TODO: from_utf8_unchecked?
|
|
String::from_utf8(v).unwrap()
|
|
} else {
|
|
// not ascii, so the codepoints have to be at least 2 bytes each
|
|
let mut s = String::with_capacity(2 * ((range.len() / step) + 1));
|
|
s.extend(
|
|
self.as_str()
|
|
.chars()
|
|
.rev()
|
|
.skip(self.char_len() - range.end)
|
|
.take(range.end - range.start)
|
|
.step_by(step),
|
|
);
|
|
s
|
|
}
|
|
}
|
|
|
|
fn empty() -> Self::Sliced {
|
|
String::new()
|
|
}
|
|
|
|
fn len(&self) -> usize {
|
|
self.char_len()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::Interpreter;
|
|
use std::ops::Deref;
|
|
|
|
#[test]
|
|
fn str_title() {
|
|
let tests = vec![
|
|
(" Hello ", " hello "),
|
|
("Hello ", "hello "),
|
|
("Hello ", "Hello "),
|
|
("Format This As Title String", "fOrMaT thIs aS titLe String"),
|
|
("Format,This-As*Title;String", "fOrMaT,thIs-aS*titLe;String"),
|
|
("Getint", "getInt"),
|
|
("Greek Ωppercases ...", "greek ωppercases ..."),
|
|
("Greek ῼitlecases ...", "greek ῳitlecases ..."),
|
|
];
|
|
for (title, input) in tests {
|
|
assert_eq!(PyStr::from(input).title().as_str(), title);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn str_istitle() {
|
|
let pos = vec![
|
|
"A",
|
|
"A Titlecased Line",
|
|
"A\nTitlecased Line",
|
|
"A Titlecased, Line",
|
|
"Greek Ωppercases ...",
|
|
"Greek ῼitlecases ...",
|
|
];
|
|
|
|
for s in pos {
|
|
assert!(PyStr::from(s).istitle());
|
|
}
|
|
|
|
let neg = vec![
|
|
"",
|
|
"a",
|
|
"\n",
|
|
"Not a capitalized String",
|
|
"Not\ta Titlecase String",
|
|
"Not--a Titlecase String",
|
|
"NOT",
|
|
];
|
|
for s in neg {
|
|
assert!(!PyStr::from(s).istitle());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn str_maketrans_and_translate() {
|
|
Interpreter::without_stdlib(Default::default()).enter(|vm| {
|
|
let table = vm.ctx.new_dict();
|
|
table
|
|
.set_item("a", vm.ctx.new_str("🎅").into(), &vm)
|
|
.unwrap();
|
|
table.set_item("b", vm.ctx.none(), &vm).unwrap();
|
|
table
|
|
.set_item("c", vm.ctx.new_str(ascii!("xda")).into(), &vm)
|
|
.unwrap();
|
|
let translated = PyStr::maketrans(
|
|
table.into(),
|
|
OptionalArg::Missing,
|
|
OptionalArg::Missing,
|
|
&vm,
|
|
)
|
|
.unwrap();
|
|
let text = PyStr::from("abc");
|
|
let translated = text.translate(translated, &vm).unwrap();
|
|
assert_eq!(translated, "🎅xda".to_owned());
|
|
let translated = text.translate(vm.ctx.new_int(3).into(), &vm);
|
|
assert_eq!(
|
|
translated.unwrap_err().class().name().deref(),
|
|
"TypeError".to_owned()
|
|
);
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<'s> AnyStrWrapper<'s> for PyStrRef {
|
|
type Str = str;
|
|
fn as_ref(&self) -> &str {
|
|
&*self.as_str()
|
|
}
|
|
}
|
|
|
|
impl AnyStrContainer<str> for String {
|
|
fn new() -> Self {
|
|
String::new()
|
|
}
|
|
|
|
fn with_capacity(capacity: usize) -> Self {
|
|
String::with_capacity(capacity)
|
|
}
|
|
|
|
fn push_str(&mut self, other: &str) {
|
|
String::push_str(self, other)
|
|
}
|
|
}
|
|
|
|
impl<'s> AnyStr<'s> for str {
|
|
type Char = char;
|
|
type Container = String;
|
|
type CharIter = std::str::Chars<'s>;
|
|
type ElementIter = std::str::Chars<'s>;
|
|
|
|
fn element_bytes_len(c: char) -> usize {
|
|
c.len_utf8()
|
|
}
|
|
|
|
fn to_container(&self) -> Self::Container {
|
|
self.to_owned()
|
|
}
|
|
|
|
fn as_bytes(&self) -> &[u8] {
|
|
self.as_bytes()
|
|
}
|
|
|
|
fn as_utf8_str(&self) -> Result<&str, std::str::Utf8Error> {
|
|
Ok(self)
|
|
}
|
|
|
|
fn chars(&'s self) -> Self::CharIter {
|
|
str::chars(self)
|
|
}
|
|
|
|
fn elements(&'s self) -> Self::ElementIter {
|
|
str::chars(self)
|
|
}
|
|
|
|
fn get_bytes(&self, range: std::ops::Range<usize>) -> &Self {
|
|
&self[range]
|
|
}
|
|
|
|
fn get_chars(&self, range: std::ops::Range<usize>) -> &Self {
|
|
rustpython_common::str::get_chars(self, range)
|
|
}
|
|
|
|
fn is_empty(&self) -> bool {
|
|
Self::is_empty(self)
|
|
}
|
|
|
|
fn bytes_len(&self) -> usize {
|
|
Self::len(self)
|
|
}
|
|
|
|
fn py_split_whitespace<F>(&self, maxsplit: isize, convert: F) -> Vec<PyObjectRef>
|
|
where
|
|
F: Fn(&Self) -> PyObjectRef,
|
|
{
|
|
// CPython split_whitespace
|
|
let mut splited = Vec::new();
|
|
let mut last_offset = 0;
|
|
let mut count = maxsplit;
|
|
for (offset, _) in self.match_indices(|c: char| c.is_ascii_whitespace() || c == '\x0b') {
|
|
if last_offset == offset {
|
|
last_offset += 1;
|
|
continue;
|
|
}
|
|
if count == 0 {
|
|
break;
|
|
}
|
|
splited.push(convert(&self[last_offset..offset]));
|
|
last_offset = offset + 1;
|
|
count -= 1;
|
|
}
|
|
if last_offset != self.len() {
|
|
splited.push(convert(&self[last_offset..]));
|
|
}
|
|
splited
|
|
}
|
|
|
|
fn py_rsplit_whitespace<F>(&self, maxsplit: isize, convert: F) -> Vec<PyObjectRef>
|
|
where
|
|
F: Fn(&Self) -> PyObjectRef,
|
|
{
|
|
// CPython rsplit_whitespace
|
|
let mut splited = Vec::new();
|
|
let mut last_offset = self.len();
|
|
let mut count = maxsplit;
|
|
for (offset, _) in self.rmatch_indices(|c: char| c.is_ascii_whitespace() || c == '\x0b') {
|
|
if last_offset == offset + 1 {
|
|
last_offset -= 1;
|
|
continue;
|
|
}
|
|
if count == 0 {
|
|
break;
|
|
}
|
|
splited.push(convert(&self[offset + 1..last_offset]));
|
|
last_offset = offset;
|
|
count -= 1;
|
|
}
|
|
if last_offset != 0 {
|
|
splited.push(convert(&self[..last_offset]));
|
|
}
|
|
splited
|
|
}
|
|
}
|