diff --git a/examples/getitem/.template/Cargo.toml b/examples/getitem/.template/Cargo.toml new file mode 100644 index 00000000..47dbb7b0 --- /dev/null +++ b/examples/getitem/.template/Cargo.toml @@ -0,0 +1,12 @@ +[package] +authors = ["{{authors}}"] +name = "{{project-name}}" +version = "0.1.0" +edition = "2021" + +[lib] +name = "getitem" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "{{PYO3_VERSION}}", features = ["extension-module"] } diff --git a/examples/getitem/.template/pre-script.rhai b/examples/getitem/.template/pre-script.rhai new file mode 100644 index 00000000..0368bb1f --- /dev/null +++ b/examples/getitem/.template/pre-script.rhai @@ -0,0 +1,4 @@ +variable::set("PYO3_VERSION", "0.19.0"); +file::rename(".template/Cargo.toml", "Cargo.toml"); +file::rename(".template/pyproject.toml", "pyproject.toml"); +file::delete(".template"); diff --git a/examples/getitem/.template/pyproject.toml b/examples/getitem/.template/pyproject.toml new file mode 100644 index 00000000..537fdacc --- /dev/null +++ b/examples/getitem/.template/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +requires = ["maturin>=1,<2"] +build-backend = "maturin" + +[project] +name = "{{project-name}}" +version = "0.1.0" diff --git a/examples/getitem/Cargo.toml b/examples/getitem/Cargo.toml new file mode 100644 index 00000000..17020b9b --- /dev/null +++ b/examples/getitem/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "getitem" +version = "0.1.0" +edition = "2021" + +[lib] +name = "getitem" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { path = "../../", features = ["extension-module"] } + +[workspace] diff --git a/examples/getitem/MANIFEST.in b/examples/getitem/MANIFEST.in new file mode 100644 index 00000000..becccf7b --- /dev/null +++ b/examples/getitem/MANIFEST.in @@ -0,0 +1,2 @@ +include pyproject.toml Cargo.toml +recursive-include src * diff --git a/examples/getitem/README.md b/examples/getitem/README.md new file mode 100644 index 00000000..fc06fc1b --- /dev/null +++ b/examples/getitem/README.md @@ -0,0 +1,46 @@ +# getitem + +A project showcasing how to create a `__getitem__` override that also showcases how to deal with multiple incoming types + +## Relevant Documentation + +Some of the relevant documentation links for this example: + +* Converting Slices to Indices: https://docs.rs/pyo3/latest/pyo3/types/struct.PySlice.html#method.indices +* GetItem Docs: https://pyo3.rs/latest/class/protocols.html?highlight=__getitem__#mapping--sequence-types +* Extract: https://pyo3.rs/latest/conversions/traits.html?highlight=extract#extract-and-the-frompyobject-trait +* Downcast and getattr: https://pyo3.rs/v0.19.0/types.html?highlight=getattr#pyany + + +## Building and Testing + +To build this package, first install `maturin`: + +```shell +pip install maturin +``` + +To build and test use `maturin develop`: + +```shell +pip install -r requirements-dev.txt +maturin develop +pytest +``` + +Alternatively, install nox and run the tests inside an isolated environment: + +```shell +nox +``` + +## Copying this example + +Use [`cargo-generate`](https://crates.io/crates/cargo-generate): + +```bash +$ cargo install cargo-generate +$ cargo generate --git https://github.com/PyO3/pyo3 examples/decorator +``` + +(`cargo generate` will take a little while to clone the PyO3 repo first; be patient when waiting for the command to run.) diff --git a/examples/getitem/cargo-generate.toml b/examples/getitem/cargo-generate.toml new file mode 100644 index 00000000..d750c4de --- /dev/null +++ b/examples/getitem/cargo-generate.toml @@ -0,0 +1,5 @@ +[template] +ignore = [".nox"] + +[hooks] +pre = [".template/pre-script.rhai"] diff --git a/examples/getitem/noxfile.py b/examples/getitem/noxfile.py new file mode 100644 index 00000000..17a6b80f --- /dev/null +++ b/examples/getitem/noxfile.py @@ -0,0 +1,9 @@ +import nox + + +@nox.session +def python(session): + session.install("-rrequirements-dev.txt") + session.install("maturin") + session.run_always("maturin", "develop") + session.run("pytest") diff --git a/examples/getitem/pyproject.toml b/examples/getitem/pyproject.toml new file mode 100644 index 00000000..2cb6dc71 --- /dev/null +++ b/examples/getitem/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["maturin>=1,<2"] +build-backend = "maturin" + +[project] +name = "getitem" +version = "0.1.0" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Rust", + "Operating System :: POSIX", + "Operating System :: MacOS :: MacOS X", +] diff --git a/examples/getitem/requirements-dev.txt b/examples/getitem/requirements-dev.txt new file mode 100644 index 00000000..d9913970 --- /dev/null +++ b/examples/getitem/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest>=3.5.0 +pip>=21.3 +maturin>=1,<2 diff --git a/examples/getitem/src/lib.rs b/examples/getitem/src/lib.rs new file mode 100644 index 00000000..90a3e9fc --- /dev/null +++ b/examples/getitem/src/lib.rs @@ -0,0 +1,82 @@ +// This is a very fake example of how to check __getitem__ parameter and handle appropriately +use pyo3::exceptions::PyTypeError; +use pyo3::prelude::*; +use pyo3::types::PySlice; +use std::os::raw::c_long; + +#[derive(FromPyObject)] +enum IntOrSlice<'py> { + Int(i32), + Slice(&'py PySlice), +} + +#[pyclass] +struct ExampleContainer { + // represent the maximum length our container is pretending to be + max_length: i32, +} + +#[pymethods] +impl ExampleContainer { + #[new] + fn new() -> Self { + ExampleContainer { max_length: 100 } + } + + fn __getitem__(&self, key: &PyAny) -> PyResult { + if let Ok(position) = key.extract::() { + return Ok(position); + } else if let Ok(slice) = key.downcast::() { + // METHOD 1 - the use PySliceIndices to help with bounds checking and for cases when only start or end are provided + // in this case the start/stop/step all filled in to give valid values based on the max_length given + let index = slice.indices(self.max_length as c_long).unwrap(); + let _delta = index.stop - index.start; + + // METHOD 2 - Do the getattr manually really only needed if you have some special cases for stop/_step not being present + // convert to indices and this will help you deal with stop being the max length + let start: i32 = slice.getattr("start")?.extract()?; + // This particular example assumes stop is present, but note that if not present, this will cause us to return due to the + // extract failing. Not needing custom code to deal with this is a good reason to use the Indices method. + let stop: i32 = slice.getattr("stop")?.extract()?; + // example of grabbing step since it is not always present + let _step: i32 = match slice.getattr("step")?.extract() { + // if no value found assume step is 1 + Ok(v) => v, + Err(_) => 1 as i32, + }; + + // Use something like this if you don't support negative stepping and want to give users + // leeway on how they provide their ordering + let (start, stop) = if start > stop { + (stop, start) + } else { + (start, stop) + }; + let delta = stop - start; + + return Ok(delta); + } else { + return Err(PyTypeError::new_err("Unsupported type")); + } + } + fn __setitem__(&self, idx: IntOrSlice, value: u32) -> PyResult<()> { + match idx { + IntOrSlice::Slice(slice) => { + let index = slice.indices(self.max_length as c_long).unwrap(); + println!("Got a slice! {}-{}, step: {}, value: {}", index.start, index.stop, index.step, value); + } + IntOrSlice::Int(index) => { + println!("Got an index! {} : value: {}", index, value); + } + } + Ok(()) + } +} + +#[pymodule] +#[pyo3(name = "getitem")] +fn example(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + // ? -https://github.com/PyO3/maturin/issues/475 + m.add_class::()?; + Ok(()) +} diff --git a/examples/getitem/tests/test_getitem.py b/examples/getitem/tests/test_getitem.py new file mode 100644 index 00000000..7f2d9a62 --- /dev/null +++ b/examples/getitem/tests/test_getitem.py @@ -0,0 +1,18 @@ +import getitem +import pytest + + +def test_simple(): + container = getitem.ExampleContainer() + assert container[3] == 3 + assert container[4] == 4 + assert container[-1] == -1 + assert container[5:3] == 2 + assert container[3:5] == 2 + # test setitem, but this just displays, no return to check + container[3:5] = 2 + container[2] = 2 + # and note we will get an error on this one since we didn't + # add strings + with pytest.raises(TypeError): + container["foo"] = 2 diff --git a/newsfragments/3222.added.md b/newsfragments/3222.added.md new file mode 100644 index 00000000..fde599bc --- /dev/null +++ b/newsfragments/3222.added.md @@ -0,0 +1 @@ +Simple getitem example showing type-check for possible attribute types \ No newline at end of file