mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
* Report errors from #[pyclass] trait items The trait branch of impl_pyclass_impl built its own ImplContext, so the errors collected while extracting trait items were never emitted: an invalid #[pymethod] or #[pygetset] in a #[pyclass] trait was left out of the class without a compile error. The trait branch now uses the shared context whose errors are emitted. Assisted-by: Claude Code:claude-opus-5-5 * Pass the object to #[pymethod] and #[pygetset] receivers outside vm Outside crates/vm, where impl Py<T> cannot be written, #[pymethod] and #[pygetset] receivers that took &self become zelf: &Py<Self> inside the existing impl T blocks. Where Rust code calls one of these methods on a payload value, the logic stays in a payload method and the Python method forwards to it. Assisted-by: Claude Code:claude-opus-5-5 Assisted-by: Grok:grok-4.7 * Reject payload receivers on #[pymethod] and #[pygetset] A #[pymethod] or #[pygetset] item with a self receiver is now a compile error when self is the payload: in an impl for T rather than Py<T> or PyRef<T>, and in a #[pyclass] trait. The error points to taking zelf: &Py<Self> or zelf: PyRef<Self>, or defining the item in impl Py<T>. Assisted-by: Claude Code:claude-opus-5-5 * Pass the object to the layout test getter The subclass layout test's extra getter is a #[pygetset] on the payload. Take zelf: &Py<Self> so the receiver is the object. Assisted-by: Grok:4.7
2512 lines
94 KiB
Rust
2512 lines
94 KiB
Rust
//! `_elementtree`, the accelerator behind `xml.etree.ElementTree`.
|
|
//!
|
|
//! `Lib/xml/etree/ElementTree.py` ends with `from _elementtree import *`, so
|
|
//! the names defined here (`Element`, `SubElement`, `TreeBuilder`,
|
|
//! `XMLParser`, `ParseError`) replace the pure-Python ones, and
|
|
//! `_set_factories` hands this module the `Comment`/`ProcessingInstruction`
|
|
//! factories that only exist on the Python side. The pure-Python classes stay
|
|
//! importable (as `_Element_Py` and friends) and remain the fallback whenever
|
|
//! this module is missing, so everything here has to match their observable
|
|
//! behaviour rather than merely be fast.
|
|
//!
|
|
//! Module-level state (the two factories, plus the lazily imported
|
|
//! `xml.etree.ElementPath` and `copy.deepcopy` helpers) lives in an
|
|
//! `ElementTreeState` instance stored on the module object rather than in a
|
|
//! Rust `static`, so two `VirtualMachine`s never share it; `module_state()`
|
|
//! finds it back through `sys.modules` without going through the full import
|
|
//! machinery.
|
|
|
|
// spell-checker: ignore elementtree elementpath iterfind findtext itertext
|
|
// spell-checker: ignore treebuilder xmlparser subelement makeelement pis
|
|
|
|
pub(crate) use _elementtree::module_def;
|
|
|
|
#[pymodule(name = "_elementtree")]
|
|
pub(crate) mod _elementtree {
|
|
use crate::vm::{
|
|
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject,
|
|
VirtualMachine, atomic_func,
|
|
builtins::{PyDict, PyDictRef, PyList, PyModule, PyStr, PyType, PyTypeRef},
|
|
function::{FuncArgs, PySetterValue},
|
|
protocol::{PyMappingMethods, PyNumberMethods, PySequenceMethods},
|
|
sliceable::{SequenceIndex, SliceableSequenceOp},
|
|
types::{
|
|
AsMapping, AsNumber, AsSequence, Constructor, DefaultConstructor, Initializer,
|
|
IterNext, Iterable, Representable, SelfIter,
|
|
},
|
|
};
|
|
use rustpython_common::lock::PyRwLock;
|
|
|
|
// The per-module bookkeeping `_elementtree.c` keeps in its
|
|
// `elementtreestate`: the `Comment`/`ProcessingInstruction` factories
|
|
// installed by `_set_factories`, and two helpers imported from Python
|
|
// the first time they are needed.
|
|
#[pyclass(no_attr, module = "_elementtree", name = "_elementtree_state")]
|
|
#[derive(Debug, Default, PyPayload)]
|
|
pub(crate) struct ElementTreeState {
|
|
comment_factory: PyRwLock<Option<PyObjectRef>>,
|
|
pi_factory: PyRwLock<Option<PyObjectRef>>,
|
|
element_path: PyRwLock<Option<PyObjectRef>>,
|
|
deepcopy: PyRwLock<Option<PyObjectRef>>,
|
|
parse_error: PyRwLock<Option<PyTypeRef>>,
|
|
}
|
|
|
|
#[pyclass(flags(DISALLOW_INSTANTIATION))]
|
|
impl ElementTreeState {
|
|
/// `xml.etree.ElementPath`, imported on first use. `Element.find` and
|
|
/// friends delegate every non-trivial path to it, exactly as the C
|
|
/// accelerator does.
|
|
fn element_path(&self, vm: &VirtualMachine) -> PyResult {
|
|
let cached = self.element_path.read().clone();
|
|
if let Some(m) = cached {
|
|
return Ok(m);
|
|
}
|
|
// `vm.import()` hands back the *top* package, so the submodule
|
|
// itself is picked up from `sys.modules` afterwards.
|
|
vm.import("xml.etree.ElementPath", 0)?;
|
|
let module = vm
|
|
.sys_module
|
|
.get_attr("modules", vm)?
|
|
.get_item("xml.etree.ElementPath", vm)?;
|
|
*self.element_path.write() = Some(module.clone());
|
|
Ok(module)
|
|
}
|
|
|
|
fn deepcopy(&self, vm: &VirtualMachine) -> PyResult {
|
|
let cached = self.deepcopy.read().clone();
|
|
if let Some(f) = cached {
|
|
return Ok(f);
|
|
}
|
|
let f = vm.import("copy", 0)?.get_attr("deepcopy", vm)?;
|
|
*self.deepcopy.write() = Some(f.clone());
|
|
Ok(f)
|
|
}
|
|
}
|
|
|
|
/// The state object stashed on the module by `module_exec`.
|
|
///
|
|
/// Looked up through `sys.modules` rather than `vm.import()`: the module
|
|
/// is always already imported by the time any of its own code runs, and
|
|
/// this keeps the lookup down to two dictionary hits.
|
|
fn module_state(vm: &VirtualMachine) -> PyResult<PyRef<ElementTreeState>> {
|
|
let modules = vm.sys_module.get_attr("modules", vm)?;
|
|
let module = match modules.get_item("_elementtree", vm) {
|
|
Ok(module) => module,
|
|
Err(_) => vm.import("_elementtree", 0)?,
|
|
};
|
|
module
|
|
.get_attr("_state", vm)?
|
|
.downcast::<ElementTreeState>()
|
|
.map_err(|_| vm.new_runtime_error("_elementtree state is corrupted"))
|
|
}
|
|
|
|
pub(crate) fn module_exec(vm: &VirtualMachine, module: &Py<PyModule>) -> PyResult<()> {
|
|
__module_exec(vm, module);
|
|
let state = ElementTreeState::default().into_ref(&vm.ctx);
|
|
*state.parse_error.write() = module.get_attr("ParseError", vm)?.downcast::<PyType>().ok();
|
|
module.set_attr("_state", state, vm)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[pyattr(name = "ParseError", once)]
|
|
fn parse_error(vm: &VirtualMachine) -> PyTypeRef {
|
|
vm.ctx.new_exception_type(
|
|
"xml.etree.ElementTree",
|
|
"ParseError",
|
|
Some(vec![vm.ctx.exceptions.syntax_error.to_owned()]),
|
|
)
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn _set_factories(
|
|
comment_factory: PyObjectRef,
|
|
pi_factory: PyObjectRef,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<(PyObjectRef, PyObjectRef)> {
|
|
if !vm.is_none(&comment_factory) && !comment_factory.is_callable() {
|
|
return Err(vm.new_type_error(format!(
|
|
"Comment factory must be callable, not {}",
|
|
comment_factory.class().name()
|
|
)));
|
|
}
|
|
if !vm.is_none(&pi_factory) && !pi_factory.is_callable() {
|
|
return Err(vm.new_type_error(format!(
|
|
"PI factory must be callable, not {}",
|
|
pi_factory.class().name()
|
|
)));
|
|
}
|
|
let state = module_state(vm)?;
|
|
let old_comment = state.comment_factory.read().clone();
|
|
let old_pi = state.pi_factory.read().clone();
|
|
*state.comment_factory.write() = (!vm.is_none(&comment_factory)).then_some(comment_factory);
|
|
*state.pi_factory.write() = (!vm.is_none(&pi_factory)).then_some(pi_factory);
|
|
Ok((
|
|
old_comment.unwrap_or_else(|| vm.ctx.none()),
|
|
old_pi.unwrap_or_else(|| vm.ctx.none()),
|
|
))
|
|
}
|
|
|
|
/// `text`/`tail` as the C accelerator stores them: either the value the
|
|
/// user sees, or a list of fragments the `TreeBuilder` accumulated that
|
|
/// is joined lazily the first time the attribute is read. Keeping the
|
|
/// deferred form is what makes character data arriving in many expat
|
|
/// callbacks cost one join per element rather than one per callback.
|
|
#[derive(Debug)]
|
|
struct Joined {
|
|
obj: PyObjectRef,
|
|
/// `obj` is a list of fragments still to be joined.
|
|
pending: bool,
|
|
}
|
|
|
|
impl Joined {
|
|
fn plain(obj: PyObjectRef) -> Self {
|
|
Self {
|
|
obj,
|
|
pending: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ElementInner {
|
|
tag: PyObjectRef,
|
|
text: Joined,
|
|
tail: Joined,
|
|
/// `None` until an attribute is actually needed, mirroring the C
|
|
/// implementation's lazily allocated `extra->attrib`.
|
|
attrib: Option<PyDictRef>,
|
|
children: Vec<PyObjectRef>,
|
|
}
|
|
|
|
#[pyattr]
|
|
#[pyclass(
|
|
module = "xml.etree.ElementTree",
|
|
name = "Element",
|
|
traverse = "manual"
|
|
)]
|
|
#[derive(Debug, PyPayload)]
|
|
pub(crate) struct PyElement {
|
|
inner: PyRwLock<ElementInner>,
|
|
}
|
|
|
|
// SAFETY: every owned PyObjectRef reachable from an element is visited.
|
|
unsafe impl crate::vm::object::Traverse for PyElement {
|
|
fn traverse(&self, traverse_fn: &mut crate::vm::object::TraverseFn<'_>) {
|
|
let Some(inner) = self.inner.try_read() else {
|
|
return;
|
|
};
|
|
inner.tag.traverse(traverse_fn);
|
|
inner.text.obj.traverse(traverse_fn);
|
|
inner.tail.obj.traverse(traverse_fn);
|
|
inner.attrib.traverse(traverse_fn);
|
|
inner.children.traverse(traverse_fn);
|
|
}
|
|
|
|
fn clear(&mut self, out: &mut Vec<PyObjectRef>) {
|
|
// Only reached for an object the collector has already proven
|
|
// unreachable, so grabbing the lock cannot race with user code.
|
|
let Some(mut inner) = self.inner.try_write() else {
|
|
return;
|
|
};
|
|
let none = || -> PyObjectRef { crate::vm::Context::genesis().none.to_owned().into() };
|
|
out.append(&mut inner.children);
|
|
if let Some(attrib) = inner.attrib.take() {
|
|
out.push(attrib.into());
|
|
}
|
|
out.push(core::mem::replace(&mut inner.tag, none()));
|
|
out.push(core::mem::replace(&mut inner.text.obj, none()));
|
|
out.push(core::mem::replace(&mut inner.tail.obj, none()));
|
|
inner.text.pending = false;
|
|
inner.tail.pending = false;
|
|
}
|
|
}
|
|
|
|
impl PyElement {
|
|
fn new(tag: PyObjectRef, attrib: Option<PyDictRef>, vm: &VirtualMachine) -> Self {
|
|
let attrib = attrib.filter(|d| !d.is_empty());
|
|
Self {
|
|
inner: PyRwLock::new(ElementInner {
|
|
tag,
|
|
text: Joined::plain(vm.ctx.none()),
|
|
tail: Joined::plain(vm.ctx.none()),
|
|
attrib,
|
|
children: Vec::new(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn len(&self) -> usize {
|
|
self.inner.read().children.len()
|
|
}
|
|
|
|
fn is_empty(&self) -> bool {
|
|
self.len() == 0
|
|
}
|
|
|
|
fn child(&self, index: usize) -> Option<PyObjectRef> {
|
|
self.inner.read().children.get(index).cloned()
|
|
}
|
|
|
|
pub(crate) fn tag_obj(&self) -> PyObjectRef {
|
|
self.inner.read().tag.clone()
|
|
}
|
|
|
|
pub(crate) fn push(&self, child: PyObjectRef) {
|
|
self.inner.write().children.push(child);
|
|
}
|
|
|
|
/// Read `text`/`tail`, joining a pending fragment list in place.
|
|
fn joined_get(&self, tail: bool, vm: &VirtualMachine) -> PyResult {
|
|
{
|
|
let inner = self.inner.read();
|
|
let slot = if tail { &inner.tail } else { &inner.text };
|
|
if !slot.pending {
|
|
return Ok(slot.obj.clone());
|
|
}
|
|
if !slot.obj.downcastable::<PyList>() {
|
|
return Ok(slot.obj.clone());
|
|
}
|
|
}
|
|
// The join calls back into Python (str.join), so the lock must be
|
|
// released around it and the slot re-checked afterwards.
|
|
let list = {
|
|
let inner = self.inner.read();
|
|
let slot = if tail { &inner.tail } else { &inner.text };
|
|
slot.obj.clone()
|
|
};
|
|
let joined = list_join(&list, vm)?;
|
|
let mut inner = self.inner.write();
|
|
let slot = if tail {
|
|
&mut inner.tail
|
|
} else {
|
|
&mut inner.text
|
|
};
|
|
if slot.pending && slot.obj.is(&list) {
|
|
*slot = Joined::plain(joined.clone());
|
|
}
|
|
Ok(joined)
|
|
}
|
|
|
|
fn attrib_or_new(&self, vm: &VirtualMachine) -> PyDictRef {
|
|
let mut inner = self.inner.write();
|
|
inner
|
|
.attrib
|
|
.get_or_insert_with(|| vm.ctx.new_dict())
|
|
.clone()
|
|
}
|
|
|
|
fn attrib_opt(&self) -> Option<PyDictRef> {
|
|
self.inner.read().attrib.clone()
|
|
}
|
|
}
|
|
|
|
/// `"".join(list)`, the C helper `list_join`.
|
|
fn list_join(list: &PyObject, vm: &VirtualMachine) -> PyResult {
|
|
let empty = vm.ctx.new_str("");
|
|
vm.call_method(empty.as_object(), "join", (list.to_owned(),))
|
|
}
|
|
|
|
fn check_element(obj: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
|
|
if obj.downcastable::<PyElement>() {
|
|
Ok(())
|
|
} else {
|
|
Err(vm.new_type_error(format!(
|
|
"expected an Element, not \"{}\"",
|
|
obj.class().name()
|
|
)))
|
|
}
|
|
}
|
|
|
|
fn as_element<'a>(obj: &'a PyObject, vm: &VirtualMachine) -> PyResult<&'a Py<PyElement>> {
|
|
obj.downcast_ref::<PyElement>().ok_or_else(|| {
|
|
vm.new_type_error(format!(
|
|
"expected an Element, not \"{}\"",
|
|
obj.class().name()
|
|
))
|
|
})
|
|
}
|
|
|
|
/// Split `Element(tag, attrib, **extra)` style arguments into the tag and
|
|
/// a freshly owned attribute dictionary, matching
|
|
/// `get_attrib_from_keywords`: a positional `attrib` is copied, an
|
|
/// `attrib` keyword is popped and copied, and the rest of the keywords
|
|
/// are merged over it.
|
|
fn parse_attrib_args(
|
|
args: FuncArgs,
|
|
func_name: &str,
|
|
extra_positional: usize,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<(PyObjectRef, Option<PyDictRef>)> {
|
|
let mut args = args;
|
|
let nargs = args.args.len();
|
|
if nargs < extra_positional + 1 {
|
|
return Err(vm.new_type_error(format!(
|
|
"{func_name}() takes at least {} argument{} ({nargs} given)",
|
|
extra_positional + 1,
|
|
if extra_positional == 0 { "" } else { "s" }
|
|
)));
|
|
}
|
|
if nargs > extra_positional + 2 {
|
|
return Err(vm.new_type_error(format!(
|
|
"{func_name}() takes at most {} arguments ({nargs} given)",
|
|
extra_positional + 2
|
|
)));
|
|
}
|
|
let tag = args.args[extra_positional].clone();
|
|
let positional_attrib = args.args.get(extra_positional + 1).cloned();
|
|
let attrib = match positional_attrib {
|
|
Some(obj) => {
|
|
let dict = obj.downcast::<PyDict>().map_err(|obj| {
|
|
vm.new_type_error(format!(
|
|
"{func_name}() argument {} must be dict, not {}",
|
|
extra_positional + 2,
|
|
obj.class().name()
|
|
))
|
|
})?;
|
|
let copied = dict.copy().into_ref(&vm.ctx);
|
|
for (key, value) in args.kwargs.drain(..) {
|
|
copied.set_item(&key, value, vm)?;
|
|
}
|
|
Some(copied)
|
|
}
|
|
None if !args.kwargs.is_empty() => {
|
|
let popped = args.kwargs.shift_remove("attrib");
|
|
let copied = match popped {
|
|
Some(obj) => {
|
|
let dict = obj.downcast::<PyDict>().map_err(|obj| {
|
|
vm.new_type_error(format!(
|
|
"attrib must be dict, not {}",
|
|
obj.class().name()
|
|
))
|
|
})?;
|
|
dict.copy().into_ref(&vm.ctx)
|
|
}
|
|
None => vm.ctx.new_dict(),
|
|
};
|
|
for (key, value) in args.kwargs.drain(..) {
|
|
copied.set_item(&key, value, vm)?;
|
|
}
|
|
Some(copied)
|
|
}
|
|
None => None,
|
|
};
|
|
Ok((tag, attrib))
|
|
}
|
|
|
|
#[derive(FromArgs)]
|
|
struct FindArgs {
|
|
#[pyarg(any)]
|
|
path: PyObjectRef,
|
|
#[pyarg(any, optional)]
|
|
namespaces: Option<PyObjectRef>,
|
|
}
|
|
|
|
#[derive(FromArgs)]
|
|
struct FindTextArgs {
|
|
#[pyarg(any)]
|
|
path: PyObjectRef,
|
|
#[pyarg(any, optional)]
|
|
default: Option<PyObjectRef>,
|
|
#[pyarg(any, optional)]
|
|
namespaces: Option<PyObjectRef>,
|
|
}
|
|
|
|
#[derive(FromArgs)]
|
|
struct GetArgs {
|
|
#[pyarg(any)]
|
|
key: PyObjectRef,
|
|
#[pyarg(any, optional)]
|
|
default: Option<PyObjectRef>,
|
|
}
|
|
|
|
#[derive(FromArgs)]
|
|
struct IterArgs {
|
|
#[pyarg(any, optional)]
|
|
tag: Option<PyObjectRef>,
|
|
}
|
|
|
|
#[derive(FromArgs)]
|
|
struct PiArgs {
|
|
#[pyarg(positional)]
|
|
target: PyObjectRef,
|
|
#[pyarg(positional, optional)]
|
|
text: Option<PyObjectRef>,
|
|
}
|
|
|
|
#[derive(FromArgs)]
|
|
struct SetEventsArgs {
|
|
#[pyarg(positional)]
|
|
events_queue: PyObjectRef,
|
|
#[pyarg(positional, optional)]
|
|
events_to_report: Option<PyObjectRef>,
|
|
}
|
|
|
|
impl Constructor for PyElement {
|
|
type Args = FuncArgs;
|
|
|
|
fn py_new(_cls: &Py<PyType>, _args: Self::Args, vm: &VirtualMachine) -> PyResult<Self> {
|
|
Ok(Self::new(vm.ctx.none(), None, vm))
|
|
}
|
|
}
|
|
|
|
impl Initializer for PyElement {
|
|
type Args = FuncArgs;
|
|
|
|
fn init(zelf: &Py<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
|
|
let (tag, attrib) = parse_attrib_args(args, "Element", 0, vm)?;
|
|
let attrib = attrib.filter(|d| !d.is_empty());
|
|
let _recycle = {
|
|
let mut inner = zelf.inner.write();
|
|
let old_attrib = if attrib.is_some() {
|
|
core::mem::replace(&mut inner.attrib, attrib)
|
|
} else {
|
|
None
|
|
};
|
|
(
|
|
core::mem::replace(&mut inner.tag, tag),
|
|
core::mem::replace(&mut inner.text, Joined::plain(vm.ctx.none())),
|
|
core::mem::replace(&mut inner.tail, Joined::plain(vm.ctx.none())),
|
|
old_attrib,
|
|
)
|
|
};
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[pyclass(
|
|
with(
|
|
Constructor,
|
|
Initializer,
|
|
AsMapping,
|
|
AsSequence,
|
|
AsNumber,
|
|
Representable
|
|
),
|
|
flags(BASETYPE, HAS_WEAKREF)
|
|
)]
|
|
impl PyElement {
|
|
#[pygetset]
|
|
fn tag(zelf: &Py<Self>) -> PyObjectRef {
|
|
zelf.inner.read().tag.clone()
|
|
}
|
|
|
|
#[pygetset(setter)]
|
|
fn set_tag(zelf: &Py<Self>, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
|
|
let value = setter_value(value, vm)?;
|
|
// Whatever is replaced may run __del__ on drop, and that code is
|
|
// free to touch this very element, so it must not be dropped
|
|
// while the lock is held (test_bpo_31728).
|
|
let _recycle = core::mem::replace(&mut zelf.inner.write().tag, value);
|
|
Ok(())
|
|
}
|
|
|
|
#[pygetset]
|
|
fn text(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult {
|
|
zelf.joined_get(false, vm)
|
|
}
|
|
|
|
#[pygetset(setter)]
|
|
fn set_text(zelf: &Py<Self>, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
|
|
let value = setter_value(value, vm)?;
|
|
let _recycle = core::mem::replace(&mut zelf.inner.write().text, Joined::plain(value));
|
|
Ok(())
|
|
}
|
|
|
|
#[pygetset]
|
|
fn tail(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult {
|
|
zelf.joined_get(true, vm)
|
|
}
|
|
|
|
#[pygetset(setter)]
|
|
fn set_tail(zelf: &Py<Self>, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
|
|
let value = setter_value(value, vm)?;
|
|
let _recycle = core::mem::replace(&mut zelf.inner.write().tail, Joined::plain(value));
|
|
Ok(())
|
|
}
|
|
|
|
#[pygetset]
|
|
fn attrib(zelf: &Py<Self>, vm: &VirtualMachine) -> PyDictRef {
|
|
zelf.attrib_or_new(vm)
|
|
}
|
|
|
|
#[pygetset(setter)]
|
|
fn set_attrib(zelf: &Py<Self>, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
|
|
let value = setter_value(value, vm)?;
|
|
let dict = value.downcast::<PyDict>().map_err(|obj| {
|
|
vm.new_type_error(format!("attrib must be dict, not {}", obj.class().name()))
|
|
})?;
|
|
let _recycle = zelf.inner.write().attrib.replace(dict);
|
|
Ok(())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn append(zelf: &Py<Self>, subelement: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
|
|
check_element(&subelement, vm)?;
|
|
zelf.inner.write().children.push(subelement);
|
|
Ok(())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn extend(zelf: &Py<Self>, elements: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
|
|
let items: Vec<PyObjectRef> = elements.try_to_value(vm)?;
|
|
for element in &items {
|
|
check_element(element, vm)?;
|
|
}
|
|
zelf.inner.write().children.extend(items);
|
|
Ok(())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn insert(
|
|
zelf: &Py<Self>,
|
|
index: isize,
|
|
subelement: PyObjectRef,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
check_element(&subelement, vm)?;
|
|
let mut inner = zelf.inner.write();
|
|
let len = inner.children.len();
|
|
let pos = if index < 0 {
|
|
let i = index + len as isize;
|
|
if i < 0 { 0 } else { i as usize }
|
|
} else {
|
|
(index as usize).min(len)
|
|
};
|
|
inner.children.insert(pos, subelement);
|
|
Ok(())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn remove(zelf: &Py<Self>, subelement: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
|
|
check_element(&subelement, vm)?;
|
|
// Identity first, then equality, the way `element_remove` does;
|
|
// the equality test can run arbitrary code, so the lock is
|
|
// dropped for it and the list re-read afterwards.
|
|
let mut found = None;
|
|
let mut i = 0;
|
|
loop {
|
|
let child = {
|
|
let inner = zelf.inner.read();
|
|
match inner.children.get(i) {
|
|
Some(child) => child.clone(),
|
|
None => break,
|
|
}
|
|
};
|
|
if child.is(&subelement) {
|
|
found = Some(i);
|
|
break;
|
|
}
|
|
if vm.bool_eq(&child, &subelement)? {
|
|
found = Some(i);
|
|
break;
|
|
}
|
|
i += 1;
|
|
}
|
|
let Some(i) = found else {
|
|
return Err(vm.new_value_error("Element.remove(x): element not found"));
|
|
};
|
|
let _recycle = {
|
|
let mut inner = zelf.inner.write();
|
|
(i < inner.children.len()).then(|| inner.children.remove(i))
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn clear(zelf: &Py<Self>, vm: &VirtualMachine) {
|
|
let _recycle = {
|
|
let mut inner = zelf.inner.write();
|
|
(
|
|
inner.attrib.take(),
|
|
core::mem::take(&mut inner.children),
|
|
core::mem::replace(&mut inner.text, Joined::plain(vm.ctx.none())),
|
|
core::mem::replace(&mut inner.tail, Joined::plain(vm.ctx.none())),
|
|
)
|
|
};
|
|
}
|
|
|
|
#[pymethod]
|
|
fn get(zelf: &Py<Self>, args: GetArgs, vm: &VirtualMachine) -> PyResult {
|
|
let default = args.default.unwrap_or_else(|| vm.ctx.none());
|
|
let Some(attrib) = zelf.attrib_opt() else {
|
|
return Ok(default);
|
|
};
|
|
Ok(attrib.get_item_opt(&*args.key, vm)?.unwrap_or(default))
|
|
}
|
|
|
|
#[pymethod]
|
|
fn set(
|
|
zelf: &Py<Self>,
|
|
key: PyObjectRef,
|
|
value: PyObjectRef,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
let attrib = zelf.attrib_or_new(vm);
|
|
attrib.set_item(&*key, value, vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn keys(zelf: &Py<Self>) -> Vec<PyObjectRef> {
|
|
let Some(attrib) = zelf.attrib_opt() else {
|
|
return vec![];
|
|
};
|
|
attrib.into_iter().map(|(k, _)| k).collect()
|
|
}
|
|
|
|
#[pymethod]
|
|
fn items(zelf: &Py<Self>, vm: &VirtualMachine) -> Vec<PyObjectRef> {
|
|
let Some(attrib) = zelf.attrib_opt() else {
|
|
return vec![];
|
|
};
|
|
attrib
|
|
.into_iter()
|
|
.map(|(k, v)| vm.ctx.new_tuple(vec![k, v]).into())
|
|
.collect()
|
|
}
|
|
|
|
#[pymethod]
|
|
fn makeelement(
|
|
zelf: &Py<Self>,
|
|
tag: PyObjectRef,
|
|
attrib: PyObjectRef,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult {
|
|
let attrib = attrib.downcast::<PyDict>().map_err(|obj| {
|
|
vm.new_type_error(format!(
|
|
"makeelement() argument 2 must be dict, not {}",
|
|
obj.class().name()
|
|
))
|
|
})?;
|
|
let attrib = attrib.copy().into_ref(&vm.ctx);
|
|
create_element(zelf.class().to_owned(), tag, Some(attrib), vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn find(zelf: &Py<Self>, args: FindArgs, vm: &VirtualMachine) -> PyResult {
|
|
let FindArgs { path, namespaces } = args;
|
|
let namespaces = namespaces.unwrap_or_else(|| vm.ctx.none());
|
|
if check_path(&path) || !vm.is_none(&namespaces) {
|
|
let state = module_state(vm)?;
|
|
let element_path = state.element_path(vm)?;
|
|
return vm.call_method(&element_path, "find", (zelf.to_owned(), path, namespaces));
|
|
}
|
|
let mut i = 0;
|
|
while let Some(child) = zelf.child(i) {
|
|
let tag = as_element(&child, vm)?.tag_obj();
|
|
if vm.bool_eq(&tag, &path)? {
|
|
return Ok(child);
|
|
}
|
|
i += 1;
|
|
}
|
|
Ok(vm.ctx.none())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn findtext(zelf: &Py<Self>, args: FindTextArgs, vm: &VirtualMachine) -> PyResult {
|
|
let FindTextArgs {
|
|
path,
|
|
default,
|
|
namespaces,
|
|
} = args;
|
|
let default = default.unwrap_or_else(|| vm.ctx.none());
|
|
let namespaces = namespaces.unwrap_or_else(|| vm.ctx.none());
|
|
if check_path(&path) || !vm.is_none(&namespaces) {
|
|
let state = module_state(vm)?;
|
|
let element_path = state.element_path(vm)?;
|
|
return vm.call_method(
|
|
&element_path,
|
|
"findtext",
|
|
(zelf.to_owned(), path, default, namespaces),
|
|
);
|
|
}
|
|
let mut i = 0;
|
|
while let Some(child) = zelf.child(i) {
|
|
let elem = as_element(&child, vm)?;
|
|
if vm.bool_eq(&elem.tag_obj(), &path)? {
|
|
let text = elem.joined_get(false, vm)?;
|
|
return Ok(if vm.is_none(&text) {
|
|
vm.ctx.new_str("").into()
|
|
} else {
|
|
text
|
|
});
|
|
}
|
|
i += 1;
|
|
}
|
|
Ok(default)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn findall(zelf: &Py<Self>, args: FindArgs, vm: &VirtualMachine) -> PyResult {
|
|
let FindArgs { path, namespaces } = args;
|
|
let namespaces = namespaces.unwrap_or_else(|| vm.ctx.none());
|
|
if check_path(&path) || !vm.is_none(&namespaces) {
|
|
let state = module_state(vm)?;
|
|
let element_path = state.element_path(vm)?;
|
|
return vm.call_method(
|
|
&element_path,
|
|
"findall",
|
|
(zelf.to_owned(), path, namespaces),
|
|
);
|
|
}
|
|
let mut out = Vec::new();
|
|
let mut i = 0;
|
|
while let Some(child) = zelf.child(i) {
|
|
let tag = as_element(&child, vm)?.tag_obj();
|
|
if vm.bool_eq(&tag, &path)? {
|
|
out.push(child);
|
|
}
|
|
i += 1;
|
|
}
|
|
Ok(vm.ctx.new_list(out).into())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn iterfind(zelf: &Py<Self>, args: FindArgs, vm: &VirtualMachine) -> PyResult {
|
|
let FindArgs { path, namespaces } = args;
|
|
let namespaces = namespaces.unwrap_or_else(|| vm.ctx.none());
|
|
let state = module_state(vm)?;
|
|
let element_path = state.element_path(vm)?;
|
|
vm.call_method(
|
|
&element_path,
|
|
"iterfind",
|
|
(zelf.to_owned(), path, namespaces),
|
|
)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn iter(zelf: PyRef<Self>, args: IterArgs, vm: &VirtualMachine) -> PyElementIter {
|
|
let tag = args.tag.unwrap_or_else(|| vm.ctx.none());
|
|
let tag = match tag.downcast_ref::<PyStr>() {
|
|
Some(s) if s.as_wtf8() == "*" => vm.ctx.none(),
|
|
_ => tag,
|
|
};
|
|
PyElementIter::new(zelf, tag, false)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn itertext(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyElementIter {
|
|
PyElementIter::new(zelf, vm.ctx.none(), true)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn __copy__(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult {
|
|
let (tag, attrib, text, tail, children) = {
|
|
let inner = zelf.inner.read();
|
|
(
|
|
inner.tag.clone(),
|
|
inner.attrib.clone(),
|
|
Joined {
|
|
obj: inner.text.obj.clone(),
|
|
pending: inner.text.pending,
|
|
},
|
|
Joined {
|
|
obj: inner.tail.obj.clone(),
|
|
pending: inner.tail.pending,
|
|
},
|
|
inner.children.clone(),
|
|
)
|
|
};
|
|
let new = create_element(zelf.class().to_owned(), tag, attrib, vm)?;
|
|
{
|
|
let elem = as_element(&new, vm)?;
|
|
let mut inner = elem.inner.write();
|
|
inner.text = text;
|
|
inner.tail = tail;
|
|
inner.children = children;
|
|
drop(inner);
|
|
}
|
|
Ok(new)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn __deepcopy__(zelf: &Py<Self>, memo: PyObjectRef, vm: &VirtualMachine) -> PyResult {
|
|
let memo = memo.downcast::<PyDict>().map_err(|obj| {
|
|
vm.new_type_error(format!(
|
|
"__deepcopy__() argument 1 must be dict, not {}",
|
|
obj.class().name()
|
|
))
|
|
})?;
|
|
deepcopy_element(zelf, &memo, vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn __getstate__(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyDictRef> {
|
|
let inner = zelf.inner.read();
|
|
let children = vm.ctx.new_list(inner.children.clone());
|
|
let attrib = inner.attrib.clone().unwrap_or_else(|| vm.ctx.new_dict());
|
|
let text = inner.text.obj.clone();
|
|
let tail = inner.tail.obj.clone();
|
|
let tag = inner.tag.clone();
|
|
drop(inner);
|
|
let state = vm.ctx.new_dict();
|
|
state.set_item("tag", tag, vm)?;
|
|
state.set_item("_children", children.into(), vm)?;
|
|
state.set_item("attrib", attrib.into(), vm)?;
|
|
state.set_item("text", text, vm)?;
|
|
state.set_item("tail", tail, vm)?;
|
|
Ok(state)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn __setstate__(zelf: &Py<Self>, state: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
|
|
let dict = state.downcast_ref::<PyDict>().ok_or_else(|| {
|
|
vm.new_type_error(format!(
|
|
"Don't know how to unpickle \"{}\" as an Element",
|
|
state.repr(vm).map(|s| s.to_string()).unwrap_or_default()
|
|
))
|
|
})?;
|
|
let tag = dict.get_item_opt("tag", vm)?;
|
|
let attrib = dict.get_item_opt("attrib", vm)?;
|
|
let text = dict.get_item_opt("text", vm)?;
|
|
let tail = dict.get_item_opt("tail", vm)?;
|
|
let children = dict.get_item_opt("_children", vm)?;
|
|
let Some(tag) = tag else {
|
|
return Err(vm.new_type_error("tag may not be NULL"));
|
|
};
|
|
let children = match children {
|
|
Some(children) => {
|
|
let list = children
|
|
.downcast_ref::<PyList>()
|
|
.ok_or_else(|| vm.new_type_error("'_children' is not a list"))?;
|
|
let children = list.borrow_vec().to_vec();
|
|
for child in &children {
|
|
check_element(child, vm)?;
|
|
}
|
|
Some(children)
|
|
}
|
|
None => None,
|
|
};
|
|
let attrib = match attrib {
|
|
Some(attrib) if !vm.is_none(&attrib) => {
|
|
Some(attrib.downcast::<PyDict>().map_err(|obj| {
|
|
vm.new_type_error(format!(
|
|
"attrib must be dict, not {}",
|
|
obj.class().name()
|
|
))
|
|
})?)
|
|
}
|
|
_ => None,
|
|
};
|
|
let new_text = match text {
|
|
Some(text) => {
|
|
let pending = text.downcastable::<PyList>();
|
|
Joined { obj: text, pending }
|
|
}
|
|
None => Joined::plain(vm.ctx.none()),
|
|
};
|
|
let new_tail = match tail {
|
|
Some(tail) => {
|
|
let pending = tail.downcastable::<PyList>();
|
|
Joined { obj: tail, pending }
|
|
}
|
|
None => Joined::plain(vm.ctx.none()),
|
|
};
|
|
let _recycle = {
|
|
let mut inner = zelf.inner.write();
|
|
let old_tag = core::mem::replace(&mut inner.tag, tag);
|
|
let old_text = core::mem::replace(&mut inner.text, new_text);
|
|
let old_tail = core::mem::replace(&mut inner.tail, new_tail);
|
|
let old_children = match children {
|
|
Some(children) => Some(core::mem::replace(&mut inner.children, children)),
|
|
None => None,
|
|
};
|
|
let old_attrib = if attrib.is_some() {
|
|
core::mem::replace(&mut inner.attrib, attrib)
|
|
} else {
|
|
None
|
|
};
|
|
(old_tag, old_text, old_tail, old_children, old_attrib)
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
fn _getitem(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult {
|
|
match sequence_index(vm, needle)? {
|
|
SequenceIndex::Int(i) => self
|
|
.inner
|
|
.read()
|
|
.children
|
|
.getitem_by_index(vm, i)
|
|
.map_err(|_| vm.new_index_error("child index out of range")),
|
|
SequenceIndex::Slice(slice) => {
|
|
let items = self.inner.read().children.getitem_by_slice(vm, slice)?;
|
|
Ok(vm.ctx.new_list(items).into())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn _setitem(
|
|
&self,
|
|
needle: &PyObject,
|
|
value: PyObjectRef,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
// `recycle` outlives the lock guard on purpose: dropping a
|
|
// displaced child can run __del__, which may come back into this
|
|
// element.
|
|
#[expect(clippy::collection_is_never_read, reason = "dropped after the lock")]
|
|
let mut recycle: Vec<PyObjectRef> = Vec::new();
|
|
match sequence_index(vm, needle)? {
|
|
SequenceIndex::Int(i) => {
|
|
check_element(&value, vm)?;
|
|
let mut inner = self.inner.write();
|
|
let pos = inner
|
|
.children
|
|
.wrap_index(i)
|
|
.ok_or_else(|| vm.new_index_error("child assignment index out of range"))?;
|
|
recycle.push(core::mem::replace(&mut inner.children[pos], value));
|
|
Ok(())
|
|
}
|
|
SequenceIndex::Slice(slice) => {
|
|
let items: Vec<PyObjectRef> = value.try_to_value(vm)?;
|
|
for item in &items {
|
|
check_element(item, vm)?;
|
|
}
|
|
let mut inner = self.inner.write();
|
|
let (range, step, slice_len) = slice.adjust_indices(inner.children.len());
|
|
if step == 1 {
|
|
recycle.extend(inner.children.splice(range, items));
|
|
Ok(())
|
|
} else if slice_len == items.len() {
|
|
let iter = crate::vm::sliceable::SaturatedSliceIter::from_adjust_indices(
|
|
range, step, slice_len,
|
|
);
|
|
for (pos, item) in iter.zip(items) {
|
|
recycle.push(core::mem::replace(&mut inner.children[pos], item));
|
|
}
|
|
Ok(())
|
|
} else {
|
|
Err(vm.new_value_error(format!(
|
|
"attempt to assign sequence of size {} to extended slice of size {}",
|
|
items.len(),
|
|
slice_len
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn _delitem(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
|
|
#[expect(clippy::collection_is_never_read, reason = "dropped after the lock")]
|
|
let mut recycle: Vec<PyObjectRef> = Vec::new();
|
|
match sequence_index(vm, needle)? {
|
|
SequenceIndex::Int(i) => {
|
|
let mut inner = self.inner.write();
|
|
let pos = inner
|
|
.children
|
|
.wrap_index(i)
|
|
.ok_or_else(|| vm.new_index_error("child assignment index out of range"))?;
|
|
recycle.push(inner.children.remove(pos));
|
|
Ok(())
|
|
}
|
|
SequenceIndex::Slice(slice) => {
|
|
let mut inner = self.inner.write();
|
|
let (range, step, slice_len) = slice.adjust_indices(inner.children.len());
|
|
if slice_len == 0 {
|
|
return Ok(());
|
|
}
|
|
let mut doomed: Vec<usize> =
|
|
crate::vm::sliceable::SaturatedSliceIter::from_adjust_indices(
|
|
range, step, slice_len,
|
|
)
|
|
.collect();
|
|
doomed.sort_unstable();
|
|
for pos in doomed.into_iter().rev() {
|
|
recycle.push(inner.children.remove(pos));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn setter_value(value: PySetterValue, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
|
|
match value {
|
|
PySetterValue::Assign(v) => Ok(v),
|
|
PySetterValue::Delete => Err(vm.new_attribute_error("can't delete element attribute")),
|
|
}
|
|
}
|
|
|
|
fn sequence_index(vm: &VirtualMachine, needle: &PyObject) -> PyResult<SequenceIndex> {
|
|
SequenceIndex::try_from_borrowed_object(vm, needle, "element")
|
|
.map_err(|_| vm.new_type_error("element indices must be integers"))
|
|
}
|
|
|
|
/// Instantiate `cls` (which may be an `Element` subclass) directly,
|
|
/// bypassing `__init__`, the way `create_new_element` does.
|
|
fn create_element(
|
|
cls: PyTypeRef,
|
|
tag: PyObjectRef,
|
|
attrib: Option<PyDictRef>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult {
|
|
let element = PyElement::new(tag, attrib, vm);
|
|
Ok(element.into_ref_with_type(vm, cls)?.into())
|
|
}
|
|
|
|
fn deepcopy_obj(obj: &PyObject, memo: &Py<PyDict>, vm: &VirtualMachine) -> PyResult {
|
|
if vm.is_none(obj) || obj.class().is(vm.ctx.types.str_type) {
|
|
return Ok(obj.to_owned());
|
|
}
|
|
if let Some(elem) = obj.downcast_ref::<PyElement>()
|
|
&& elem.class().is(PyElement::class(&vm.ctx))
|
|
{
|
|
return deepcopy_element(elem, memo, vm);
|
|
}
|
|
let state = module_state(vm)?;
|
|
state
|
|
.deepcopy(vm)?
|
|
.call((obj.to_owned(), memo.to_owned()), vm)
|
|
}
|
|
|
|
fn deepcopy_element(zelf: &Py<PyElement>, memo: &Py<PyDict>, vm: &VirtualMachine) -> PyResult {
|
|
// A chain of elements is copied by native recursion, so the depth
|
|
// has to be charged to the interpreter's recursion budget or a deep
|
|
// enough tree overflows the real stack instead of raising.
|
|
vm.with_recursion(" in Element.__deepcopy__", || {
|
|
deepcopy_element_inner(zelf, memo, vm)
|
|
})
|
|
}
|
|
|
|
fn deepcopy_element_inner(
|
|
zelf: &Py<PyElement>,
|
|
memo: &Py<PyDict>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult {
|
|
let (tag, attrib, text, text_pending, tail, tail_pending, children) = {
|
|
let inner = zelf.inner.read();
|
|
(
|
|
inner.tag.clone(),
|
|
inner.attrib.clone(),
|
|
inner.text.obj.clone(),
|
|
inner.text.pending,
|
|
inner.tail.obj.clone(),
|
|
inner.tail.pending,
|
|
inner.children.clone(),
|
|
)
|
|
};
|
|
let tag = deepcopy_obj(&tag, memo, vm)?;
|
|
let attrib = match attrib {
|
|
Some(attrib) => Some(
|
|
deepcopy_obj(attrib.as_object(), memo, vm)?
|
|
.downcast::<PyDict>()
|
|
.map_err(|_| vm.new_type_error("attrib must be dict"))?,
|
|
),
|
|
None => None,
|
|
};
|
|
let new = create_element(zelf.class().to_owned(), tag, attrib, vm)?;
|
|
let elem = as_element(&new, vm)?;
|
|
let text = deepcopy_obj(&text, memo, vm)?;
|
|
let tail = deepcopy_obj(&tail, memo, vm)?;
|
|
// Re-read the child list on every step instead of iterating a
|
|
// snapshot: a user __deepcopy__ is free to clear or grow the
|
|
// original while the copy is in progress, and the C accelerator
|
|
// follows the live list (test_deepcopy_clear/test_deepcopy_grow).
|
|
let _ = children;
|
|
let mut new_children: Vec<PyObjectRef> = Vec::new();
|
|
let mut i = 0;
|
|
loop {
|
|
let Some(child) = zelf.child(i) else { break };
|
|
let copied = deepcopy_obj(&child, memo, vm)?;
|
|
check_element(&copied, vm)?;
|
|
new_children.push(copied);
|
|
i += 1;
|
|
}
|
|
{
|
|
let mut inner = elem.inner.write();
|
|
inner.text = Joined {
|
|
obj: text,
|
|
pending: text_pending,
|
|
};
|
|
inner.tail = Joined {
|
|
obj: tail,
|
|
pending: tail_pending,
|
|
};
|
|
inner.children = new_children;
|
|
}
|
|
let key: PyObjectRef = vm.ctx.new_int(zelf.get_id()).into();
|
|
memo.set_item(&*key, new.clone(), vm)?;
|
|
Ok(new)
|
|
}
|
|
|
|
/// `checkpath`: does this look like an XPath expression rather than a
|
|
/// plain tag, in which case `find`/`findall` must go through
|
|
/// `ElementPath`?
|
|
fn check_path(tag: &PyObject) -> bool {
|
|
let bytes = if let Some(s) = tag.downcast_ref::<PyStr>() {
|
|
s.as_bytes()
|
|
} else if let Some(b) = tag.downcast_ref::<crate::vm::builtins::PyBytes>() {
|
|
b.as_bytes()
|
|
} else {
|
|
// Unknown type; might be a path expression.
|
|
return true;
|
|
};
|
|
if bytes.len() >= 3
|
|
&& bytes[0] == b'{'
|
|
&& (bytes[1] == b'}' || (bytes[1] == b'*' && bytes[2] == b'}'))
|
|
{
|
|
return true;
|
|
}
|
|
let mut check = true;
|
|
for &ch in bytes {
|
|
match ch {
|
|
b'{' => check = false,
|
|
b'}' => check = true,
|
|
b'/' | b'*' | b'[' | b'@' | b'.' if check => return true,
|
|
_ => {}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
impl AsMapping for PyElement {
|
|
fn as_mapping() -> &'static PyMappingMethods {
|
|
static AS_MAPPING: PyMappingMethods = PyMappingMethods {
|
|
length: atomic_func!(|mapping, _vm| Ok(PyElement::mapping_downcast(mapping).len())),
|
|
subscript: atomic_func!(
|
|
|mapping, needle, vm| PyElement::mapping_downcast(mapping)._getitem(needle, vm)
|
|
),
|
|
ass_subscript: atomic_func!(|mapping, needle, value, vm| {
|
|
let zelf = PyElement::mapping_downcast(mapping);
|
|
if let Some(value) = value {
|
|
zelf._setitem(needle, value, vm)
|
|
} else {
|
|
zelf._delitem(needle, vm)
|
|
}
|
|
}),
|
|
};
|
|
&AS_MAPPING
|
|
}
|
|
}
|
|
|
|
impl AsSequence for PyElement {
|
|
fn as_sequence() -> &'static PySequenceMethods {
|
|
static AS_SEQUENCE: PySequenceMethods = PySequenceMethods {
|
|
length: atomic_func!(|seq, _vm| Ok(PyElement::sequence_downcast(seq).len())),
|
|
item: atomic_func!(|seq, i, vm| {
|
|
let zelf = PyElement::sequence_downcast(seq);
|
|
zelf.child(
|
|
i.try_into()
|
|
.map_err(|_| vm.new_index_error("child index out of range"))?,
|
|
)
|
|
.ok_or_else(|| vm.new_index_error("child index out of range"))
|
|
}),
|
|
ass_item: atomic_func!(|seq, i, value, vm| {
|
|
let zelf = PyElement::sequence_downcast(seq);
|
|
let recycle = if let Some(value) = value {
|
|
check_element(&value, vm)?;
|
|
let mut inner = zelf.inner.write();
|
|
let pos = inner.children.wrap_index(i).ok_or_else(|| {
|
|
vm.new_index_error("child assignment index out of range")
|
|
})?;
|
|
core::mem::replace(&mut inner.children[pos], value)
|
|
} else {
|
|
let mut inner = zelf.inner.write();
|
|
let pos = inner.children.wrap_index(i).ok_or_else(|| {
|
|
vm.new_index_error("child assignment index out of range")
|
|
})?;
|
|
inner.children.remove(pos)
|
|
};
|
|
drop(recycle);
|
|
Ok(())
|
|
}),
|
|
..PySequenceMethods::NOT_IMPLEMENTED
|
|
};
|
|
&AS_SEQUENCE
|
|
}
|
|
}
|
|
|
|
impl AsNumber for PyElement {
|
|
fn as_number() -> &'static PyNumberMethods {
|
|
static AS_NUMBER: PyNumberMethods = PyNumberMethods {
|
|
boolean: Some(|number, vm| {
|
|
let zelf = number.obj.downcast_ref::<PyElement>().unwrap();
|
|
crate::vm::warn::warn(
|
|
vm.ctx
|
|
.new_str(
|
|
"Testing an element's truth value will always return True in \
|
|
future versions. Use specific 'len(elem)' or 'elem is not None' \
|
|
test instead.",
|
|
)
|
|
.into(),
|
|
Some(vm.ctx.exceptions.deprecation_warning.to_owned()),
|
|
1,
|
|
None,
|
|
vm,
|
|
)?;
|
|
Ok(!zelf.is_empty())
|
|
}),
|
|
..PyNumberMethods::NOT_IMPLEMENTED
|
|
};
|
|
&AS_NUMBER
|
|
}
|
|
}
|
|
|
|
impl Representable for PyElement {
|
|
fn repr_str(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
|
|
let tag = zelf.tag_obj();
|
|
Ok(format!(
|
|
"<Element {} at {:#x}>",
|
|
tag.repr(vm)?,
|
|
zelf.get_id()
|
|
))
|
|
}
|
|
}
|
|
|
|
#[pyfunction(name = "SubElement")]
|
|
fn sub_element(args: FuncArgs, vm: &VirtualMachine) -> PyResult {
|
|
let parent = args.args.first().cloned().ok_or_else(|| {
|
|
vm.new_type_error("SubElement() takes at least 2 arguments (0 given)")
|
|
})?;
|
|
let parent = parent.downcast::<PyElement>().map_err(|obj| {
|
|
vm.new_type_error(format!(
|
|
"SubElement() argument 1 must be xml.etree.ElementTree.Element, not {}",
|
|
obj.class().name()
|
|
))
|
|
})?;
|
|
let (tag, attrib) = parse_attrib_args(args, "SubElement", 1, vm)?;
|
|
let element = create_element(parent.class().to_owned(), tag, attrib, vm)?;
|
|
parent.push(element.clone());
|
|
Ok(element)
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct TreeBuilderState {
|
|
/// First element created; what `close()` hands back.
|
|
root: Option<PyObjectRef>,
|
|
/// Element currently open (`None` == CPython's `Py_None`).
|
|
this: Option<PyObjectRef>,
|
|
/// Most recently created element.
|
|
last: Option<PyObjectRef>,
|
|
/// Most recently *closed* node, i.e. the one character data now
|
|
/// belongs to as a tail rather than as text.
|
|
last_for_tail: Option<PyObjectRef>,
|
|
/// Character data seen since the last flush: a single string while
|
|
/// there is only one fragment, a list once there are more.
|
|
data: Option<PyObjectRef>,
|
|
stack: Vec<Option<PyObjectRef>>,
|
|
element_factory: Option<PyObjectRef>,
|
|
comment_factory: Option<PyObjectRef>,
|
|
pi_factory: Option<PyObjectRef>,
|
|
/// `.append` of the XMLPullParser event queue, once `_setevents`
|
|
/// has wired this builder up for event reporting.
|
|
events_append: Option<PyObjectRef>,
|
|
start_event: Option<PyObjectRef>,
|
|
end_event: Option<PyObjectRef>,
|
|
start_ns_event: Option<PyObjectRef>,
|
|
end_ns_event: Option<PyObjectRef>,
|
|
comment_event: Option<PyObjectRef>,
|
|
pi_event: Option<PyObjectRef>,
|
|
insert_comments: bool,
|
|
insert_pis: bool,
|
|
}
|
|
|
|
#[pyattr]
|
|
#[pyclass(
|
|
module = "xml.etree.ElementTree",
|
|
name = "TreeBuilder",
|
|
traverse = "manual"
|
|
)]
|
|
#[derive(Debug, PyPayload)]
|
|
pub(crate) struct PyTreeBuilder {
|
|
state: PyRwLock<TreeBuilderState>,
|
|
}
|
|
|
|
// SAFETY: every owned PyObjectRef held by the builder is visited.
|
|
unsafe impl crate::vm::object::Traverse for PyTreeBuilder {
|
|
fn traverse(&self, traverse_fn: &mut crate::vm::object::TraverseFn<'_>) {
|
|
let Some(st) = self.state.try_read() else {
|
|
return;
|
|
};
|
|
st.root.traverse(traverse_fn);
|
|
st.this.traverse(traverse_fn);
|
|
st.last.traverse(traverse_fn);
|
|
st.last_for_tail.traverse(traverse_fn);
|
|
st.data.traverse(traverse_fn);
|
|
st.stack.traverse(traverse_fn);
|
|
st.element_factory.traverse(traverse_fn);
|
|
st.comment_factory.traverse(traverse_fn);
|
|
st.pi_factory.traverse(traverse_fn);
|
|
st.events_append.traverse(traverse_fn);
|
|
st.start_event.traverse(traverse_fn);
|
|
st.end_event.traverse(traverse_fn);
|
|
st.start_ns_event.traverse(traverse_fn);
|
|
st.end_ns_event.traverse(traverse_fn);
|
|
st.comment_event.traverse(traverse_fn);
|
|
st.pi_event.traverse(traverse_fn);
|
|
}
|
|
|
|
fn clear(&mut self, out: &mut Vec<PyObjectRef>) {
|
|
let Some(mut st) = self.state.try_write() else {
|
|
return;
|
|
};
|
|
out.extend(st.stack.drain(..).flatten());
|
|
out.extend(
|
|
[
|
|
st.root.take(),
|
|
st.this.take(),
|
|
st.last.take(),
|
|
st.last_for_tail.take(),
|
|
st.data.take(),
|
|
st.element_factory.take(),
|
|
st.comment_factory.take(),
|
|
st.pi_factory.take(),
|
|
st.events_append.take(),
|
|
st.start_event.take(),
|
|
st.end_event.take(),
|
|
st.start_ns_event.take(),
|
|
st.end_ns_event.take(),
|
|
st.comment_event.take(),
|
|
st.pi_event.take(),
|
|
]
|
|
.into_iter()
|
|
.flatten(),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[derive(FromArgs)]
|
|
pub(crate) struct TreeBuilderArgs {
|
|
#[pyarg(any, default)]
|
|
element_factory: Option<PyObjectRef>,
|
|
#[pyarg(named, default)]
|
|
comment_factory: Option<PyObjectRef>,
|
|
#[pyarg(named, default)]
|
|
pi_factory: Option<PyObjectRef>,
|
|
#[pyarg(named, default)]
|
|
insert_comments: bool,
|
|
#[pyarg(named, default)]
|
|
insert_pis: bool,
|
|
}
|
|
|
|
impl DefaultConstructor for PyTreeBuilder {}
|
|
|
|
impl Default for PyTreeBuilder {
|
|
fn default() -> Self {
|
|
Self {
|
|
state: PyRwLock::new(TreeBuilderState::default()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Initializer for PyTreeBuilder {
|
|
type Args = TreeBuilderArgs;
|
|
|
|
fn init(zelf: &Py<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
|
|
let module = module_state(vm)?;
|
|
let element_factory = args.element_factory.filter(|f| !vm.is_none(f));
|
|
let comment_factory = match args.comment_factory {
|
|
Some(f) if !vm.is_none(&f) => Some(f),
|
|
// A `None` comment_factory means "use whatever
|
|
// `_set_factories` installed", not "no factory".
|
|
_ => module.comment_factory.read().clone(),
|
|
};
|
|
let pi_factory = match args.pi_factory {
|
|
Some(f) if !vm.is_none(&f) => Some(f),
|
|
_ => module.pi_factory.read().clone(),
|
|
};
|
|
let _recycle = {
|
|
let mut st = zelf.state.write();
|
|
st.insert_comments = comment_factory.is_some() && args.insert_comments;
|
|
st.insert_pis = pi_factory.is_some() && args.insert_pis;
|
|
(
|
|
core::mem::replace(&mut st.element_factory, element_factory),
|
|
core::mem::replace(&mut st.comment_factory, comment_factory),
|
|
core::mem::replace(&mut st.pi_factory, pi_factory),
|
|
)
|
|
};
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Append `child` to `element`, taking the direct route when the parent
|
|
/// really is one of our elements and falling back to `.append()` for
|
|
/// subclasses and foreign targets.
|
|
fn tb_add_subelement(
|
|
element: &PyObject,
|
|
child: PyObjectRef,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
if element.class().is(PyElement::class(&vm.ctx)) {
|
|
check_element(&child, vm)?;
|
|
element.downcast_ref::<PyElement>().unwrap().push(child);
|
|
Ok(())
|
|
} else {
|
|
vm.call_method(element, "append", (child,))?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Attach the buffered character data to `element` as its text or tail.
|
|
fn extend_text_or_tail(
|
|
element: &PyObject,
|
|
data: PyObjectRef,
|
|
is_tail: bool,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
if element.class().is(PyElement::class(&vm.ctx)) {
|
|
let elem = element.downcast_ref::<PyElement>().unwrap();
|
|
enum Fast {
|
|
Done,
|
|
Pending(PyObjectRef),
|
|
Slow,
|
|
}
|
|
let outcome = {
|
|
let mut inner = elem.inner.write();
|
|
let slot = if is_tail {
|
|
&mut inner.tail
|
|
} else {
|
|
&mut inner.text
|
|
};
|
|
if vm.is_none(&slot.obj) {
|
|
// Nothing there yet: adopt the fragment (or fragment
|
|
// list) as-is and let the join happen on first read.
|
|
let pending = data.downcastable::<PyList>();
|
|
slot.obj = data.clone();
|
|
slot.pending = pending;
|
|
Fast::Done
|
|
} else if slot.pending {
|
|
Fast::Pending(slot.obj.clone())
|
|
} else {
|
|
Fast::Slow
|
|
}
|
|
};
|
|
match outcome {
|
|
Fast::Done => return Ok(()),
|
|
Fast::Pending(list) => {
|
|
let list = list
|
|
.downcast::<PyList>()
|
|
.map_err(|_| vm.new_type_error("internal error (text)"))?;
|
|
if let Some(more) = data.downcast_ref::<PyList>() {
|
|
let items = more.borrow_vec().to_vec();
|
|
list.borrow_vec_mut().extend(items);
|
|
} else {
|
|
list.borrow_vec_mut().push(data);
|
|
}
|
|
return Ok(());
|
|
}
|
|
Fast::Slow => {}
|
|
}
|
|
}
|
|
let name = if is_tail { "tail" } else { "text" };
|
|
let previous = element.get_attr(name, vm)?;
|
|
let joined = list_join(&data, vm)?;
|
|
let joined = if vm.is_none(&previous) {
|
|
joined
|
|
} else {
|
|
vm._add(&previous, &joined)?
|
|
};
|
|
element.set_attr(name, joined, vm)?;
|
|
Ok(())
|
|
}
|
|
|
|
impl PyTreeBuilder {
|
|
fn append_event(
|
|
&self,
|
|
action: Option<PyObjectRef>,
|
|
node: PyObjectRef,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
let Some(action) = action else { return Ok(()) };
|
|
let append = self.state.read().events_append.clone();
|
|
let Some(append) = append else { return Ok(()) };
|
|
let event = vm.ctx.new_tuple(vec![action, node]);
|
|
append.call((event,), vm)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn flush_data(&self, vm: &VirtualMachine) -> PyResult<()> {
|
|
let Some((data, target, is_tail)) = ({
|
|
let mut st = self.state.write();
|
|
match st.data.take() {
|
|
None => None,
|
|
Some(data) => match st.last_for_tail.clone() {
|
|
Some(target) => Some((data, target, true)),
|
|
None => st.last.clone().map(|target| (data, target, false)),
|
|
},
|
|
}
|
|
}) else {
|
|
return Ok(());
|
|
};
|
|
extend_text_or_tail(&target, data, is_tail, vm)
|
|
}
|
|
|
|
pub(crate) fn handle_start(
|
|
&self,
|
|
tag: PyObjectRef,
|
|
attrib: Option<PyDictRef>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult {
|
|
self.flush_data(vm)?;
|
|
let factory = self.state.read().element_factory.clone();
|
|
let node = match factory {
|
|
// CPython adopts the caller's dict rather than copying it.
|
|
None => PyElement::new(tag, attrib, vm).into_ref(&vm.ctx).into(),
|
|
Some(factory) => {
|
|
let attrib = attrib.unwrap_or_else(|| vm.ctx.new_dict());
|
|
factory.call((tag, attrib), vm)?
|
|
}
|
|
};
|
|
let this = {
|
|
let mut st = self.state.write();
|
|
st.last_for_tail = None;
|
|
st.this.clone()
|
|
};
|
|
match &this {
|
|
Some(this) => tb_add_subelement(this, node.clone(), vm)?,
|
|
None => {
|
|
let mut st = self.state.write();
|
|
if st.root.is_some() {
|
|
drop(st);
|
|
return Err(new_parse_error(
|
|
"multiple elements on top level",
|
|
None,
|
|
None,
|
|
vm,
|
|
)?);
|
|
}
|
|
st.root = Some(node.clone());
|
|
}
|
|
}
|
|
let start_event = {
|
|
let mut st = self.state.write();
|
|
st.stack.push(this);
|
|
st.this = Some(node.clone());
|
|
st.last = Some(node.clone());
|
|
st.start_event.clone()
|
|
};
|
|
self.append_event(start_event, node.clone(), vm)?;
|
|
Ok(node)
|
|
}
|
|
|
|
pub(crate) fn handle_data(&self, data: PyObjectRef, vm: &VirtualMachine) {
|
|
let existing = {
|
|
let mut st = self.state.write();
|
|
match st.data.take() {
|
|
None => {
|
|
if st.last.is_none() {
|
|
// Data before the first start tag is dropped.
|
|
return;
|
|
}
|
|
st.data = Some(data);
|
|
return;
|
|
}
|
|
Some(existing) => existing,
|
|
}
|
|
};
|
|
// Growing the fragment list touches only the list's own lock.
|
|
if let Some(list) = existing.downcast_ref::<PyList>() {
|
|
list.borrow_vec_mut().push(data);
|
|
self.state.write().data = Some(existing);
|
|
} else {
|
|
let list = vm.ctx.new_list(vec![existing, data]);
|
|
self.state.write().data = Some(list.into());
|
|
}
|
|
}
|
|
|
|
pub(crate) fn handle_end(&self, vm: &VirtualMachine) -> PyResult {
|
|
self.flush_data(vm)?;
|
|
let (last, end_event) = {
|
|
let mut st = self.state.write();
|
|
let Some(parent) = st.stack.pop() else {
|
|
return Err(vm.new_index_error("pop from empty stack"));
|
|
};
|
|
let closed = st.this.take();
|
|
st.last.clone_from(&closed);
|
|
st.last_for_tail.clone_from(&closed);
|
|
st.this = parent;
|
|
(closed, st.end_event.clone())
|
|
};
|
|
let last = last.unwrap_or_else(|| vm.ctx.none());
|
|
self.append_event(end_event, last.clone(), vm)?;
|
|
Ok(last)
|
|
}
|
|
|
|
pub(crate) fn handle_comment(&self, text: PyObjectRef, vm: &VirtualMachine) -> PyResult {
|
|
self.flush_data(vm)?;
|
|
let factory = self.state.read().comment_factory.clone();
|
|
let comment = match factory {
|
|
Some(factory) => {
|
|
let comment = factory.call((text,), vm)?;
|
|
let (insert, this) = {
|
|
let st = self.state.read();
|
|
(st.insert_comments, st.this.clone())
|
|
};
|
|
if insert && let Some(this) = this {
|
|
tb_add_subelement(&this, comment.clone(), vm)?;
|
|
self.state.write().last_for_tail = Some(comment.clone());
|
|
}
|
|
comment
|
|
}
|
|
None => text,
|
|
};
|
|
let event = self.state.read().comment_event.clone();
|
|
self.append_event(event, comment.clone(), vm)?;
|
|
Ok(comment)
|
|
}
|
|
|
|
pub(crate) fn handle_pi(
|
|
&self,
|
|
target: PyObjectRef,
|
|
text: PyObjectRef,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult {
|
|
self.flush_data(vm)?;
|
|
let factory = self.state.read().pi_factory.clone();
|
|
let pi = match factory {
|
|
Some(factory) => {
|
|
let pi = factory.call((target, text), vm)?;
|
|
let (insert, this) = {
|
|
let st = self.state.read();
|
|
(st.insert_pis, st.this.clone())
|
|
};
|
|
if insert && let Some(this) = this {
|
|
tb_add_subelement(&this, pi.clone(), vm)?;
|
|
self.state.write().last_for_tail = Some(pi.clone());
|
|
}
|
|
pi
|
|
}
|
|
None => vm.ctx.new_tuple(vec![target, text]).into(),
|
|
};
|
|
let event = self.state.read().pi_event.clone();
|
|
self.append_event(event, pi.clone(), vm)?;
|
|
Ok(pi)
|
|
}
|
|
|
|
pub(crate) fn handle_start_ns(
|
|
&self,
|
|
prefix: PyObjectRef,
|
|
uri: PyObjectRef,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
let event = self.state.read().start_ns_event.clone();
|
|
if event.is_none() {
|
|
return Ok(());
|
|
}
|
|
let parcel = vm.ctx.new_tuple(vec![prefix, uri]);
|
|
self.append_event(event, parcel.into(), vm)
|
|
}
|
|
|
|
pub(crate) fn handle_end_ns(
|
|
&self,
|
|
prefix: PyObjectRef,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
let event = self.state.read().end_ns_event.clone();
|
|
if event.is_none() {
|
|
return Ok(());
|
|
}
|
|
self.append_event(event, prefix, vm)
|
|
}
|
|
|
|
fn done(&self, vm: &VirtualMachine) -> PyObjectRef {
|
|
self.state
|
|
.read()
|
|
.root
|
|
.clone()
|
|
.unwrap_or_else(|| vm.ctx.none())
|
|
}
|
|
|
|
fn set_events(
|
|
&self,
|
|
events_append: PyObjectRef,
|
|
events_to_report: &PyObject,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
{
|
|
let mut st = self.state.write();
|
|
st.events_append = Some(events_append);
|
|
st.start_event = None;
|
|
st.end_event = None;
|
|
st.start_ns_event = None;
|
|
st.end_ns_event = None;
|
|
st.comment_event = None;
|
|
st.pi_event = None;
|
|
if vm.is_none(events_to_report) {
|
|
st.end_event = Some(vm.ctx.new_str("end").into());
|
|
return Ok(());
|
|
}
|
|
}
|
|
let events: Vec<PyObjectRef> = events_to_report
|
|
.try_to_value(vm)
|
|
.map_err(|_| vm.new_type_error("events must be a sequence"))?;
|
|
for event in events {
|
|
let name = if let Some(s) = event.downcast_ref::<PyStr>() {
|
|
s.as_bytes().to_vec()
|
|
} else if let Some(b) = event.downcast_ref::<crate::vm::builtins::PyBytes>() {
|
|
b.as_bytes().to_vec()
|
|
} else {
|
|
return Err(vm.new_value_error("invalid events sequence"));
|
|
};
|
|
let mut st = self.state.write();
|
|
match name.as_slice() {
|
|
b"start" => st.start_event = Some(event),
|
|
b"end" => st.end_event = Some(event),
|
|
b"start-ns" => st.start_ns_event = Some(event),
|
|
b"end-ns" => st.end_ns_event = Some(event),
|
|
b"comment" => st.comment_event = Some(event),
|
|
b"pi" => st.pi_event = Some(event),
|
|
_ => {
|
|
drop(st);
|
|
return Err(
|
|
vm.new_value_error(format!("unknown event {}", event.repr(vm)?))
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn wants_pi(&self) -> bool {
|
|
let st = self.state.read();
|
|
st.insert_pis || (st.events_append.is_some() && st.pi_event.is_some())
|
|
}
|
|
}
|
|
|
|
#[pyclass(with(Constructor, Initializer), flags(BASETYPE, HAS_WEAKREF))]
|
|
impl PyTreeBuilder {
|
|
#[pymethod]
|
|
fn start(
|
|
zelf: &Py<Self>,
|
|
tag: PyObjectRef,
|
|
attrs: PyObjectRef,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult {
|
|
let attrs = attrs.downcast::<PyDict>().map_err(|obj| {
|
|
vm.new_type_error(format!(
|
|
"start() argument 2 must be dict, not {}",
|
|
obj.class().name()
|
|
))
|
|
})?;
|
|
zelf.handle_start(tag, Some(attrs), vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn data(zelf: &Py<Self>, data: PyObjectRef, vm: &VirtualMachine) {
|
|
zelf.handle_data(data, vm);
|
|
}
|
|
|
|
#[pymethod]
|
|
fn end(zelf: &Py<Self>, _tag: PyObjectRef, vm: &VirtualMachine) -> PyResult {
|
|
zelf.handle_end(vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn comment(zelf: &Py<Self>, text: PyObjectRef, vm: &VirtualMachine) -> PyResult {
|
|
zelf.handle_comment(text, vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn pi(zelf: &Py<Self>, args: PiArgs, vm: &VirtualMachine) -> PyResult {
|
|
zelf.handle_pi(args.target, args.text.unwrap_or_else(|| vm.ctx.none()), vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn close(zelf: &Py<Self>, vm: &VirtualMachine) -> PyObjectRef {
|
|
zelf.done(vm)
|
|
}
|
|
}
|
|
|
|
fn new_parse_error(
|
|
message: &str,
|
|
code: Option<i32>,
|
|
position: Option<(i64, i64)>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<crate::vm::builtins::PyBaseExceptionRef> {
|
|
let state = module_state(vm)?;
|
|
let cls = state
|
|
.parse_error
|
|
.read()
|
|
.clone()
|
|
.ok_or_else(|| vm.new_runtime_error("ParseError is missing"))?;
|
|
let exc = vm.invoke_exception(&cls, vec![vm.ctx.new_str(message).into()])?;
|
|
exc.as_object()
|
|
.set_attr("code", vm.ctx.new_int(code.unwrap_or(0)), vm)?;
|
|
let (line, column) = position.unwrap_or((0, 0));
|
|
exc.as_object().set_attr(
|
|
"position",
|
|
vm.ctx.new_tuple(vec![
|
|
vm.ctx.new_int(line).into(),
|
|
vm.ctx.new_int(column).into(),
|
|
]),
|
|
vm,
|
|
)?;
|
|
Ok(exc)
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct XMLParserState {
|
|
/// The `pyexpat.xmlparser` doing the actual scanning, or `None`
|
|
/// before `__init__` (and after `close()` dropped it).
|
|
parser: Option<PyObjectRef>,
|
|
/// The target again, when it is exactly our own `TreeBuilder`, so a
|
|
/// per-event dispatch is one clone rather than a type check.
|
|
native_target: Option<PyRef<PyTreeBuilder>>,
|
|
/// Cache of raw expat names to their `{uri}local` form.
|
|
names: Option<PyDictRef>,
|
|
handle_start: Option<PyObjectRef>,
|
|
handle_end: Option<PyObjectRef>,
|
|
handle_data: Option<PyObjectRef>,
|
|
handle_comment: Option<PyObjectRef>,
|
|
handle_pi: Option<PyObjectRef>,
|
|
handle_start_ns: Option<PyObjectRef>,
|
|
handle_end_ns: Option<PyObjectRef>,
|
|
handle_close: Option<PyObjectRef>,
|
|
handle_doctype: Option<PyObjectRef>,
|
|
}
|
|
|
|
#[pyattr]
|
|
#[pyclass(
|
|
module = "xml.etree.ElementTree",
|
|
name = "XMLParser",
|
|
traverse = "manual"
|
|
)]
|
|
#[derive(Debug, PyPayload)]
|
|
pub(crate) struct PyXMLParser {
|
|
#[pymember]
|
|
entity: crate::vm::object::PyAtomicRef<Option<PyObject>>,
|
|
#[pymember]
|
|
target: crate::vm::object::PyAtomicRef<Option<PyObject>>,
|
|
state: PyRwLock<XMLParserState>,
|
|
}
|
|
|
|
// SAFETY: every owned PyObjectRef held by the parser is visited.
|
|
unsafe impl crate::vm::object::Traverse for PyXMLParser {
|
|
fn traverse(&self, traverse_fn: &mut crate::vm::object::TraverseFn<'_>) {
|
|
let Some(st) = self.state.try_read() else {
|
|
return;
|
|
};
|
|
st.parser.traverse(traverse_fn);
|
|
st.native_target.traverse(traverse_fn);
|
|
self.entity.traverse(traverse_fn);
|
|
self.target.traverse(traverse_fn);
|
|
st.names.traverse(traverse_fn);
|
|
st.handle_start.traverse(traverse_fn);
|
|
st.handle_end.traverse(traverse_fn);
|
|
st.handle_data.traverse(traverse_fn);
|
|
st.handle_comment.traverse(traverse_fn);
|
|
st.handle_pi.traverse(traverse_fn);
|
|
st.handle_start_ns.traverse(traverse_fn);
|
|
st.handle_end_ns.traverse(traverse_fn);
|
|
st.handle_close.traverse(traverse_fn);
|
|
st.handle_doctype.traverse(traverse_fn);
|
|
}
|
|
|
|
fn clear(&mut self, out: &mut Vec<PyObjectRef>) {
|
|
let Some(mut st) = self.state.try_write() else {
|
|
return;
|
|
};
|
|
out.extend(
|
|
[
|
|
st.parser.take(),
|
|
unsafe { self.entity.swap(None) },
|
|
unsafe { self.target.swap(None) },
|
|
st.native_target.take().map(Into::into),
|
|
st.names.take().map(Into::into),
|
|
st.handle_start.take(),
|
|
st.handle_end.take(),
|
|
st.handle_data.take(),
|
|
st.handle_comment.take(),
|
|
st.handle_pi.take(),
|
|
st.handle_start_ns.take(),
|
|
st.handle_end_ns.take(),
|
|
st.handle_close.take(),
|
|
st.handle_doctype.take(),
|
|
]
|
|
.into_iter()
|
|
.flatten(),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[derive(FromArgs)]
|
|
pub(crate) struct XMLParserArgs {
|
|
#[pyarg(named, default)]
|
|
target: Option<PyObjectRef>,
|
|
#[pyarg(named, default)]
|
|
encoding: Option<PyObjectRef>,
|
|
}
|
|
|
|
impl DefaultConstructor for PyXMLParser {}
|
|
|
|
impl Default for PyXMLParser {
|
|
fn default() -> Self {
|
|
Self {
|
|
entity: crate::vm::object::PyAtomicRef::from(None),
|
|
target: crate::vm::object::PyAtomicRef::from(None),
|
|
state: PyRwLock::new(XMLParserState::default()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `getattr(target, name)`, treating a missing attribute as "the target
|
|
/// does not implement this callback" rather than an error.
|
|
fn optional_handler(
|
|
target: &PyObject,
|
|
name: &'static str,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<Option<PyObjectRef>> {
|
|
vm.get_attribute_opt(target, name)
|
|
}
|
|
|
|
impl Initializer for PyXMLParser {
|
|
type Args = XMLParserArgs;
|
|
|
|
fn init(zelf: &Py<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
|
|
let encoding = match args.encoding {
|
|
Some(e) if !vm.is_none(&e) => {
|
|
if !e.downcastable::<PyStr>() {
|
|
return Err(vm.new_type_error(format!(
|
|
"XMLParser() argument 'encoding' must be str or None, not {}",
|
|
e.class().name()
|
|
)));
|
|
}
|
|
e
|
|
}
|
|
_ => vm.ctx.none(),
|
|
};
|
|
let expat = vm.import("pyexpat", 0)?;
|
|
// The "}" namespace separator makes expat report names as
|
|
// "uri}local", which `makeuniversal` turns into "{uri}local".
|
|
let parser = vm.call_method(&expat, "ParserCreate", (encoding, "}"))?;
|
|
parser.set_attr("buffer_text", vm.ctx.new_int(1), vm)?;
|
|
parser.set_attr("ordered_attributes", vm.ctx.new_int(1), vm)?;
|
|
|
|
let target = match args.target {
|
|
Some(t) if !vm.is_none(&t) => t,
|
|
_ => PyTreeBuilder::default().into_ref(&vm.ctx).into(),
|
|
};
|
|
|
|
let entity = vm.ctx.new_dict();
|
|
// Lookups can fail. Build the whole state before publishing it, so a
|
|
// failed re-init leaves the previous parser, entity, and target in place.
|
|
let handlers = XMLParserState {
|
|
parser: Some(parser.clone()),
|
|
names: Some(vm.ctx.new_dict()),
|
|
handle_start_ns: optional_handler(&target, "start_ns", vm)?,
|
|
handle_end_ns: optional_handler(&target, "end_ns", vm)?,
|
|
handle_start: optional_handler(&target, "start", vm)?,
|
|
handle_data: optional_handler(&target, "data", vm)?,
|
|
handle_end: optional_handler(&target, "end", vm)?,
|
|
handle_comment: optional_handler(&target, "comment", vm)?,
|
|
handle_pi: optional_handler(&target, "pi", vm)?,
|
|
handle_close: optional_handler(&target, "close", vm)?,
|
|
handle_doctype: optional_handler(&target, "doctype", vm)?,
|
|
native_target: target
|
|
.class()
|
|
.is(PyTreeBuilder::class(&vm.ctx))
|
|
.then(|| target.clone().downcast::<PyTreeBuilder>().ok())
|
|
.flatten(),
|
|
};
|
|
let has_comment = handlers.handle_comment.is_some();
|
|
let has_pi = handlers.handle_pi.is_some();
|
|
let has_ns = handlers.handle_start_ns.is_some() || handlers.handle_end_ns.is_some();
|
|
*zelf.state.write() = handlers;
|
|
// SAFETY: `swap` returns the previous object. `_previous` holds it
|
|
// until `init` returns, so a `deref` of the old pointer stays valid
|
|
// for the rest of this call.
|
|
let _previous = unsafe { zelf.entity.swap(Some(entity.into())) };
|
|
let _previous = unsafe { zelf.target.swap(Some(target)) };
|
|
|
|
// Expat calls back into these; they dispatch straight to the
|
|
// target (and, when it is our own TreeBuilder, straight into
|
|
// Rust) without a Python frame in between.
|
|
let this = zelf.as_object();
|
|
parser.set_attr(
|
|
"StartElementHandler",
|
|
this.get_attr("_expat_start", vm)?,
|
|
vm,
|
|
)?;
|
|
parser.set_attr("EndElementHandler", this.get_attr("_expat_end", vm)?, vm)?;
|
|
parser.set_attr(
|
|
"CharacterDataHandler",
|
|
this.get_attr("_expat_data", vm)?,
|
|
vm,
|
|
)?;
|
|
if has_comment {
|
|
parser.set_attr("CommentHandler", this.get_attr("_expat_comment", vm)?, vm)?;
|
|
}
|
|
if has_pi {
|
|
parser.set_attr(
|
|
"ProcessingInstructionHandler",
|
|
this.get_attr("_expat_pi", vm)?,
|
|
vm,
|
|
)?;
|
|
}
|
|
if has_ns {
|
|
install_ns_handlers(zelf.as_object(), &parser, vm)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Namespace declarations are only reported when someone asked for
|
|
/// them, either by giving the target `start_ns`/`end_ns` methods or by
|
|
/// requesting the `start-ns`/`end-ns` pull-parser events.
|
|
fn install_ns_handlers(
|
|
this: &PyObject,
|
|
parser: &PyObject,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
parser.set_attr(
|
|
"StartNamespaceDeclHandler",
|
|
this.get_attr("_expat_start_ns", vm)?,
|
|
vm,
|
|
)?;
|
|
parser.set_attr(
|
|
"EndNamespaceDeclHandler",
|
|
this.get_attr("_expat_end_ns", vm)?,
|
|
vm,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
impl PyXMLParser {
|
|
fn check(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
|
|
let st = self.state.read();
|
|
if self.target.deref().is_none() {
|
|
return Err(vm.new_value_error("XMLParser.__init__() wasn't called"));
|
|
}
|
|
st.parser
|
|
.clone()
|
|
.ok_or_else(|| vm.new_value_error("XMLParser.__init__() wasn't called"))
|
|
}
|
|
|
|
/// The target, when it is exactly our own `TreeBuilder` and can
|
|
/// therefore be driven without going back through Python.
|
|
fn native_target(&self, _vm: &VirtualMachine) -> Option<PyRef<PyTreeBuilder>> {
|
|
self.state.read().native_target.clone()
|
|
}
|
|
|
|
/// Turn expat's "uri}local" into ElementTree's "{uri}local",
|
|
/// memoized so a repeated tag costs one dictionary hit.
|
|
fn make_universal(&self, name: PyObjectRef, vm: &VirtualMachine) -> PyResult {
|
|
let names = self.state.read().names.clone();
|
|
let Some(names) = names else { return Ok(name) };
|
|
if let Some(cached) = names.get_item_opt(&*name, vm)? {
|
|
return Ok(cached);
|
|
}
|
|
let value = match name.downcast_ref::<PyStr>() {
|
|
Some(s) if s.as_bytes().contains(&b'}') => {
|
|
vm.ctx.new_str(format!("{{{}", s.as_wtf8())).into()
|
|
}
|
|
_ => name.clone(),
|
|
};
|
|
names.set_item(&*name, value.clone(), vm)?;
|
|
Ok(value)
|
|
}
|
|
|
|
fn raise_expat_error(&self, err: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
|
|
let message = err.str(vm)?;
|
|
let code = err
|
|
.get_attr("code", vm)
|
|
.ok()
|
|
.and_then(|c| i32::try_from_object(vm, c).ok());
|
|
let lineno = err
|
|
.get_attr("lineno", vm)
|
|
.ok()
|
|
.and_then(|c| i64::try_from_object(vm, c).ok());
|
|
let offset = err
|
|
.get_attr("offset", vm)
|
|
.ok()
|
|
.and_then(|c| i64::try_from_object(vm, c).ok());
|
|
let position = match (lineno, offset) {
|
|
(Some(l), Some(o)) => Some((l, o)),
|
|
_ => None,
|
|
};
|
|
Err(new_parse_error(message.as_ref(), code, position, vm)?)
|
|
}
|
|
|
|
/// Feed one chunk through expat, translating its `ExpatError` into
|
|
/// the `ParseError` callers of `xml.etree` expect.
|
|
fn expat_parse(
|
|
&self,
|
|
data: PyObjectRef,
|
|
final_: bool,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
let parser = self.check(vm)?;
|
|
match vm.call_method(&parser, "Parse", (data, final_)) {
|
|
Ok(_) => Ok(()),
|
|
Err(e) => {
|
|
let expat = vm.import("pyexpat", 0)?;
|
|
let error_type = expat.get_attr("error", vm)?;
|
|
if let Ok(error_type) = error_type.downcast::<PyType>()
|
|
&& e.fast_isinstance(&error_type)
|
|
{
|
|
self.raise_expat_error(e.as_object(), vm)?;
|
|
unreachable!()
|
|
}
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[pyclass(with(Constructor, Initializer), flags(BASETYPE, HAS_WEAKREF))]
|
|
impl PyXMLParser {
|
|
#[pygetset]
|
|
fn version(_zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
|
|
let expat = vm.import("pyexpat", 0)?;
|
|
let info: Vec<PyObjectRef> = expat.get_attr("version_info", vm)?.try_to_value(vm)?;
|
|
let part = |i: usize| -> String {
|
|
info.get(i)
|
|
.map(|o| o.str(vm).map(|s| s.to_string()).unwrap_or_default())
|
|
.unwrap_or_default()
|
|
};
|
|
Ok(format!("Expat {}.{}.{}", part(0), part(1), part(2)))
|
|
}
|
|
|
|
#[pymethod]
|
|
fn feed(zelf: &Py<Self>, data: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
|
|
zelf.expat_parse(data, false, vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn close(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult {
|
|
zelf.expat_parse(vm.ctx.new_bytes(vec![]).into(), true, vm)?;
|
|
if let Some(builder) = zelf.native_target(vm) {
|
|
return Ok(builder.done(vm));
|
|
}
|
|
let close = zelf.state.read().handle_close.clone();
|
|
match close {
|
|
Some(close) => close.call((), vm),
|
|
None => Ok(vm.ctx.none()),
|
|
}
|
|
}
|
|
|
|
#[pymethod]
|
|
fn flush(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<()> {
|
|
// This backend has no reparse deferral to turn off, so, like the
|
|
// C accelerator built against an expat without
|
|
// XML_SetReparseDeferralEnabled, there is nothing to flush.
|
|
zelf.check(vm)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn _parse_whole(zelf: &Py<Self>, file: PyObjectRef, vm: &VirtualMachine) -> PyResult {
|
|
zelf.check(vm)?;
|
|
let read = file.get_attr("read", vm)?;
|
|
loop {
|
|
let buffer = read.call((64 * 1024,), vm)?;
|
|
if let Some(s) = buffer.downcast_ref::<PyStr>() {
|
|
if s.is_empty() {
|
|
break;
|
|
}
|
|
} else if let Some(b) = buffer.downcast_ref::<crate::vm::builtins::PyBytes>() {
|
|
if b.as_bytes().is_empty() {
|
|
break;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
zelf.expat_parse(buffer, false, vm)?;
|
|
}
|
|
zelf.expat_parse(vm.ctx.new_bytes(vec![]).into(), true, vm)?;
|
|
if let Some(builder) = zelf.native_target(vm) {
|
|
return Ok(builder.done(vm));
|
|
}
|
|
Ok(vm.ctx.none())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn _setevents(zelf: &Py<Self>, args: SetEventsArgs, vm: &VirtualMachine) -> PyResult<()> {
|
|
let SetEventsArgs {
|
|
events_queue,
|
|
events_to_report,
|
|
} = args;
|
|
let parser = zelf.check(vm)?;
|
|
let Some(builder) = zelf.native_target(vm) else {
|
|
return Err(vm.new_type_error(
|
|
"event handling only supported for ElementTree.TreeBuilder targets",
|
|
));
|
|
};
|
|
let append = events_queue.get_attr("append", vm)?;
|
|
builder.set_events(
|
|
append,
|
|
&events_to_report.unwrap_or_else(|| vm.ctx.none()),
|
|
vm,
|
|
)?;
|
|
// Comments and processing instructions are only reported once
|
|
// asked for, so their handlers are installed lazily here.
|
|
let this = zelf.as_object();
|
|
parser.set_attr("CommentHandler", this.get_attr("_expat_comment", vm)?, vm)?;
|
|
parser.set_attr(
|
|
"ProcessingInstructionHandler",
|
|
this.get_attr("_expat_pi", vm)?,
|
|
vm,
|
|
)?;
|
|
install_ns_handlers(this, &parser, vm)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn _expat_start(
|
|
zelf: &Py<Self>,
|
|
tag: PyObjectRef,
|
|
attr_list: PyObjectRef,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
let tag = zelf.make_universal(tag, vm)?;
|
|
let items = attr_list
|
|
.downcast_ref::<PyList>()
|
|
.map(|l| l.borrow_vec().to_vec())
|
|
.unwrap_or_default();
|
|
let attrib = if items.is_empty() {
|
|
None
|
|
} else {
|
|
let dict = vm.ctx.new_dict();
|
|
let mut i = 0;
|
|
while i + 1 < items.len() {
|
|
let key = zelf.make_universal(items[i].clone(), vm)?;
|
|
dict.set_item(&*key, items[i + 1].clone(), vm)?;
|
|
i += 2;
|
|
}
|
|
Some(dict)
|
|
};
|
|
if let Some(builder) = zelf.native_target(vm) {
|
|
builder.handle_start(tag, attrib, vm)?;
|
|
return Ok(());
|
|
}
|
|
let handler = zelf.state.read().handle_start.clone();
|
|
if let Some(handler) = handler {
|
|
let attrib = attrib.unwrap_or_else(|| vm.ctx.new_dict());
|
|
handler.call((tag, attrib), vm)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn _expat_end(zelf: &Py<Self>, tag: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
|
|
if let Some(builder) = zelf.native_target(vm) {
|
|
// The standard tree builder does not look at the end tag.
|
|
builder.handle_end(vm)?;
|
|
return Ok(());
|
|
}
|
|
let handler = zelf.state.read().handle_end.clone();
|
|
if let Some(handler) = handler {
|
|
let tag = zelf.make_universal(tag, vm)?;
|
|
handler.call((tag,), vm)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn _expat_data(zelf: &Py<Self>, data: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
|
|
if let Some(builder) = zelf.native_target(vm) {
|
|
builder.handle_data(data, vm);
|
|
return Ok(());
|
|
}
|
|
let handler = zelf.state.read().handle_data.clone();
|
|
if let Some(handler) = handler {
|
|
handler.call((data,), vm)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn _expat_comment(zelf: &Py<Self>, text: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
|
|
if let Some(builder) = zelf.native_target(vm) {
|
|
builder.handle_comment(text, vm)?;
|
|
return Ok(());
|
|
}
|
|
let handler = zelf.state.read().handle_comment.clone();
|
|
if let Some(handler) = handler {
|
|
handler.call((text,), vm)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn _expat_start_ns(
|
|
zelf: &Py<Self>,
|
|
prefix: PyObjectRef,
|
|
uri: PyObjectRef,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
if let Some(builder) = zelf.native_target(vm) {
|
|
// The standard tree builder has no start_ns() of its own; it
|
|
// only forwards the event when one was asked for.
|
|
return builder.handle_start_ns(prefix, uri, vm);
|
|
}
|
|
let handler = zelf.state.read().handle_start_ns.clone();
|
|
if let Some(handler) = handler {
|
|
handler.call((prefix, uri), vm)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn _expat_end_ns(
|
|
zelf: &Py<Self>,
|
|
prefix: PyObjectRef,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
if let Some(builder) = zelf.native_target(vm) {
|
|
return builder.handle_end_ns(vm.ctx.none(), vm);
|
|
}
|
|
let handler = zelf.state.read().handle_end_ns.clone();
|
|
if let Some(handler) = handler {
|
|
handler.call((prefix,), vm)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn _expat_pi(
|
|
zelf: &Py<Self>,
|
|
target: PyObjectRef,
|
|
data: PyObjectRef,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
if let Some(builder) = zelf.native_target(vm) {
|
|
if builder.wants_pi() {
|
|
builder.handle_pi(target, data, vm)?;
|
|
}
|
|
return Ok(());
|
|
}
|
|
let handler = zelf.state.read().handle_pi.clone();
|
|
if let Some(handler) = handler {
|
|
handler.call((target, data), vm)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ParentLocator {
|
|
parent: PyRef<PyElement>,
|
|
child_index: usize,
|
|
}
|
|
|
|
// Pre-order traversal shared by `Element.iter()` and
|
|
// `Element.itertext()`, kept as an explicit parent stack so a deep tree
|
|
// costs no Rust recursion.
|
|
#[pyclass(no_attr, module = "_elementtree", name = "_element_iterator")]
|
|
#[derive(Debug, PyPayload)]
|
|
pub(crate) struct PyElementIter {
|
|
state: PyRwLock<ElementIterState>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ElementIterState {
|
|
parent_stack: Vec<ParentLocator>,
|
|
root: Option<PyRef<PyElement>>,
|
|
sought_tag: PyObjectRef,
|
|
gettext: bool,
|
|
}
|
|
|
|
impl PyElementIter {
|
|
fn new(root: PyRef<PyElement>, sought_tag: PyObjectRef, gettext: bool) -> Self {
|
|
Self {
|
|
state: PyRwLock::new(ElementIterState {
|
|
parent_stack: Vec::new(),
|
|
root: Some(root),
|
|
sought_tag,
|
|
gettext,
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[pyclass(with(IterNext, Iterable), flags(DISALLOW_INSTANTIATION))]
|
|
impl PyElementIter {}
|
|
|
|
impl SelfIter for PyElementIter {}
|
|
|
|
impl IterNext for PyElementIter {
|
|
fn next(
|
|
zelf: &Py<Self>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<crate::vm::protocol::PyIterReturn> {
|
|
use crate::vm::protocol::PyIterReturn;
|
|
loop {
|
|
// Pull the next candidate element out of the traversal, or a
|
|
// pending tail text when running for itertext().
|
|
enum Step {
|
|
Elem(PyRef<PyElement>),
|
|
Text(PyObjectRef),
|
|
Done,
|
|
}
|
|
let step = {
|
|
let mut state = zelf.state.write();
|
|
if state.parent_stack.is_empty() {
|
|
match state.root.take() {
|
|
Some(root) => Step::Elem(root),
|
|
None => Step::Done,
|
|
}
|
|
} else {
|
|
let gettext = state.gettext;
|
|
let last = state.parent_stack.len() - 1;
|
|
let item = &mut state.parent_stack[last];
|
|
let parent = item.parent.clone();
|
|
let child_index = item.child_index;
|
|
match parent.child(child_index) {
|
|
Some(child) => {
|
|
item.child_index += 1;
|
|
drop(state);
|
|
let child = child.downcast::<PyElement>().map_err(|obj| {
|
|
vm.new_type_error(format!(
|
|
"expected an Element, not \"{}\"",
|
|
obj.class().name()
|
|
))
|
|
})?;
|
|
Step::Elem(child)
|
|
}
|
|
None => {
|
|
state.parent_stack.pop();
|
|
// itertext() reports only *inner* text, so
|
|
// the tail of the element iteration started
|
|
// from is skipped.
|
|
let want_tail = gettext && !state.parent_stack.is_empty();
|
|
drop(state);
|
|
if want_tail {
|
|
Step::Text(parent.joined_get(true, vm)?)
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
let elem = match step {
|
|
Step::Done => return Ok(PyIterReturn::StopIteration(None)),
|
|
Step::Text(text) => {
|
|
if !vm.is_none(&text) && text.clone().is_true(vm)? {
|
|
return Ok(PyIterReturn::Return(text));
|
|
}
|
|
continue;
|
|
}
|
|
Step::Elem(elem) => elem,
|
|
};
|
|
let (gettext, sought_tag) = {
|
|
let state = zelf.state.read();
|
|
(state.gettext, state.sought_tag.clone())
|
|
};
|
|
zelf.state.write().parent_stack.push(ParentLocator {
|
|
parent: elem.clone(),
|
|
child_index: 0,
|
|
});
|
|
if gettext {
|
|
let tag = elem.tag_obj();
|
|
if !vm.is_none(&tag) && !tag.downcastable::<PyStr>() {
|
|
continue;
|
|
}
|
|
let text = elem.joined_get(false, vm)?;
|
|
if !vm.is_none(&text) && text.clone().is_true(vm)? {
|
|
return Ok(PyIterReturn::Return(text));
|
|
}
|
|
continue;
|
|
}
|
|
if vm.is_none(&sought_tag) {
|
|
return Ok(PyIterReturn::Return(elem.into()));
|
|
}
|
|
if vm.bool_eq(&elem.tag_obj(), &sought_tag)? {
|
|
return Ok(PyIterReturn::Return(elem.into()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|