Skip to main content

pyforge_macros/
lib.rs

1//! This crate declares only the proc macro attributes, as a crate defining proc macro attributes
2//! must not contain any other public items.
3
4#![cfg_attr(docsrs, feature(doc_cfg))]
5use proc_macro::TokenStream;
6use proc_macro2::TokenStream as TokenStream2;
7use pyforge_macros_backend::{
8    build_derive_from_pyobject, build_derive_into_pyobject, build_py_class, build_py_enum,
9    build_py_function, build_py_methods, pymodule_function_impl, pymodule_module_impl, PyClassArgs,
10    PyClassMethodsType, PyFunctionOptions, PyModuleOptions,
11};
12use quote::quote;
13use syn::{parse_macro_input, Item};
14
15/// A proc macro used to implement Python modules.
16///
17/// The name of the module will be taken from the module name, unless `#[pyo3(name = "my_name")]`
18/// is also annotated on the module to override the name. **Important**: the module name should
19/// match the `lib.name` setting in `Cargo.toml`, so that Python is able to import the module
20/// without needing a custom import loader.
21///
22/// Modules annotated with `#[pymodule]` can also be annotated with the following:
23///
24/// |  Annotation  |  Description |
25/// | :-  | :- |
26/// | `#[pyo3(name = "...")]` | Defines the name of the module in Python. |
27/// | `#[pyo3(submodule)]`    | Skips adding a `PyInit_` FFI symbol to the compiled binary. |
28/// | `#[pyo3(module = "...")]` | Defines the Python `dotted.path` to the parent module for use in introspection. |
29/// | `#[pyo3(crate = "pyforge")]` | Defines the path to PyForge to use code generated by the macro. |
30/// | `#[pyo3(gil_used = true)]` | Declares the GIL is needed to run this module safely under free-threaded Python. |
31///
32/// For more on creating Python modules see the [module section of the guide][1].
33///
34/// It is also possible to use this macro on functions but this is a deprecated usage. In this case,
35/// due to technical limitations on how `#[pymodule]` is implemented, a function marked
36/// `#[pymodule]` cannot have a module with the same name in the same scope. (The
37/// `#[pymodule]` implementation generates a hidden module with the same name containing
38/// metadata about the module, which is used by `wrap_pymodule!`).
39///
40#[doc = concat!("[1]: https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/module.html")]
41#[proc_macro_attribute]
42pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream {
43    let options = parse_macro_input!(args as PyModuleOptions);
44
45    let mut ast = parse_macro_input!(input as Item);
46    let expanded = match &mut ast {
47        Item::Mod(module) => {
48            match pymodule_module_impl(module, options) {
49                // #[pymodule] on a module will rebuild the original ast, so we don't emit it here
50                Ok(expanded) => return expanded.into(),
51                Err(e) => Err(e),
52            }
53        }
54        Item::Fn(function) => pymodule_function_impl(function, options),
55        unsupported => Err(syn::Error::new_spanned(
56            unsupported,
57            "#[pymodule] only supports modules and functions.",
58        )),
59    }
60    .unwrap_or_compile_error();
61
62    quote!(
63        #ast
64        #expanded
65    )
66    .into()
67}
68
69#[proc_macro_attribute]
70pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream {
71    let item = parse_macro_input!(input as Item);
72    match item {
73        Item::Struct(struct_) => pyclass_impl(attr, struct_, methods_type()),
74        Item::Enum(enum_) => pyclass_enum_impl(attr, enum_, methods_type()),
75        unsupported => {
76            syn::Error::new_spanned(unsupported, "#[pyclass] only supports structs and enums.")
77                .into_compile_error()
78                .into()
79        }
80    }
81}
82
83/// A proc macro used to expose methods to Python.
84///
85/// Methods within a `#[pymethods]` block can be annotated with  as well as the following:
86///
87/// |  Annotation  |  Description |
88/// | :-  | :- |
89/// | [`#[new]`][4]  | Defines the class constructor, like Python's `__new__` method. |
90/// | [`#[getter]`][5] and [`#[setter]`][5] | These define getters and setters, similar to Python's `@property` decorator. This is useful for getters/setters that require computation or side effects; if that is not the case consider using [`#[pyo3(get, set)]`][12] on the struct's field(s).|
91/// | [`#[staticmethod]`][6]| Defines the method as a staticmethod, like Python's `@staticmethod` decorator.|
92/// | [`#[classmethod]`][7]  | Defines the method as a classmethod, like Python's `@classmethod` decorator.|
93/// | [`#[classattr]`][9]  | Defines a class variable. |
94/// | [`#[args]`][10]  | Deprecated way to define a method's default arguments and allows the function to receive `*args` and `**kwargs`. Use `#[pyo3(signature = (...))]` instead. |
95/// | <nobr>[`#[pyo3(<option> = <value>)`][11]</nobr> | Any of the `#[pyo3]` options supported on [`macro@pyfunction`]. |
96///
97/// For more on creating class methods,
98/// see the [class section of the guide][1].
99///
100/// If the [`multiple-pymethods`][2] feature is enabled, it is possible to implement
101/// multiple `#[pymethods]` blocks for a single `#[pyclass]`.
102/// This will add a transitive dependency on the [`inventory`][3] crate.
103///
104#[doc = concat!("[1]: https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/class.html#instance-methods")]
105#[doc = concat!("[2]: https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/features.html#multiple-pymethods")]
106/// [3]: https://docs.rs/inventory/
107#[doc = concat!("[4]: https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/class.html#constructor")]
108#[doc = concat!("[5]: https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/class.html#object-properties-using-getter-and-setter")]
109#[doc = concat!("[6]: https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/class.html#static-methods")]
110#[doc = concat!("[7]: https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/class.html#class-methods")]
111#[doc = concat!("[8]: https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/class.html#callable-objects")]
112#[doc = concat!("[9]: https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/class.html#class-attributes")]
113#[doc = concat!("[10]: https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/class.html#method-arguments")]
114#[doc = concat!("[11]: https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/function.html#function-options")]
115#[doc = concat!("[12]: https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/class.html#object-properties-using-pyo3get-set")]
116#[proc_macro_attribute]
117pub fn pymethods(attr: TokenStream, input: TokenStream) -> TokenStream {
118    let methods_type = if cfg!(feature = "multiple-pymethods") {
119        PyClassMethodsType::Inventory
120    } else {
121        PyClassMethodsType::Specialization
122    };
123    pymethods_impl(attr, input, methods_type)
124}
125
126/// A proc macro used to expose Rust functions to Python.
127///
128/// Functions annotated with `#[pyfunction]` can also be annotated with the following `#[pyo3]`
129/// options:
130///
131/// |  Annotation  |  Description |
132/// | :-  | :- |
133/// | `#[pyo3(name = "...")]` | Defines the name of the function in Python. |
134/// | `#[pyo3(text_signature = "...")]` | Defines the `__text_signature__` attribute of the function in Python. |
135/// | `#[pyo3(pass_module)]` | Passes the module containing the function as a `&PyModule` first argument to the function. |
136/// | `#[pyo3(warn(message = "...", category = ...))]` | Generate warning given a message and a category |
137///
138/// For more on exposing functions see the [function section of the guide][1].
139///
140/// Due to technical limitations on how `#[pyfunction]` is implemented, a function marked
141/// `#[pyfunction]` cannot have a module with the same name in the same scope. (The
142/// `#[pyfunction]` implementation generates a hidden module with the same name containing
143/// metadata about the function, which is used by `wrap_pyfunction!`).
144///
145#[doc = concat!("[1]: https://github.com/abdulwahed-sweden/pyforge/v", env!("CARGO_PKG_VERSION"), "/function.html")]
146#[proc_macro_attribute]
147pub fn pyfunction(attr: TokenStream, input: TokenStream) -> TokenStream {
148    let mut ast = parse_macro_input!(input as syn::ItemFn);
149    let options = parse_macro_input!(attr as PyFunctionOptions);
150
151    let expanded = build_py_function(&mut ast, options).unwrap_or_compile_error();
152
153    quote!(
154        #ast
155        #expanded
156    )
157    .into()
158}
159
160#[proc_macro_derive(IntoPyObject, attributes(pyo3))]
161pub fn derive_into_py_object(item: TokenStream) -> TokenStream {
162    let ast = parse_macro_input!(item as syn::DeriveInput);
163    let expanded = build_derive_into_pyobject::<false>(&ast).unwrap_or_compile_error();
164    quote!(
165        #expanded
166    )
167    .into()
168}
169
170#[proc_macro_derive(IntoPyObjectRef, attributes(pyo3))]
171pub fn derive_into_py_object_ref(item: TokenStream) -> TokenStream {
172    let ast = parse_macro_input!(item as syn::DeriveInput);
173    let expanded =
174        pyforge_macros_backend::build_derive_into_pyobject::<true>(&ast).unwrap_or_compile_error();
175    quote!(
176        #expanded
177    )
178    .into()
179}
180
181#[proc_macro_derive(FromPyObject, attributes(pyo3))]
182pub fn derive_from_py_object(item: TokenStream) -> TokenStream {
183    let ast = parse_macro_input!(item as syn::DeriveInput);
184    let expanded = build_derive_from_pyobject(&ast).unwrap_or_compile_error();
185    quote!(
186        #expanded
187    )
188    .into()
189}
190
191fn pyclass_impl(
192    attrs: TokenStream,
193    mut ast: syn::ItemStruct,
194    methods_type: PyClassMethodsType,
195) -> TokenStream {
196    let args = parse_macro_input!(attrs with PyClassArgs::parse_struct_args);
197    let expanded = build_py_class(&mut ast, args, methods_type).unwrap_or_compile_error();
198
199    quote!(
200        #ast
201        #expanded
202    )
203    .into()
204}
205
206fn pyclass_enum_impl(
207    attrs: TokenStream,
208    mut ast: syn::ItemEnum,
209    methods_type: PyClassMethodsType,
210) -> TokenStream {
211    let args = parse_macro_input!(attrs with PyClassArgs::parse_enum_args);
212    let expanded = build_py_enum(&mut ast, args, methods_type).unwrap_or_compile_error();
213
214    quote!(
215        #ast
216        #expanded
217    )
218    .into()
219}
220
221fn pymethods_impl(
222    attr: TokenStream,
223    input: TokenStream,
224    methods_type: PyClassMethodsType,
225) -> TokenStream {
226    let mut ast = parse_macro_input!(input as syn::ItemImpl);
227    // Apply all options as a #[pyo3] attribute on the ItemImpl
228    // e.g. #[pymethods(crate = "crate")] impl Foo { }
229    // -> #[pyo3(crate = "crate")] impl Foo { }
230    let attr: TokenStream2 = attr.into();
231    ast.attrs.push(syn::parse_quote!( #[pyo3(#attr)] ));
232    let expanded = build_py_methods(&mut ast, methods_type).unwrap_or_compile_error();
233
234    quote!(
235        #ast
236        #expanded
237    )
238    .into()
239}
240
241fn methods_type() -> PyClassMethodsType {
242    if cfg!(feature = "multiple-pymethods") {
243        PyClassMethodsType::Inventory
244    } else {
245        PyClassMethodsType::Specialization
246    }
247}
248
249trait UnwrapOrCompileError {
250    fn unwrap_or_compile_error(self) -> TokenStream2;
251}
252
253impl UnwrapOrCompileError for syn::Result<TokenStream2> {
254    fn unwrap_or_compile_error(self) -> TokenStream2 {
255        self.unwrap_or_else(|e| e.into_compile_error())
256    }
257}