From 5bb3b0bec3f98c716db3eceeb424301377f8fbde Mon Sep 17 00:00:00 2001 From: Julian Popescu Date: Sun, 19 May 2024 15:27:57 +0200 Subject: [PATCH] fix: mutability of python objects --- Cargo.toml | 6 +- pyproject.toml | 2 +- src/lib.rs | 4 +- src/molecule.rs | 9 ++ src/python.rs | 259 +++++++++++++++++++++++++++--------------------- 5 files changed, 164 insertions(+), 116 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a3265b7..a630c94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xyz-parse" -version = "0.1.2" +version = "0.1.3" edition = "2021" authors = ["Julian Popescu "] license = "MIT OR Apache-2.0" @@ -19,8 +19,8 @@ crate-type = ["cdylib"] [features] default = [] -python = ["pyo3"] +pyo3-lib = ["pyo3", "pyo3/extension-module", "pyo3/abi3-py37"] [dependencies] rust_decimal = { version = "1.35", default-features = false } -pyo3 = { version = "0.21", features = ["rust_decimal", "extension-module", "abi3-py37"], optional = true } +pyo3 = { version = "0.21", features = ["rust_decimal"], optional = true } diff --git a/pyproject.toml b/pyproject.toml index 1cf9c42..d545da4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,4 +13,4 @@ classifiers = [ dynamic = ["version"] [tool.maturin] -features = ["python"] +features = ["pyo3-lib"] diff --git a/src/lib.rs b/src/lib.rs index c8d9588..b246bde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,9 +15,11 @@ mod atom; mod molecule; mod xyz; -#[cfg(feature = "python")] +#[cfg(feature = "pyo3")] mod python; +pub use rust_decimal; + pub use atom::*; pub use molecule::*; pub use xyz::*; diff --git a/src/molecule.rs b/src/molecule.rs index 14d5fa4..ff32ca6 100644 --- a/src/molecule.rs +++ b/src/molecule.rs @@ -1,4 +1,5 @@ use crate::atom::{Atom, AtomParseError}; +use rust_decimal::Decimal; use std::{borrow::Cow, error::Error, fmt, str::FromStr}; /// An error that can occur when parsing a [`Molecule`] @@ -119,6 +120,14 @@ impl<'a> Molecule<'a> { .collect(), } } + + pub fn symbols(&self) -> impl ExactSizeIterator { + self.atoms.iter().map(|atom| atom.symbol.as_ref()) + } + + pub fn coordinates(&self) -> impl ExactSizeIterator + '_ { + self.atoms.iter().map(|atom| atom.coordinates()) + } } impl<'a> fmt::Display for Molecule<'a> { diff --git a/src/python.rs b/src/python.rs index 856edd1..693e338 100644 --- a/src/python.rs +++ b/src/python.rs @@ -1,6 +1,10 @@ use crate::{atom::Atom, molecule::Molecule, xyz::Xyz}; -use pyo3::{create_exception, exceptions::PyException, prelude::*, types::PyType}; -use rust_decimal::Decimal; +use pyo3::{ + create_exception, + exceptions::PyException, + prelude::*, + types::{PyList, PyString, PyTuple, PyType}, +}; use std::borrow::Cow; create_exception!(xyz_parse, ParseError, PyException); @@ -17,181 +21,214 @@ fn xyz_parse(m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyclass(name = "Atom", module = "xyz_parse")] #[derive(Debug, Clone)] -pub struct PyAtom(Atom<'static>); +pub struct PyAtom { + #[pyo3(get, set)] + pub symbol: Py, + #[pyo3(get, set)] + pub x: Py, + #[pyo3(get, set)] + pub y: Py, + #[pyo3(get, set)] + pub z: Py, +} + +impl<'a> Atom<'a> { + pub fn to_py(&self, py: Python<'a>) -> PyAtom { + PyAtom { + symbol: PyString::new_bound(py, &self.symbol).unbind(), + x: self.x.to_object(py), + y: self.y.to_object(py), + z: self.z.to_object(py), + } + } +} + +impl PyAtom { + pub fn to_rust(&self, py: Python<'_>) -> PyResult> { + Ok(Atom { + symbol: Cow::Owned(self.symbol.extract(py)?), + x: self.x.extract(py)?, + y: self.y.extract(py)?, + z: self.z.extract(py)?, + }) + } +} #[pymethods] impl PyAtom { #[new] - fn new(symbol: String, x: Decimal, y: Decimal, z: Decimal) -> Self { - PyAtom(Atom { - symbol: Cow::Owned(symbol), - x, - y, - z, - }) + fn new(symbol: Py, x: Py, y: Py, z: Py) -> Self { + PyAtom { symbol, x, y, z } } #[classmethod] - fn parse(_: &Bound<'_, PyType>, input: &str) -> PyResult { + fn parse(_: &Bound<'_, PyType>, py: Python<'_>, input: &str) -> PyResult { Atom::parse(input) - .map(|atom| PyAtom(atom.into_owned())) + .map(|atom| atom.to_py(py)) .map_err(|err| ParseError::new_err(err.to_string())) } #[getter] - fn get_symbol(&self) -> Cow<'_, str> { - self.0.symbol.clone() - } - - #[setter] - fn set_symbol(&mut self, symbol: String) { - self.0.symbol = Cow::Owned(symbol); - } - - #[getter] - fn get_x(&self) -> Decimal { - self.0.x + fn coordinates<'py>(&self, py: Python<'py>) -> Bound<'py, PyTuple> { + PyTuple::new_bound( + py, + [ + self.x.clone_ref(py), + self.y.clone_ref(py), + self.z.clone_ref(py), + ], + ) } - #[setter] - fn set_x(&mut self, decimal: Decimal) { - self.0.x = decimal; + fn __str__(&self, py: Python<'_>) -> PyResult { + Ok(self.to_rust(py)?.to_string()) } - #[getter] - fn get_y(&self) -> Decimal { - self.0.y - } - - #[setter] - fn set_y(&mut self, decimal: Decimal) { - self.0.y = decimal; - } - - #[getter] - fn get_z(&self) -> Decimal { - self.0.z + fn __repr__(&self, py: Python<'_>) -> PyResult { + Ok(format!("{:?}", self.to_rust(py)?)) } +} - #[setter] - fn set_z(&mut self, decimal: Decimal) { - self.0.z = decimal; - } +#[pyclass(name = "Molecule", module = "xyz_parse")] +#[derive(Debug, Clone)] +pub struct PyMolecule { + #[pyo3(get, set)] + pub comment: Py, + #[pyo3(get, set)] + pub atoms: Py, +} - #[getter] - fn coordinates(&self) -> (Decimal, Decimal, Decimal) { - (self.0.x, self.0.y, self.0.z) +impl<'a> Molecule<'a> { + pub fn to_py(&self, py: Python<'a>) -> PyMolecule { + PyMolecule { + comment: PyString::new_bound(py, &self.comment).unbind(), + atoms: PyList::new_bound(py, self.atoms.iter().map(|atom| atom.to_py(py).into_py(py))) + .unbind(), + } } +} - fn __str__(&self) -> String { - self.0.to_string() +impl PyMolecule { + pub fn to_rust(&self, py: Python<'_>) -> PyResult> { + Ok(Molecule { + comment: Cow::Owned(self.comment.extract(py)?), + atoms: self + .atoms + .bind(py) + .iter() + .map(|atom| atom.extract::()?.to_rust(py)) + .collect::>()?, + }) } - fn __repr__(&self) -> String { - format!("{:?}", self.0) + pub fn py_atoms(&self, py: Python<'_>) -> PyResult> { + self.atoms + .bind(py) + .iter() + .map(|atom| atom.extract::()) + .collect() } } -#[pyclass(name = "Molecule", module = "xyz_parse")] -#[derive(Debug, Clone)] -pub struct PyMolecule(Molecule<'static>); - #[pymethods] impl PyMolecule { #[new] - fn new(comment: String, atoms: Vec) -> Self { - PyMolecule(Molecule { - comment: Cow::Owned(comment), - atoms: atoms.into_iter().map(|atom| atom.0).collect(), - }) + fn new(comment: Py, atoms: Py) -> Self { + PyMolecule { comment, atoms } } #[classmethod] - fn parse(_: &Bound<'_, PyType>, input: &str) -> PyResult { + fn parse(_: &Bound<'_, PyType>, py: Python<'_>, input: &str) -> PyResult { Molecule::parse(input) - .map(|molecule| PyMolecule(molecule.into_owned())) + .map(|molecule| molecule.to_py(py)) .map_err(|err| ParseError::new_err(err.to_string())) } #[getter] - fn get_comment(&self) -> Cow<'static, str> { - self.0.comment.clone() - } - - #[setter] - fn set_comment(&mut self, comment: String) { - self.0.comment = Cow::Owned(comment); + fn symbols<'py>(&self, py: Python<'py>) -> PyResult> { + Ok(PyList::new_bound( + py, + self.py_atoms(py)?.iter().map(|atom| atom.symbol.bind(py)), + )) } #[getter] - fn get_atoms(&self) -> Vec { - self.0 - .atoms - .iter() - .map(|atom| PyAtom(atom.clone())) - .collect() - } - - #[setter] - fn set_atoms(&mut self, atoms: Vec) { - self.0.atoms = atoms.into_iter().map(|atom| atom.0).collect(); + fn coordinates<'py>(&self, py: Python<'py>) -> PyResult> { + Ok(PyList::new_bound( + py, + self.py_atoms(py)?.iter().map(|atom| atom.coordinates(py)), + )) } - fn __str__(&self) -> String { - self.0.to_string() + fn __str__(&self, py: Python<'_>) -> PyResult { + Ok(self.to_rust(py)?.to_string()) } - fn __repr__(&self) -> String { - format!("{:?}", self.0) + fn __repr__(&self, py: Python<'_>) -> PyResult { + Ok(format!("{:?}", self.to_rust(py)?)) } } #[pyclass(name = "Xyz", module = "xyz_parse")] #[derive(Debug, Clone)] -pub struct PyXyz(Xyz<'static>); +pub struct PyXyz { + #[pyo3(get, set)] + pub molecules: Py, +} + +impl<'a> Xyz<'a> { + pub fn to_py(&self, py: Python<'a>) -> PyXyz { + PyXyz { + molecules: PyList::new_bound( + py, + self.molecules + .iter() + .map(|molecule| molecule.to_py(py).into_py(py)), + ) + .unbind(), + } + } +} + +impl PyXyz { + pub fn to_rust(&self, py: Python<'_>) -> PyResult> { + Ok(Xyz { + molecules: self + .molecules + .bind(py) + .iter() + .map(|molecule| molecule.extract::()?.to_rust(py)) + .collect::>()?, + }) + } +} #[pymethods] impl PyXyz { #[new] - fn new(molecules: Vec) -> Self { - PyXyz(Xyz { - molecules: molecules.into_iter().map(|molecule| molecule.0).collect(), - }) + fn new(molecules: Py) -> Self { + PyXyz { molecules } } #[classmethod] - fn parse(_: &Bound<'_, PyType>, input: &str) -> PyResult { + fn parse(_: &Bound<'_, PyType>, py: Python<'_>, input: &str) -> PyResult { Xyz::parse(input) - .map(|xyz| PyXyz(xyz.into_owned())) + .map(|xyz| xyz.to_py(py)) .map_err(|err| ParseError::new_err(err.to_string())) } - #[getter] - fn get_molecules(&self) -> Vec { - self.0 - .molecules - .iter() - .map(|molecule| PyMolecule(molecule.clone())) - .collect() - } - - #[setter] - fn set_molecules(&mut self, molecules: Vec) { - self.0.molecules = molecules.into_iter().map(|molecule| molecule.0).collect(); - } - - fn __str__(&self) -> String { - self.0.to_string() + fn __str__(&self, py: Python<'_>) -> PyResult { + Ok(self.to_rust(py)?.to_string()) } - fn __repr__(&self) -> String { - format!("{:?}", self.0) + fn __repr__(&self, py: Python<'_>) -> PyResult { + Ok(format!("{:?}", self.to_rust(py)?)) } } #[pyfunction] -fn parse_xyz(input: &str) -> PyResult { +fn parse_xyz(py: Python<'_>, input: &str) -> PyResult { Xyz::parse(input) - .map(|xyz| PyXyz(xyz.into_owned())) + .map(|xyz| xyz.to_py(py)) .map_err(|err| ParseError::new_err(err.to_string())) }