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