Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 70 additions & 100 deletions crates/vm/src/builtins/interpolation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ use super::{
use crate::{
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
class::PyClassImpl,
common::hash::PyHash,
convert::ToPyObject,
function::PyComparisonValue,
types::{Comparable, Constructor, Hashable, PyComparisonOp, Representable},
function::{ArgSpec, FuncArgs},
types::{Constructor, Representable},
};
use itertools::Itertools;
use rustpython_common::wtf8::Wtf8Buf;
use rustpython_common::wtf8::{Wtf8Buf, wtf8_concat};

#[pyclass(module = "string.templatelib", name = "Interpolation")]
#[derive(Debug, Clone)]
Expand All @@ -34,6 +32,41 @@ impl PyPayload for PyInterpolation {
}

impl PyInterpolation {
const INVALID_CONVERSION: &'static str =
"Interpolation() argument 'conversion' must be one of 's', 'a' or 'r'";

fn valid_conversion(conversion: &PyObject, vm: &VirtualMachine) -> bool {
vm.is_none(conversion)
|| conversion
.downcast_ref::<PyStr>()
.is_some_and(|s| matches!(s.to_str(), Some("s" | "r" | "a")))
}

fn string_argument(
value: Option<PyObjectRef>,
name: &str,
vm: &VirtualMachine,
) -> PyResult<PyStrRef> {
value.map_or_else(
|| Ok(vm.ctx.empty_str.to_owned()),
|value| {
value.downcast::<PyStr>().map_err(|value| {
let type_name = value.class().slot_name();
// _PyArg_BadArgument limits tp_name to 50 UTF-8 bytes,
// omitting a partial final code point.
let type_name = if vm.is_none(&value) {
"None"
} else {
&type_name[..type_name.floor_char_boundary(50)]
};
vm.new_type_error(format!(
"Interpolation() argument '{name}' must be str, not {type_name}"
))
})
},
)
}

pub fn new(
value: PyObjectRef,
expression: PyObjectRef,
Expand All @@ -42,14 +75,8 @@ impl PyInterpolation {
vm: &VirtualMachine,
) -> PyResult<Self> {
// Validate conversion like _PyInterpolation_Build does
let is_valid = vm.is_none(&conversion)
|| conversion
.downcast_ref::<PyStr>()
.is_some_and(|s| matches!(s.to_str(), Some("s" | "r" | "a")));
if !is_valid {
return Err(vm.new_system_error(
"Interpolation() argument 'conversion' must be one of 's', 'a' or 'r'",
));
if !Self::valid_conversion(&conversion, vm) {
return Err(vm.new_system_error(Self::INVALID_CONVERSION));
}
Ok(Self {
value,
Expand All @@ -61,54 +88,41 @@ impl PyInterpolation {
}

impl Constructor for PyInterpolation {
type Args = InterpolationArgs;
type Args = FuncArgs;

fn py_new(_cls: &Py<PyType>, args: Self::Args, vm: &VirtualMachine) -> PyResult<Self> {
let conversion: PyObjectRef = if let Some(s) = args.conversion {
let has_flag = s
.as_bytes()
.iter()
.exactly_one()
.is_ok_and(|s| matches!(*s, b's' | b'r' | b'a'));
if !has_flag {
return Err(vm.new_value_error(
"Interpolation() argument 'conversion' must be one of 's', 'a' or 'r'",
));
}
s.into()
} else {
vm.ctx.none()
};

let expression = args.expression.into();
let format_spec = args.format_spec;
// Clinic binds every argument before converting them in declaration order.
let mut bound = ArgSpec {
fname: "Interpolation",
keywords: &["value", "expression", "conversion", "format_spec"],
required: 1,
max_positional: 4,
}
.parse(&args, vm)?;
let value = bound[0].take().unwrap();
let expression = Self::string_argument(bound[1].take(), "expression", vm)?;
let conversion = bound[2].take().unwrap_or_else(|| vm.ctx.none());
if !vm.is_none(&conversion) && conversion.downcast_ref::<PyStr>().is_none() {
return Err(vm.new_type_error(wtf8_concat!(
"Interpolation() argument 'conversion' must be str, not ",
conversion.class().fully_qualified_name(vm)?,
)));
}
if !Self::valid_conversion(&conversion, vm) {
return Err(vm.new_value_error(Self::INVALID_CONVERSION));
}
let format_spec = Self::string_argument(bound[3].take(), "format_spec", vm)?;

Ok(Self {
value: args.value,
expression,
value,
expression: expression.into(),
conversion,
format_spec,
})
}
}

#[derive(FromArgs)]
pub struct InterpolationArgs {
#[pyarg(positional)]
value: PyObjectRef,
#[pyarg(any, default = "")]
expression: PyStrRef,
#[pyarg(
any,
optional,
error_msg = "Interpolation() argument 'conversion' must be str or None"
)]
conversion: Option<PyStrRef>,
#[pyarg(any, default = "")]
format_spec: PyStrRef,
}

#[pyclass(with(Constructor, Comparable, Hashable, Representable))]
#[pyclass(with(Constructor, Representable))]
impl PyInterpolation {
#[pyattr]
fn __match_args__(ctx: &Context) -> PyTupleRef {
Expand Down Expand Up @@ -142,63 +156,19 @@ impl PyInterpolation {
}
}

impl Comparable for PyInterpolation {
fn cmp(
zelf: &Py<Self>,
other: &PyObject,
op: PyComparisonOp,
vm: &VirtualMachine,
) -> PyResult<PyComparisonValue> {
op.eq_only(|| {
let other = class_or_notimplemented!(Self, other);

let eq = vm.bool_eq(&zelf.value, &other.value)?
&& vm.bool_eq(zelf.expression.as_object(), other.expression.as_object())?
&& vm.bool_eq(&zelf.conversion, &other.conversion)?
&& vm.bool_eq(zelf.format_spec.as_object(), other.format_spec.as_object())?;

Ok(eq.into())
})
}
}

impl Hashable for PyInterpolation {
fn hash(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyHash> {
// Hash based on (value, expression, conversion, format_spec)
let value_hash = zelf.value.hash(vm)?;
let expr_hash = zelf.expression.as_object().hash(vm)?;
let conv_hash = zelf.conversion.hash(vm)?;
let spec_hash = zelf.format_spec.as_object().hash(vm)?;

// Combine hashes
Ok(value_hash
.wrapping_add(expr_hash.wrapping_mul(3))
.wrapping_add(conv_hash.wrapping_mul(5))
.wrapping_add(spec_hash.wrapping_mul(7)))
}
}

impl Representable for PyInterpolation {
#[inline]
fn repr_wtf8(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<Wtf8Buf> {
let value_repr = zelf.value.repr(vm)?;
let expr_repr = zelf.expression.repr(vm)?;
let spec_repr = zelf.format_spec.repr(vm)?;

let mut result = Wtf8Buf::from("Interpolation(");
result.push_wtf8(value_repr.as_wtf8());
// Release each repr result before invoking the next field's callback.
result.push_wtf8(zelf.value.repr(vm)?.as_wtf8());
result.push_str(", ");
result.push_wtf8(expr_repr.as_wtf8());
result.push_wtf8(zelf.expression.repr(vm)?.as_wtf8());
result.push_str(", ");
if vm.is_none(&zelf.conversion) {
result.push_str("None");
} else {
result.push_wtf8(zelf.conversion.repr(vm)?.as_wtf8());
}
result.push_wtf8(zelf.conversion.repr(vm)?.as_wtf8());
result.push_str(", ");
result.push_str(&spec_repr);
result.push_wtf8(zelf.format_spec.as_object().repr(vm)?.as_wtf8());
result.push_char(')');

Ok(result)
}
}
Expand Down
53 changes: 16 additions & 37 deletions crates/vm/src/builtins/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@ use crate::{
atomic_func,
class::{PyClassDef, PyClassImpl},
common::lock::LazyLock,
function::{FuncArgs, PyComparisonValue},
function::FuncArgs,
protocol::{PyIterReturn, PySequenceMethods},
types::{
AsSequence, Comparable, Constructor, IterNext, Iterable, PyComparisonOp, Representable,
SelfIter,
},
types::{AsSequence, Constructor, IterNext, Iterable, Representable, SelfIter},
};
use rustpython_common::wtf8::{Wtf8Buf, wtf8_concat};

Expand Down Expand Up @@ -76,9 +73,9 @@ impl Constructor for PyTemplate {
interpolations.push(item.clone());
last_was_str = false;
} else {
return Err(vm.new_type_error(format!(
"Template.__new__ *args need to be of type 'str' or 'Interpolation', got {}",
item.class().name()
return Err(vm.new_type_error(wtf8_concat!(
"Template.__new__ *args need to be of type 'str' or 'Interpolation', got ",
item.class().fully_qualified_name(vm)?,
)));
}
}
Expand All @@ -97,13 +94,16 @@ impl Constructor for PyTemplate {

impl PyTemplate {
fn concat(&self, other: &PyObject, vm: &VirtualMachine) -> PyResult<PyRef<Self>> {
let other = other.downcast_ref::<Self>().ok_or_else(|| {
vm.new_type_error(format!(
r#"can only concatenate {name} (not "{}") to {name}"#,
other.class().slot_name(),
name = Self::TP_NAME,
))
})?;
let Some(other) = other.downcast_ref::<Self>() else {
return Err(vm.new_type_error(wtf8_concat!(
"can only concatenate ",
Self::TP_NAME,
" (not \"",
other.class().fully_qualified_name(vm)?,
"\") to ",
Self::TP_NAME,
)));
};

// Concatenate the two templates
let mut new_strings: Vec<PyObjectRef> = Vec::new();
Expand Down Expand Up @@ -163,7 +163,7 @@ impl PyTemplate {
}
}

#[pyclass(with(Constructor, Comparable, Iterable, Representable, AsSequence))]
#[pyclass(with(Constructor, Iterable, Representable, AsSequence))]
impl Py<PyTemplate> {
#[pygetset]
fn values(&self, vm: &VirtualMachine) -> PyTupleRef {
Expand Down Expand Up @@ -220,27 +220,6 @@ impl AsSequence for PyTemplate {
}
}

impl Comparable for PyTemplate {
fn cmp(
zelf: &Py<Self>,
other: &PyObject,
op: PyComparisonOp,
vm: &VirtualMachine,
) -> PyResult<PyComparisonValue> {
op.eq_only(|| {
let other = class_or_notimplemented!(Self, other);

let eq = vm.bool_eq(zelf.strings.as_object(), other.strings.as_object())?
&& vm.bool_eq(
zelf.interpolations.as_object(),
other.interpolations.as_object(),
)?;

Ok(eq.into())
})
}
}

impl Iterable for PyTemplate {
fn iter(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult {
Ok(PyTemplateIter::new(zelf).into_pyobject(vm))
Expand Down
Loading