Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions guide/src/class.md
Original file line number Diff line number Diff line change
Expand Up @@ -1430,6 +1430,9 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass {
type WeakRef = pyo3::impl_::pyclass::PyClassDummySlot;
type BaseNativeType = pyo3::PyAny;

const RAW_DOC: &'static std::ffi::CStr = pyo3::ffi::c_str!("...");
const DOC: &'static std::ffi::CStr = pyo3::ffi::c_str!("...");

fn items_iter() -> pyo3::impl_::pyclass::PyClassItemsIter {
use pyo3::impl_::pyclass::*;
let collector = PyClassImplCollector::<MyClass>::new();
Expand All @@ -1442,15 +1445,6 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass {
static TYPE_OBJECT: LazyTypeObject<MyClass> = LazyTypeObject::new();
&TYPE_OBJECT
}

fn doc(py: Python<'_>) -> pyo3::PyResult<&'static ::std::ffi::CStr> {
use pyo3::impl_::pyclass::*;
static DOC: pyo3::sync::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = pyo3::sync::GILOnceCell::new();
DOC.get_or_try_init(py, || {
let collector = PyClassImplCollector::<Self>::new();
build_pyclass_doc(<MyClass as pyo3::PyTypeInfo>::NAME, pyo3::ffi::c_str!(""), collector.new_text_signature())
}).map(::std::ops::Deref::deref)
}
}

# Python::attach(|py| {
Expand Down
1 change: 1 addition & 0 deletions newsfragments/5286.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Move `#[pyclass]` docstring formatting from import time to compile time.
2 changes: 2 additions & 0 deletions pyo3-build-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,9 @@ pub fn print_feature_cfgs() {
// https://github.com/rust-lang/rust/issues/124651 just in case
print_feature_cfg(79, "diagnostic_namespace");
print_feature_cfg(83, "io_error_more");
print_feature_cfg(83, "mut_ref_in_const_fn");
print_feature_cfg(85, "fn_ptr_eq");
print_feature_cfg(86, "from_bytes_with_nul_error");
}

/// Registers `pyo3`s config names as reachable cfg expressions
Expand Down
2 changes: 1 addition & 1 deletion pyo3-macros-backend/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,7 @@ impl<'a> FnSpec<'a> {
}

/// Forwards to [utils::get_doc] with the text signature of this spec.
pub fn get_doc(&self, attrs: &[syn::Attribute], ctx: &Ctx) -> PythonDoc {
pub fn get_doc(&self, attrs: &[syn::Attribute], ctx: &Ctx) -> syn::Result<PythonDoc> {
let text_signature = self
.text_signature_call_signature()
.map(|sig| format!("{}{}", self.python_name, sig));
Expand Down
4 changes: 2 additions & 2 deletions pyo3-macros-backend/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ pub fn pymodule_module_impl(
options.take_pyo3_options(attrs)?;
let ctx = &Ctx::new(&options.krate, None);
let Ctx { pyo3_path, .. } = ctx;
let doc = get_doc(attrs, None, ctx);
let doc = get_doc(attrs, None, ctx)?;
let name = options
.name
.map_or_else(|| ident.unraw(), |name| name.value.0);
Expand Down Expand Up @@ -453,7 +453,7 @@ pub fn pymodule_function_impl(
.name
.map_or_else(|| ident.unraw(), |name| name.value.0);
let vis = &function.vis;
let doc = get_doc(&function.attrs, None, ctx);
let doc = get_doc(&function.attrs, None, ctx)?;

let initialization = module_initialization(
&name,
Expand Down
29 changes: 17 additions & 12 deletions pyo3-macros-backend/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ pub fn build_py_class(
args.options.take_pyo3_options(&mut class.attrs)?;

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

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

let doc = utils::get_doc(&enum_.attrs, None, ctx);
let doc = utils::get_doc(&enum_.attrs, None, ctx)?;
let enum_ = PyClassEnum::new(enum_)?;
impl_enum(enum_, &args, doc, method_type, ctx)
}
Expand Down Expand Up @@ -1758,7 +1758,7 @@ fn complex_enum_variant_field_getter<'a>(
let property_type = crate::pymethod::PropertyType::Function {
self_type: &self_type,
spec: &spec,
doc: crate::get_doc(&[], None, ctx),
doc: crate::get_doc(&[], None, ctx)?,
};

let getter = crate::pymethod::impl_py_getter_def(variant_cls_type, property_type, ctx)?;
Expand Down Expand Up @@ -2056,7 +2056,7 @@ fn pyclass_class_geitem(
let class_geitem_method = crate::pymethod::impl_py_method_def(
cls,
&spec,
&spec.get_doc(&class_geitem_impl.attrs, ctx),
&spec.get_doc(&class_geitem_impl.attrs, ctx)?,
Some(quote!(#pyo3_path::ffi::METH_CLASS)),
ctx,
)?;
Expand Down Expand Up @@ -2387,14 +2387,19 @@ impl<'a> PyClassImplsBuilder<'a> {
PyClassItemsIter::new(&INTRINSIC_ITEMS, #pymethods_items)
}

fn doc(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<&'static ::std::ffi::CStr> {
use #pyo3_path::impl_::pyclass::*;
static DOC: #pyo3_path::sync::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = #pyo3_path::sync::GILOnceCell::new();
DOC.get_or_try_init(py, || {
let collector = PyClassImplCollector::<Self>::new();
build_pyclass_doc(<Self as #pyo3_path::PyTypeInfo>::NAME, #doc, collector.new_text_signature())
}).map(::std::ops::Deref::deref)
}
const RAW_DOC: &'static ::std::ffi::CStr = #doc;

const DOC: &'static ::std::ffi::CStr = {
use #pyo3_path::impl_ as impl_;
use impl_::pyclass::Probe as _;
const DOC_PIECES: &'static [&'static [u8]] = impl_::pyclass::doc::PyClassDocGenerator::<
#cls,
{ impl_::pyclass::HasNewTextSignature::<#cls>::VALUE }
>::DOC_PIECES;
const LEN: usize = impl_::concat::combined_len_bytes(DOC_PIECES);
const DOC: &'static [u8] = &impl_::concat::combine_bytes_to_array::<LEN>(DOC_PIECES);
impl_::pyclass::doc::doc_bytes_as_cstr(DOC)
};

#dict_offset

Expand Down
2 changes: 1 addition & 1 deletion pyo3-macros-backend/src/pyfunction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ pub fn impl_wrap_pyfunction(
);
}
let wrapper = spec.get_wrapper_function(&wrapper_ident, None, ctx)?;
let methoddef = spec.get_methoddef(wrapper_ident, &spec.get_doc(&func.attrs, ctx), ctx);
let methoddef = spec.get_methoddef(wrapper_ident, &spec.get_doc(&func.attrs, ctx)?, ctx);

let wrapped_pyfunction = quote! {
// Create a module with the same name as the `#[pyfunction]` - this way `use <the function>`
Expand Down
41 changes: 20 additions & 21 deletions pyo3-macros-backend/src/pymethod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,21 +236,21 @@ pub fn gen_py_method(
(_, FnType::Fn(_)) => GeneratedPyMethod::Method(impl_py_method_def(
cls,
spec,
&spec.get_doc(meth_attrs, ctx),
&spec.get_doc(meth_attrs, ctx)?,
None,
ctx,
)?),
(_, FnType::FnClass(_)) => GeneratedPyMethod::Method(impl_py_method_def(
cls,
spec,
&spec.get_doc(meth_attrs, ctx),
&spec.get_doc(meth_attrs, ctx)?,
Some(quote!(#pyo3_path::ffi::METH_CLASS)),
ctx,
)?),
(_, FnType::FnStatic) => GeneratedPyMethod::Method(impl_py_method_def(
cls,
spec,
&spec.get_doc(meth_attrs, ctx),
&spec.get_doc(meth_attrs, ctx)?,
Some(quote!(#pyo3_path::ffi::METH_STATIC)),
ctx,
)?),
Expand All @@ -264,7 +264,7 @@ pub fn gen_py_method(
PropertyType::Function {
self_type,
spec,
doc: spec.get_doc(meth_attrs, ctx),
doc: spec.get_doc(meth_attrs, ctx)?,
},
ctx,
)?),
Expand All @@ -273,7 +273,7 @@ pub fn gen_py_method(
PropertyType::Function {
self_type,
spec,
doc: spec.get_doc(meth_attrs, ctx),
doc: spec.get_doc(meth_attrs, ctx)?,
},
ctx,
)?),
Expand Down Expand Up @@ -360,10 +360,14 @@ pub fn impl_py_method_def_new(
// Use just the text_signature_call_signature() because the class' Python name
// isn't known to `#[pymethods]` - that has to be attached at runtime from the PyClassImpl
// trait implementation created by `#[pyclass]`.
let text_signature_body = spec.text_signature_call_signature().map_or_else(
|| quote!(::std::option::Option::None),
|text_signature| quote!(::std::option::Option::Some(#text_signature)),
);
let text_signature_impl = spec.text_signature_call_signature().map(|text_signature| {
quote! {
#[allow(unknown_lints, non_local_definitions)]
impl #pyo3_path::impl_::pyclass::doc::PyClassNewTextSignature for #cls {
const TEXT_SIGNATURE: &'static str = #text_signature;
}
}
});
let slot_def = quote! {
#pyo3_path::ffi::PyType_Slot {
slot: #pyo3_path::ffi::Py_tp_new,
Expand All @@ -373,13 +377,8 @@ pub fn impl_py_method_def_new(
args: *mut #pyo3_path::ffi::PyObject,
kwargs: *mut #pyo3_path::ffi::PyObject,
) -> *mut #pyo3_path::ffi::PyObject {
#[allow(unknown_lints, non_local_definitions)]
impl #pyo3_path::impl_::pyclass::PyClassNewTextSignature<#cls> for #pyo3_path::impl_::pyclass::PyClassImplCollector<#cls> {
#[inline]
fn new_text_signature(self) -> ::std::option::Option<&'static str> {
#text_signature_body
}
}

#text_signature_impl

#pyo3_path::impl_::trampoline::newfunc(
subtype,
Expand Down Expand Up @@ -627,7 +626,7 @@ pub fn impl_py_setter_def(
) -> Result<MethodAndMethodDef> {
let Ctx { pyo3_path, .. } = ctx;
let python_name = property_type.null_terminated_python_name(ctx)?;
let doc = property_type.doc(ctx);
let doc = property_type.doc(ctx)?;
let mut holders = Holders::new();
let setter_impl = match property_type {
PropertyType::Descriptor {
Expand Down Expand Up @@ -815,7 +814,7 @@ pub fn impl_py_getter_def(
) -> Result<MethodAndMethodDef> {
let Ctx { pyo3_path, .. } = ctx;
let python_name = property_type.null_terminated_python_name(ctx)?;
let doc = property_type.doc(ctx);
let doc = property_type.doc(ctx)?;

let mut cfg_attrs = TokenStream::new();
if let PropertyType::Descriptor { field, .. } = &property_type {
Expand Down Expand Up @@ -978,12 +977,12 @@ impl PropertyType<'_> {
}
}

fn doc(&self, ctx: &Ctx) -> Cow<'_, PythonDoc> {
fn doc(&self, ctx: &Ctx) -> Result<Cow<'_, PythonDoc>> {
match self {
PropertyType::Descriptor { field, .. } => {
Cow::Owned(utils::get_doc(&field.attrs, None, ctx))
utils::get_doc(&field.attrs, None, ctx).map(Cow::Owned)
}
PropertyType::Function { doc, .. } => Cow::Borrowed(doc),
PropertyType::Function { doc, .. } => Ok(Cow::Borrowed(doc)),
}
}
}
Expand Down
35 changes: 25 additions & 10 deletions pyo3-macros-backend/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::attributes::{CrateAttribute, RenamingRule};
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use quote::{quote, quote_spanned, ToTokens};
use std::ffi::CString;
use syn::spanned::Spanned;
use syn::visit_mut::VisitMut;
Expand Down Expand Up @@ -132,7 +132,7 @@ pub fn get_doc(
attrs: &[syn::Attribute],
mut text_signature: Option<String>,
ctx: &Ctx,
) -> PythonDoc {
) -> syn::Result<PythonDoc> {
let Ctx { pyo3_path, .. } = ctx;
// insert special divider between `__text_signature__` and doc
// (assume text_signature is itself well-formed)
Expand All @@ -143,10 +143,15 @@ pub fn get_doc(
let mut parts = Punctuated::<TokenStream, Token![,]>::new();
let mut first = true;
let mut current_part = text_signature.unwrap_or_default();
let mut current_part_span = None;

for attr in attrs {
if attr.path().is_ident("doc") {
if let Ok(nv) = attr.meta.require_name_value() {
current_part_span = match current_part_span {
None => Some(nv.value.span()),
Some(span) => span.join(nv.value.span()),
};
if !first {
current_part.push('\n');
} else {
Expand All @@ -164,7 +169,7 @@ pub fn get_doc(
} else {
// This is probably a macro doc from Rust 1.54, e.g. #[doc = include_str!(...)]
// Reset the string buffer, write that part, and then push this macro part too.
parts.push(current_part.to_token_stream());
parts.push(quote_spanned!(current_part_span.unwrap_or(Span::call_site()) => #current_part));
current_part.clear();
parts.push(nv.value.to_token_stream());
}
Expand All @@ -175,7 +180,9 @@ pub fn get_doc(
if !parts.is_empty() {
// Doc contained macro pieces - return as `concat!` expression
if !current_part.is_empty() {
parts.push(current_part.to_token_stream());
parts.push(
quote_spanned!(current_part_span.unwrap_or(Span::call_site()) => #current_part),
);
}

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

PythonDoc(PythonDocKind::Tokens(
Ok(PythonDoc(PythonDocKind::Tokens(
quote!(#pyo3_path::ffi::c_str!(#tokens)),
))
)))
} else {
// Just a string doc - return directly with nul terminator
let docs = CString::new(current_part).unwrap();
PythonDoc(PythonDocKind::LitCStr(LitCStr::new(
let docs = CString::new(current_part).map_err(|e| {
syn::Error::new(
current_part_span.unwrap_or(Span::call_site()),
format!(
"Python doc may not contain nul byte, found nul at position {}",
e.nul_position()
),
)
})?;
Ok(PythonDoc(PythonDocKind::LitCStr(LitCStr::new(
docs,
Span::call_site(),
current_part_span.unwrap_or(Span::call_site()),
ctx,
)))
))))
}
}

Expand Down
1 change: 0 additions & 1 deletion src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
//! breaking semver guarantees.

pub mod callback;
#[cfg(feature = "experimental-inspect")]
pub mod concat;
#[cfg(feature = "experimental-async")]
pub mod coroutine;
Expand Down
Loading
Loading