Skip to content

Commit c366646

Browse files
authored
move #[pyclass] docstring generation to compile time (#5286)
* move `#[pyclass]` docstring generation to compile time * improve error message * clean up to use cstr all the way through * fixup msrv * fixup final tests, newsfragment * runtime test coverage * use "copy_from_slice" (with a note for the future) * fix clippy beta
1 parent d265254 commit c366646

File tree

20 files changed

+464
-143
lines changed

20 files changed

+464
-143
lines changed

guide/src/class.md

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,9 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass {
14301430
type WeakRef = pyo3::impl_::pyclass::PyClassDummySlot;
14311431
type BaseNativeType = pyo3::PyAny;
14321432

1433+
const RAW_DOC: &'static std::ffi::CStr = pyo3::ffi::c_str!("...");
1434+
const DOC: &'static std::ffi::CStr = pyo3::ffi::c_str!("...");
1435+
14331436
fn items_iter() -> pyo3::impl_::pyclass::PyClassItemsIter {
14341437
use pyo3::impl_::pyclass::*;
14351438
let collector = PyClassImplCollector::<MyClass>::new();
@@ -1442,15 +1445,6 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass {
14421445
static TYPE_OBJECT: LazyTypeObject<MyClass> = LazyTypeObject::new();
14431446
&TYPE_OBJECT
14441447
}
1445-
1446-
fn doc(py: Python<'_>) -> pyo3::PyResult<&'static ::std::ffi::CStr> {
1447-
use pyo3::impl_::pyclass::*;
1448-
static DOC: pyo3::sync::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = pyo3::sync::GILOnceCell::new();
1449-
DOC.get_or_try_init(py, || {
1450-
let collector = PyClassImplCollector::<Self>::new();
1451-
build_pyclass_doc(<MyClass as pyo3::PyTypeInfo>::NAME, pyo3::ffi::c_str!(""), collector.new_text_signature())
1452-
}).map(::std::ops::Deref::deref)
1453-
}
14541448
}
14551449

14561450
# Python::attach(|py| {

newsfragments/5286.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Move `#[pyclass]` docstring formatting from import time to compile time.

pyo3-build-config/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,9 @@ pub fn print_feature_cfgs() {
186186
// https://github.com/rust-lang/rust/issues/124651 just in case
187187
print_feature_cfg(79, "diagnostic_namespace");
188188
print_feature_cfg(83, "io_error_more");
189+
print_feature_cfg(83, "mut_ref_in_const_fn");
189190
print_feature_cfg(85, "fn_ptr_eq");
191+
print_feature_cfg(86, "from_bytes_with_nul_error");
190192
}
191193

192194
/// Registers `pyo3`s config names as reachable cfg expressions

pyo3-macros-backend/src/method.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -976,7 +976,7 @@ impl<'a> FnSpec<'a> {
976976
}
977977

978978
/// Forwards to [utils::get_doc] with the text signature of this spec.
979-
pub fn get_doc(&self, attrs: &[syn::Attribute], ctx: &Ctx) -> PythonDoc {
979+
pub fn get_doc(&self, attrs: &[syn::Attribute], ctx: &Ctx) -> syn::Result<PythonDoc> {
980980
let text_signature = self
981981
.text_signature_call_signature()
982982
.map(|sig| format!("{}{}", self.python_name, sig));

pyo3-macros-backend/src/module.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ pub fn pymodule_module_impl(
115115
options.take_pyo3_options(attrs)?;
116116
let ctx = &Ctx::new(&options.krate, None);
117117
let Ctx { pyo3_path, .. } = ctx;
118-
let doc = get_doc(attrs, None, ctx);
118+
let doc = get_doc(attrs, None, ctx)?;
119119
let name = options
120120
.name
121121
.map_or_else(|| ident.unraw(), |name| name.value.0);
@@ -453,7 +453,7 @@ pub fn pymodule_function_impl(
453453
.name
454454
.map_or_else(|| ident.unraw(), |name| name.value.0);
455455
let vis = &function.vis;
456-
let doc = get_doc(&function.attrs, None, ctx);
456+
let doc = get_doc(&function.attrs, None, ctx)?;
457457

458458
let initialization = module_initialization(
459459
&name,

pyo3-macros-backend/src/pyclass.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ pub fn build_py_class(
250250
args.options.take_pyo3_options(&mut class.attrs)?;
251251

252252
let ctx = &Ctx::new(&args.options.krate, None);
253-
let doc = utils::get_doc(&class.attrs, None, ctx);
253+
let doc = utils::get_doc(&class.attrs, None, ctx)?;
254254

255255
if let Some(lt) = class.generics.lifetimes().next() {
256256
bail_spanned!(
@@ -521,7 +521,7 @@ pub fn build_py_enum(
521521
bail_spanned!(generic.span() => "enums do not support #[pyclass(generic)]");
522522
}
523523

524-
let doc = utils::get_doc(&enum_.attrs, None, ctx);
524+
let doc = utils::get_doc(&enum_.attrs, None, ctx)?;
525525
let enum_ = PyClassEnum::new(enum_)?;
526526
impl_enum(enum_, &args, doc, method_type, ctx)
527527
}
@@ -1758,7 +1758,7 @@ fn complex_enum_variant_field_getter<'a>(
17581758
let property_type = crate::pymethod::PropertyType::Function {
17591759
self_type: &self_type,
17601760
spec: &spec,
1761-
doc: crate::get_doc(&[], None, ctx),
1761+
doc: crate::get_doc(&[], None, ctx)?,
17621762
};
17631763

17641764
let getter = crate::pymethod::impl_py_getter_def(variant_cls_type, property_type, ctx)?;
@@ -2056,7 +2056,7 @@ fn pyclass_class_geitem(
20562056
let class_geitem_method = crate::pymethod::impl_py_method_def(
20572057
cls,
20582058
&spec,
2059-
&spec.get_doc(&class_geitem_impl.attrs, ctx),
2059+
&spec.get_doc(&class_geitem_impl.attrs, ctx)?,
20602060
Some(quote!(#pyo3_path::ffi::METH_CLASS)),
20612061
ctx,
20622062
)?;
@@ -2387,14 +2387,19 @@ impl<'a> PyClassImplsBuilder<'a> {
23872387
PyClassItemsIter::new(&INTRINSIC_ITEMS, #pymethods_items)
23882388
}
23892389

2390-
fn doc(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<&'static ::std::ffi::CStr> {
2391-
use #pyo3_path::impl_::pyclass::*;
2392-
static DOC: #pyo3_path::sync::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = #pyo3_path::sync::GILOnceCell::new();
2393-
DOC.get_or_try_init(py, || {
2394-
let collector = PyClassImplCollector::<Self>::new();
2395-
build_pyclass_doc(<Self as #pyo3_path::PyTypeInfo>::NAME, #doc, collector.new_text_signature())
2396-
}).map(::std::ops::Deref::deref)
2397-
}
2390+
const RAW_DOC: &'static ::std::ffi::CStr = #doc;
2391+
2392+
const DOC: &'static ::std::ffi::CStr = {
2393+
use #pyo3_path::impl_ as impl_;
2394+
use impl_::pyclass::Probe as _;
2395+
const DOC_PIECES: &'static [&'static [u8]] = impl_::pyclass::doc::PyClassDocGenerator::<
2396+
#cls,
2397+
{ impl_::pyclass::HasNewTextSignature::<#cls>::VALUE }
2398+
>::DOC_PIECES;
2399+
const LEN: usize = impl_::concat::combined_len_bytes(DOC_PIECES);
2400+
const DOC: &'static [u8] = &impl_::concat::combine_bytes_to_array::<LEN>(DOC_PIECES);
2401+
impl_::pyclass::doc::doc_bytes_as_cstr(DOC)
2402+
};
23982403

23992404
#dict_offset
24002405

pyo3-macros-backend/src/pyfunction.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ pub fn impl_wrap_pyfunction(
425425
);
426426
}
427427
let wrapper = spec.get_wrapper_function(&wrapper_ident, None, ctx)?;
428-
let methoddef = spec.get_methoddef(wrapper_ident, &spec.get_doc(&func.attrs, ctx), ctx);
428+
let methoddef = spec.get_methoddef(wrapper_ident, &spec.get_doc(&func.attrs, ctx)?, ctx);
429429

430430
let wrapped_pyfunction = quote! {
431431
// Create a module with the same name as the `#[pyfunction]` - this way `use <the function>`

pyo3-macros-backend/src/pymethod.rs

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -236,21 +236,21 @@ pub fn gen_py_method(
236236
(_, FnType::Fn(_)) => GeneratedPyMethod::Method(impl_py_method_def(
237237
cls,
238238
spec,
239-
&spec.get_doc(meth_attrs, ctx),
239+
&spec.get_doc(meth_attrs, ctx)?,
240240
None,
241241
ctx,
242242
)?),
243243
(_, FnType::FnClass(_)) => GeneratedPyMethod::Method(impl_py_method_def(
244244
cls,
245245
spec,
246-
&spec.get_doc(meth_attrs, ctx),
246+
&spec.get_doc(meth_attrs, ctx)?,
247247
Some(quote!(#pyo3_path::ffi::METH_CLASS)),
248248
ctx,
249249
)?),
250250
(_, FnType::FnStatic) => GeneratedPyMethod::Method(impl_py_method_def(
251251
cls,
252252
spec,
253-
&spec.get_doc(meth_attrs, ctx),
253+
&spec.get_doc(meth_attrs, ctx)?,
254254
Some(quote!(#pyo3_path::ffi::METH_STATIC)),
255255
ctx,
256256
)?),
@@ -264,7 +264,7 @@ pub fn gen_py_method(
264264
PropertyType::Function {
265265
self_type,
266266
spec,
267-
doc: spec.get_doc(meth_attrs, ctx),
267+
doc: spec.get_doc(meth_attrs, ctx)?,
268268
},
269269
ctx,
270270
)?),
@@ -273,7 +273,7 @@ pub fn gen_py_method(
273273
PropertyType::Function {
274274
self_type,
275275
spec,
276-
doc: spec.get_doc(meth_attrs, ctx),
276+
doc: spec.get_doc(meth_attrs, ctx)?,
277277
},
278278
ctx,
279279
)?),
@@ -360,10 +360,14 @@ pub fn impl_py_method_def_new(
360360
// Use just the text_signature_call_signature() because the class' Python name
361361
// isn't known to `#[pymethods]` - that has to be attached at runtime from the PyClassImpl
362362
// trait implementation created by `#[pyclass]`.
363-
let text_signature_body = spec.text_signature_call_signature().map_or_else(
364-
|| quote!(::std::option::Option::None),
365-
|text_signature| quote!(::std::option::Option::Some(#text_signature)),
366-
);
363+
let text_signature_impl = spec.text_signature_call_signature().map(|text_signature| {
364+
quote! {
365+
#[allow(unknown_lints, non_local_definitions)]
366+
impl #pyo3_path::impl_::pyclass::doc::PyClassNewTextSignature for #cls {
367+
const TEXT_SIGNATURE: &'static str = #text_signature;
368+
}
369+
}
370+
});
367371
let slot_def = quote! {
368372
#pyo3_path::ffi::PyType_Slot {
369373
slot: #pyo3_path::ffi::Py_tp_new,
@@ -373,13 +377,8 @@ pub fn impl_py_method_def_new(
373377
args: *mut #pyo3_path::ffi::PyObject,
374378
kwargs: *mut #pyo3_path::ffi::PyObject,
375379
) -> *mut #pyo3_path::ffi::PyObject {
376-
#[allow(unknown_lints, non_local_definitions)]
377-
impl #pyo3_path::impl_::pyclass::PyClassNewTextSignature<#cls> for #pyo3_path::impl_::pyclass::PyClassImplCollector<#cls> {
378-
#[inline]
379-
fn new_text_signature(self) -> ::std::option::Option<&'static str> {
380-
#text_signature_body
381-
}
382-
}
380+
381+
#text_signature_impl
383382

384383
#pyo3_path::impl_::trampoline::newfunc(
385384
subtype,
@@ -627,7 +626,7 @@ pub fn impl_py_setter_def(
627626
) -> Result<MethodAndMethodDef> {
628627
let Ctx { pyo3_path, .. } = ctx;
629628
let python_name = property_type.null_terminated_python_name(ctx)?;
630-
let doc = property_type.doc(ctx);
629+
let doc = property_type.doc(ctx)?;
631630
let mut holders = Holders::new();
632631
let setter_impl = match property_type {
633632
PropertyType::Descriptor {
@@ -815,7 +814,7 @@ pub fn impl_py_getter_def(
815814
) -> Result<MethodAndMethodDef> {
816815
let Ctx { pyo3_path, .. } = ctx;
817816
let python_name = property_type.null_terminated_python_name(ctx)?;
818-
let doc = property_type.doc(ctx);
817+
let doc = property_type.doc(ctx)?;
819818

820819
let mut cfg_attrs = TokenStream::new();
821820
if let PropertyType::Descriptor { field, .. } = &property_type {
@@ -978,12 +977,12 @@ impl PropertyType<'_> {
978977
}
979978
}
980979

981-
fn doc(&self, ctx: &Ctx) -> Cow<'_, PythonDoc> {
980+
fn doc(&self, ctx: &Ctx) -> Result<Cow<'_, PythonDoc>> {
982981
match self {
983982
PropertyType::Descriptor { field, .. } => {
984-
Cow::Owned(utils::get_doc(&field.attrs, None, ctx))
983+
utils::get_doc(&field.attrs, None, ctx).map(Cow::Owned)
985984
}
986-
PropertyType::Function { doc, .. } => Cow::Borrowed(doc),
985+
PropertyType::Function { doc, .. } => Ok(Cow::Borrowed(doc)),
987986
}
988987
}
989988
}

pyo3-macros-backend/src/utils.rs

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::attributes::{CrateAttribute, RenamingRule};
22
use proc_macro2::{Span, TokenStream};
3-
use quote::{quote, ToTokens};
3+
use quote::{quote, quote_spanned, ToTokens};
44
use std::ffi::CString;
55
use syn::spanned::Spanned;
66
use syn::visit_mut::VisitMut;
@@ -132,7 +132,7 @@ pub fn get_doc(
132132
attrs: &[syn::Attribute],
133133
mut text_signature: Option<String>,
134134
ctx: &Ctx,
135-
) -> PythonDoc {
135+
) -> syn::Result<PythonDoc> {
136136
let Ctx { pyo3_path, .. } = ctx;
137137
// insert special divider between `__text_signature__` and doc
138138
// (assume text_signature is itself well-formed)
@@ -143,10 +143,15 @@ pub fn get_doc(
143143
let mut parts = Punctuated::<TokenStream, Token![,]>::new();
144144
let mut first = true;
145145
let mut current_part = text_signature.unwrap_or_default();
146+
let mut current_part_span = None;
146147

147148
for attr in attrs {
148149
if attr.path().is_ident("doc") {
149150
if let Ok(nv) = attr.meta.require_name_value() {
151+
current_part_span = match current_part_span {
152+
None => Some(nv.value.span()),
153+
Some(span) => span.join(nv.value.span()),
154+
};
150155
if !first {
151156
current_part.push('\n');
152157
} else {
@@ -164,7 +169,7 @@ pub fn get_doc(
164169
} else {
165170
// This is probably a macro doc from Rust 1.54, e.g. #[doc = include_str!(...)]
166171
// Reset the string buffer, write that part, and then push this macro part too.
167-
parts.push(current_part.to_token_stream());
172+
parts.push(quote_spanned!(current_part_span.unwrap_or(Span::call_site()) => #current_part));
168173
current_part.clear();
169174
parts.push(nv.value.to_token_stream());
170175
}
@@ -175,7 +180,9 @@ pub fn get_doc(
175180
if !parts.is_empty() {
176181
// Doc contained macro pieces - return as `concat!` expression
177182
if !current_part.is_empty() {
178-
parts.push(current_part.to_token_stream());
183+
parts.push(
184+
quote_spanned!(current_part_span.unwrap_or(Span::call_site()) => #current_part),
185+
);
179186
}
180187

181188
let mut tokens = TokenStream::new();
@@ -187,17 +194,25 @@ pub fn get_doc(
187194
syn::token::Comma(Span::call_site()).to_tokens(tokens);
188195
});
189196

190-
PythonDoc(PythonDocKind::Tokens(
197+
Ok(PythonDoc(PythonDocKind::Tokens(
191198
quote!(#pyo3_path::ffi::c_str!(#tokens)),
192-
))
199+
)))
193200
} else {
194201
// Just a string doc - return directly with nul terminator
195-
let docs = CString::new(current_part).unwrap();
196-
PythonDoc(PythonDocKind::LitCStr(LitCStr::new(
202+
let docs = CString::new(current_part).map_err(|e| {
203+
syn::Error::new(
204+
current_part_span.unwrap_or(Span::call_site()),
205+
format!(
206+
"Python doc may not contain nul byte, found nul at position {}",
207+
e.nul_position()
208+
),
209+
)
210+
})?;
211+
Ok(PythonDoc(PythonDocKind::LitCStr(LitCStr::new(
197212
docs,
198-
Span::call_site(),
213+
current_part_span.unwrap_or(Span::call_site()),
199214
ctx,
200-
)))
215+
))))
201216
}
202217
}
203218

src/impl_.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
//! breaking semver guarantees.
88
99
pub mod callback;
10-
#[cfg(feature = "experimental-inspect")]
1110
pub mod concat;
1211
#[cfg(feature = "experimental-async")]
1312
pub mod coroutine;

0 commit comments

Comments
 (0)