Skip to main content

pyo3_testing/
lib.rs

1//! Simplifies testing of `#[pyo3function]`s by enabling tests to be condensed to:
2//!
3//! ```no_run - expands to a test so is never actually run anyway
4//! use pyo3_testing::pyo3test;
5//! ##[pyo3test]
6//! ##[pyo3import(py_adders: from adders import addone)]
7//! fn test_pyo3test_simple_case() {
8//!     let result: isize = addone!(1);
9//!     assert_eq!(result, 2);
10//! }
11//! ```
12//!
13//! and for checking that the correct type of python Exception is raised:
14//!
15//! ```no_run - expands to a test so is never actually run anyway
16//! use pyo3_testing::{pyo3test, with_py_raises};
17//! ##[pyo3test]
18//! ##[allow(unused_macros)]
19//! ##[pyo3import(py_adders: from adders import addone)]
20//! fn test_raises() {
21//!     with_py_raises!(PyTypeError, { addone.call1(("4",)) });
22//! }
23//! ```
24//!
25//! ### Supported pyo3 version(s)
26//!
27//! As of v0.28.0 pyo3_testing version numbers will shadow pyo3's left-most non-zero
28//! major/minor version. E.g. v0.28.x matches v0.28.x. This is to simplify things so
29//! that you and cargo can always find the right match.
30//!
31//! Details of previous compatible versions are in the readme.
32
33mod pyo3test;
34mod withpyraises;
35
36use pyo3test::impl_pyo3test;
37use withpyraises::impl_with_py_raises;
38
39use proc_macro::TokenStream as TokenStream1;
40
41/// A proc macro to decorate tests, which removes boilerplate code required for testing pyO3-wrapped
42/// functions within rust.
43///
44///   1. takes a function (the "testcase") designed to test either a `#[pyo3module]`
45///      or a `#[pyo3function]`,
46///   2. imports the `pyo3module` and `pyo3function` so they are accessible to a python interpreter embedded in rust,
47///   3. creates a `"call macro"` for each `pyo3function` so you can easily call it,
48///   4. executes the body of the testcase using an embedded python interpreter.
49///
50///
51/// ## Specifying the function or module to test with `#[pyo3import(...)]`
52///
53/// Add the attribute `#[pyo3import(...)]` between `#[pyo3test]` and the testcase using the
54/// following format:
55///
56///   - `#[pyo3import(module_rustfn: from python_module import python_function)]` OR
57///   - `#[pyo3import(module_rustfn: import python_module)]`
58///
59/// where:
60///   - `module_rustfn` is the rust function identifier of the `#[pymodule]`
61///   - `python_module` is the module name exposed to python
62///   - `python_function` is the function name exposed to python
63///
64/// You can then directly call `python_function!(...)` or use `python_module` and `python_function`
65/// within the testcase as described in [pyo3: Calling Python functions][1]
66///
67/// [1]: https://pyo3.rs/latest/python-from-rust/function-calls.html#calling-python-functions
68///
69/// ### Note:
70///
71/// 1. Multiple imports are possible
72///
73/// ## "Call macros"
74///
75/// `#[pyo3test]` will automatically generate a macro for each of the `python_function`s imported.
76/// The macro will have the same name as the function name exposed to python and can be called
77/// using `python_function!()`. This avoids the need to use the correct `.call()`, `.call1()` or
78/// `.call2()` method and then `.unwrap().extract().unwrap()` the result.
79///
80/// ### Note:
81/// 1. The `"call macros"` will accept positional arguments as in the example below OR a tuple
82///    in the form of `python_function!(*args)` - the `*` is important, just as in python
83/// 2. The "Call macros" cannot currently cope with keyword arguments or a mixture of some
84///    positional arguments followed by *args
85/// 3. The macros will `panic!` if an error occurs due to incorrect argument types, missing arguments
86///    etc. - this is designed for use in tests, where panicing is the acceptable and required behaviour
87///
88/// ## Example usage:
89///
90/// ```no_run - expands to a test so is never actually run anyway
91/// use pyo3::prelude::*;
92/// use pyo3_testing::pyo3test;
93/// ##[pyfunction]
94/// ##[pyo3(name = "addone")]
95/// fn py_addone(num: isize) -> isize {
96///     num + 1
97/// }
98///
99/// ##[pymodule]
100/// ##[pyo3(name = "adders")]
101/// fn py_adders(module: &Bound<'_, PyModule>) -> PyResult<()> {
102///     module.add_function(wrap_pyfunction!(py_addone, module)?)?;
103///     Ok(())
104/// }
105///
106/// ##[pyo3test]
107/// ##[pyo3import(py_adders: from adders import addone)]
108/// fn test_pyo3test_simple_case() {
109///     let result = addone!(1_isize);
110///     assert_eq!(result, 2);
111/// }
112///
113/// ##[pyo3test]
114/// ##[pyo3import(py_adders: import adders)]
115/// fn test_pyo3test_import_module_only() {
116///     let result: isize = adders
117///         .getattr("addone")
118///         .unwrap()
119///         .call1((1_isize,))
120///         .unwrap()
121///         .extract()
122///         .unwrap();
123///     assert_eq!(result, 2);
124/// }
125/// ```
126#[proc_macro_attribute]
127pub fn pyo3test(attr: TokenStream1, input: TokenStream1) -> TokenStream1 {
128    impl_pyo3test(attr.into(), input.into()).into()
129}
130
131/// A proc macro to implement the equivalent of [pytest's `with raises`][1] context manager.
132///
133/// Use like this: `with_py_raises(ExpectedErrType, {code block which should raise error })`
134///
135/// [1]: https://docs.pytest.org/en/latest/getting-started.html#assert-that-a-certain-exception-is-raised
136///
137/// ## Note:
138///
139/// 1. The `ExpectedErrType` must be _in scope_ when calling the macro and must implement
140///    `std::from::From<E> for PyErr`
141/// 1. The code inside the block must be valid rust which returns a `PyResult<T>`. Currently it is not
142///    possible to use the autogenerated call macros provided by `#[pyo3test]`[macro@pyo3test].
143///    If you would like to see that feature, please let me know via [github][2]
144/// 1. Add `#[allow(unused_macros)]` to disable the warning that you have imported a python function
145///    but not called the associated macro.
146/// 1. The code will `panic!` if the incorrect error, or no error, is returned - this is designed for
147///    use in tests, where panicing is the acceptable and required behaviour
148///
149/// [2]: https://github.com/MusicalNinjas/pyo3-testing/issues/3
150///
151/// ## Example usage:
152///
153/// ```no_run - expands to a test so is never actually run anyway
154/// use pyo3::exceptions::PyTypeError;
155/// use pyo3_testing::{pyo3test, with_py_raises};
156/// ##[pyo3test]
157/// ##[allow(unused_macros)]
158/// ##[pyo3import(py_adders: from adders import addone)]
159/// fn test_raises() {
160///     //can't use `let result =` or `addone!()` here as they don't return a `Result`
161///     with_py_raises!(PyTypeError, { addone.call1(("4",)) });
162/// }
163/// ```
164#[proc_macro]
165pub fn with_py_raises(input: TokenStream1) -> TokenStream1 {
166    impl_with_py_raises(input.into()).into()
167}