Skip to content

Commit c63cf7c

Browse files
authored
Vector type helpers (w/ new package structure) (#45)
* New package structure + vector endian swap helper * Add extension for Vector to/from native conversions * Clean up benchmarks
1 parent 51a6fd4 commit c63cf7c

31 files changed

+1840
-93
lines changed

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@ Go into `tests/benchmarks/test_benchmarks.py` and adjust the connection details
9090
This implies you're having a running database.
9191
Then run the benchmarks with:
9292
```bash
93-
python -m tox -e py312-test -- --benchmark-only --benchmark-autosave --benchmark-group-by=fullname
93+
python -m tox -e py312-test -- --benchmark-only --benchmark-autosave
9494
# or to compare the results with the previous run
95-
python -m tox -e py312-test -- --benchmark-only --benchmark-autosave --benchmark-group-by=fullname --benchmark-compare
95+
python -m tox -e py312-test -- --benchmark-only --benchmark-autosave --benchmark-compare
9696
```
9797

9898
### Changelog Entry

bin/target_driver.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ git fetch origin
1111
git checkout "$version"
1212
git pull origin "$version"
1313
cd ..
14-
cp driver/tests/unit/common/codec/packstream/v1/test_packstream.py tests/v1/from_driver/test_packstream.py
14+
cp driver/tests/unit/common/codec/packstream/v1/test_packstream.py tests/codec/packstream/v1/from_driver/test_packstream.py
15+
cp -r driver/tests/unit/common/vector/* tests/vector/from_driver
1516

1617
towncrier create -c "Target driver version ${version}<ISSUES_LIST>." "+.feature"
1718
echo "=== Please rename the changelog file to match the PR number. ==="

changelog.d/45.feature.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add extension for the `Vector` type<ISSUES_LIST>.
2+
* Speed up endian conversion (byte flipping).
3+
* Speed up conversion from and to native python types.
File renamed without changes.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ build-backend = "maturin"
6363

6464
[tool.maturin]
6565
features = ["pyo3/extension-module", "pyo3/generate-import-lib"]
66-
module-name = "neo4j._codec.packstream._rust"
66+
module-name = "neo4j._rust"
6767
exclude = [
6868
"/.editorconfig",
6969
".gitignore",

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ isort>=6.0.1
88
tox>=4.25.0
99
pytest>=8.3.5
1010
pytest-benchmark>=5.1.0
11+
pytest-mock>=3.14.1
1112

1213
# for Python driver's TestKit backend
1314
freezegun>=1.5.1

src/codec.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) "Neo4j"
2+
// Neo4j Sweden AB [https://neo4j.com]
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
mod packstream;
17+
18+
use pyo3::prelude::*;
19+
20+
use crate::register_package;
21+
22+
pub(super) fn init_module(m: &Bound<PyModule>, name: &str) -> PyResult<()> {
23+
let py = m.py();
24+
25+
m.gil_used(false)?;
26+
register_package(m, name)?;
27+
28+
let mod_packstream = PyModule::new(py, "packstream")?;
29+
m.add_submodule(&mod_packstream)?;
30+
packstream::init_module(&mod_packstream, format!("{name}.packstream").as_str())?;
31+
32+
Ok(())
33+
}

src/codec/packstream.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (c) "Neo4j"
2+
// Neo4j Sweden AB [https://neo4j.com]
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
mod v1;
17+
18+
use pyo3::basic::CompareOp;
19+
use pyo3::exceptions::PyValueError;
20+
use pyo3::prelude::*;
21+
use pyo3::types::{PyBytes, PyTuple};
22+
use pyo3::IntoPyObjectExt;
23+
24+
use crate::register_package;
25+
26+
pub(super) fn init_module(m: &Bound<PyModule>, name: &str) -> PyResult<()> {
27+
let py = m.py();
28+
29+
m.gil_used(false)?;
30+
register_package(m, name)?;
31+
32+
let mod_v1 = PyModule::new(py, "v1")?;
33+
m.add_submodule(&mod_v1)?;
34+
v1::init_module(&mod_v1, format!("{name}.v1").as_str())?;
35+
36+
m.add_class::<Structure>()?;
37+
38+
Ok(())
39+
}
40+
41+
#[pyclass]
42+
#[derive(Debug)]
43+
pub struct Structure {
44+
tag: u8,
45+
#[pyo3(get)]
46+
fields: Vec<PyObject>,
47+
}
48+
49+
#[pymethods]
50+
impl Structure {
51+
#[new]
52+
#[pyo3(signature = (tag, *fields))]
53+
#[pyo3(text_signature = "(tag, *fields)")]
54+
fn new(tag: &[u8], fields: Vec<PyObject>) -> PyResult<Self> {
55+
if tag.len() != 1 {
56+
return Err(PyErr::new::<PyValueError, _>("tag must be a single byte"));
57+
}
58+
let tag = tag[0];
59+
Ok(Self { tag, fields })
60+
}
61+
62+
#[getter(tag)]
63+
fn read_tag<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
64+
PyBytes::new(py, &[self.tag])
65+
}
66+
67+
#[getter(fields)]
68+
fn read_fields<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
69+
PyTuple::new(py, &self.fields)
70+
}
71+
72+
fn eq(&self, other: &Self, py: Python<'_>) -> PyResult<bool> {
73+
if self.tag != other.tag || self.fields.len() != other.fields.len() {
74+
return Ok(false);
75+
}
76+
for (a, b) in self
77+
.fields
78+
.iter()
79+
.map(|e| e.bind(py))
80+
.zip(other.fields.iter().map(|e| e.bind(py)))
81+
{
82+
if !a.eq(b)? {
83+
return Ok(false);
84+
}
85+
}
86+
Ok(true)
87+
}
88+
89+
fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> PyResult<PyObject> {
90+
Ok(match op {
91+
CompareOp::Eq => self.eq(other, py)?.into_py_any(py)?,
92+
CompareOp::Ne => (!self.eq(other, py)?).into_py_any(py)?,
93+
_ => py.NotImplemented(),
94+
})
95+
}
96+
97+
fn __hash__(&self, py: Python<'_>) -> PyResult<isize> {
98+
let mut fields_hash = 0;
99+
for field in &self.fields {
100+
fields_hash += field.bind(py).hash()?;
101+
}
102+
Ok(fields_hash.wrapping_add(self.tag.into()))
103+
}
104+
}

src/v1.rs renamed to src/codec/packstream/v1.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ mod unpack;
1919
use pyo3::prelude::*;
2020
use pyo3::wrap_pyfunction;
2121

22+
use crate::register_package;
23+
2224
const TINY_STRING: u8 = 0x80;
2325
const TINY_LIST: u8 = 0x90;
2426
const TINY_MAP: u8 = 0xA0;
@@ -44,7 +46,10 @@ const BYTES_8: u8 = 0xCC;
4446
const BYTES_16: u8 = 0xCD;
4547
const BYTES_32: u8 = 0xCE;
4648

47-
pub(crate) fn register(m: &Bound<PyModule>) -> PyResult<()> {
49+
pub(crate) fn init_module(m: &Bound<PyModule>, name: &str) -> PyResult<()> {
50+
m.gil_used(false)?;
51+
register_package(m, name)?;
52+
4853
m.add_function(wrap_pyfunction!(unpack::unpack, m)?)?;
4954
m.add_function(wrap_pyfunction!(pack::pack, m)?)?;
5055

src/v1/pack.rs renamed to src/codec/packstream/v1/pack.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ use pyo3::sync::GILOnceCell;
2222
use pyo3::types::{PyBytes, PyDict, PyString, PyType};
2323
use pyo3::{intern, IntoPyObjectExt};
2424

25+
use super::super::Structure;
2526
use super::{
2627
BYTES_16, BYTES_32, BYTES_8, FALSE, FLOAT_64, INT_16, INT_32, INT_64, INT_8, LIST_16, LIST_32,
2728
LIST_8, MAP_16, MAP_32, MAP_8, NULL, STRING_16, STRING_32, STRING_8, TINY_LIST, TINY_MAP,
2829
TINY_STRING, TINY_STRUCT, TRUE,
2930
};
30-
use crate::Structure;
3131

3232
#[derive(Debug)]
3333
struct TypeMappings {

0 commit comments

Comments
 (0)