Skip to content

Commit 19e04b0

Browse files
committed
Add PyNone to represent Python None
Add a new zero-sized struct `PyNone`, that represents Python's `None`. This is similar to `NoArgs` for the empty tuple, and is useful for defining functions or methods that return None in Python.
1 parent cda5299 commit 19e04b0

File tree

4 files changed

+118
-2
lines changed

4 files changed

+118
-2
lines changed

src/objects/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub use self::capsule::PyCapsule;
3131
pub use self::dict::PyDict;
3232
pub use self::iterator::PyIterator;
3333
pub use self::list::PyList;
34+
pub use self::none::PyNone;
3435
#[cfg(feature = "python27-sys")]
3536
pub use self::num::PyInt;
3637
#[cfg(feature = "python3-sys")]
@@ -139,6 +140,7 @@ pub mod exc;
139140
mod iterator;
140141
mod list;
141142
mod module;
143+
mod none;
142144
mod num;
143145
mod object;
144146
mod sequence;

src/objects/none.rs

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) 2021 Mark Juggurnauth-Thomas
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
4+
// software and associated documentation files (the "Software"), to deal in the Software
5+
// without restriction, including without limitation the rights to use, copy, modify, merge,
6+
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7+
// to whom the Software is furnished to do so, subject to the following conditions:
8+
//
9+
// The above copyright notice and this permission notice shall be included in all copies or
10+
// substantial portions of the Software.
11+
//
12+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13+
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14+
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15+
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17+
// DEALINGS IN THE SOFTWARE.
18+
19+
use super::exc;
20+
use super::object::PyObject;
21+
use crate::conversion::{FromPyObject, ToPyObject};
22+
use crate::err::{PyErr, PyResult};
23+
use crate::python::{Python, PythonObject};
24+
25+
/// An empty struct that represents `None` in Python.
26+
///
27+
/// This can be used as a function return type for functions that should return
28+
/// `None` in Python.
29+
///
30+
/// # Example
31+
/// ```
32+
/// use cpython::{Python, PyResult, PyNone};
33+
///
34+
/// fn example(py: Python) -> PyResult<PyNone> {
35+
/// Ok(PyNone)
36+
/// }
37+
/// ```
38+
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Default, Hash, Ord)]
39+
pub struct PyNone;
40+
41+
impl ToPyObject for PyNone {
42+
type ObjectType = PyObject;
43+
44+
#[inline]
45+
fn to_py_object(&self, py: Python) -> PyObject {
46+
py.None()
47+
}
48+
}
49+
50+
impl FromPyObject<'_> for PyNone {
51+
fn extract(py: Python, obj: &PyObject) -> PyResult<Self> {
52+
if *obj == py.None() {
53+
Ok(PyNone)
54+
} else {
55+
let msg = format!("Expected None but received {}", obj.get_type(py).name(py));
56+
Err(PyErr::new_lazy_init(
57+
py.get_type::<exc::TypeError>(),
58+
Some(msg.to_py_object(py).into_object()),
59+
))
60+
}
61+
}
62+
}

tests/test_class.rs

+29-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use cpython::_detail::ffi;
44
use cpython::*;
55
use std::cell::{Cell, RefCell};
6-
use std::sync::atomic::{AtomicBool, Ordering};
6+
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
77
use std::sync::Arc;
88
use std::{isize, iter, mem};
99

@@ -189,6 +189,34 @@ fn instance_method() {
189189
py.run("assert obj.method() == 42", None, Some(&d)).unwrap();
190190
}
191191

192+
py_class!(class InstanceMethodReturnsNone |py| {
193+
data member: AtomicUsize;
194+
195+
def value(&self) -> PyResult<usize> {
196+
Ok(self.member(py).load(Ordering::Relaxed))
197+
}
198+
199+
def incr(&self) -> PyResult<PyNone> {
200+
self.member(py).fetch_add(1, Ordering::Relaxed);
201+
Ok(PyNone)
202+
}
203+
});
204+
205+
#[test]
206+
fn instance_method_returns_none() {
207+
let gil = Python::acquire_gil();
208+
let py = gil.python();
209+
210+
let obj = InstanceMethodReturnsNone::create_instance(py, AtomicUsize::new(0)).unwrap();
211+
assert!(obj.incr(py).unwrap() == PyNone);
212+
assert!(obj.value(py).unwrap() == 1);
213+
let d = PyDict::new(py);
214+
d.set_item(py, "obj", obj).unwrap();
215+
py.run("assert obj.value() == 1", None, Some(&d)).unwrap();
216+
py.run("assert obj.incr() is None", None, Some(&d)).unwrap();
217+
py.run("assert obj.value() == 2", None, Some(&d)).unwrap();
218+
}
219+
192220
py_class!(class InstanceMethodWithArgs |py| {
193221
data member: i32;
194222

tests/test_function.rs

+25-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use cpython::{py_fn, NoArgs, ObjectProtocol, PyDict, PyResult, Python};
1+
use cpython::{py_fn, NoArgs, ObjectProtocol, PyDict, PyNone, PyResult, Python};
22
use std::sync::atomic;
33
use std::sync::atomic::Ordering::Relaxed;
44

@@ -157,6 +157,30 @@ fn opt_args() {
157157
);
158158
}
159159

160+
#[test]
161+
fn none_return() {
162+
static CALL_COUNT: atomic::AtomicUsize = atomic::AtomicUsize::new(0);
163+
164+
fn f(_py: Python) -> PyResult<PyNone> {
165+
CALL_COUNT.fetch_add(1, Relaxed);
166+
Ok(PyNone)
167+
}
168+
169+
let gil = Python::acquire_gil();
170+
let py = gil.python();
171+
let obj = py_fn!(py, f());
172+
173+
assert_eq!(CALL_COUNT.load(Relaxed), 0);
174+
assert_eq!(
175+
obj.call(py, NoArgs, None)
176+
.unwrap()
177+
.extract::<PyNone>(py)
178+
.unwrap(),
179+
PyNone,
180+
);
181+
assert_eq!(CALL_COUNT.load(Relaxed), 1);
182+
}
183+
160184
/* TODO: reimplement flexible sig support
161185
#[test]
162186
fn flexible_sig() {

0 commit comments

Comments
 (0)