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
8 changes: 7 additions & 1 deletion crates/derive-impl/src/class_docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ use rustpython_doc::DocRef;
/// Modules `get_qualified` consults for `module`.
fn module_aliases(module: &str) -> Vec<String> {
let mut aliases = vec![module.to_owned()];
if module == "os" || module == "_os" {
// Same module order as `get_qualified`: `os` then posix then nt, and
// posix/nt each fall back to the other.
if module == "os" {
aliases.push("posix".to_owned());
aliases.push("nt".to_owned());
} else if module == "posix" {
aliases.push("nt".to_owned());
} else if module == "nt" {
aliases.push("posix".to_owned());
}
if let Some(rest) = module.strip_prefix('_')
&& !rest.is_empty()
Expand Down
75 changes: 65 additions & 10 deletions crates/derive-impl/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,23 @@ pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Resul
quote!(#holder::__OWN_METHOD_DEFS)
} else {
quote!(
rustpython_vm::function::PyMethodDef::__const_concat_arrays::<
{ #holder::__OWN_METHOD_DEFS.len() #(+ #with_method_defs.len())* },
>(&[#holder::__OWN_METHOD_DEFS, #(#with_method_defs,)*])
{
#[cfg(feature = "doc")]
{
::rustpython_vm::function::PyMethodDef::concat_with_attr_docs::<
{ #holder::__OWN_METHOD_DEFS.len() #(+ #with_method_defs.len())* },
>(
&[#holder::__OWN_METHOD_DEFS, #(#with_method_defs,)*],
<#payload_ty as ::rustpython_vm::class::PyClassDef>::ATTR_DOCS,
)
}
#[cfg(not(feature = "doc"))]
{
::rustpython_vm::function::PyMethodDef::__const_concat_arrays::<
{ #holder::__OWN_METHOD_DEFS.len() #(+ #with_method_defs.len())* },
>(&[#holder::__OWN_METHOD_DEFS, #(#with_method_defs,)*])
}
}
)
};
let internal_doc = class_internal_doc(
Expand Down Expand Up @@ -461,6 +475,34 @@ fn attr_doc_expr(self_ty: Option<&syn::Type>, attr: &str, rust_doc: Option<Strin
}
}

/// True when the method body is still taken from the owning class's table.
/// A non-empty Rust doc is the body. Otherwise the span is resolved here when
/// the impl can name its class; a missing class or a missing span stays pending.
fn doc_body_pending_expr(
self_ty: Option<&syn::Type>,
attr: &str,
rust_doc: Option<String>,
) -> TokenStream {
if rust_doc.as_ref().is_some_and(|doc| !doc.is_empty()) {
return quote!(false);
}
let Some(ty) = class_def_ty(self_ty) else {
return quote!(true);
};
quote! {
{
const FOUND: Option<(u32, u32)> = ::rustpython_vm::class::attr_doc(
<#ty as ::rustpython_vm::class::PyClassDef>::ATTR_DOCS,
#attr,
);
match FOUND {
Some((_, len)) if len != 0 => false,
_ => true,
}
}
}
}

fn type_matches_path(ty: &syn::Type, path: &syn::Path) -> bool {
// Compare by converting both to string representation for macro hygiene
let ty_str = quote!(#ty).to_string().replace(' ', "");
Expand Down Expand Up @@ -1224,23 +1266,27 @@ where
args.attrs.push(allow_attr);
}

let rust_doc = args.attrs.doc();
let doc = internal_doc_tokens(
func.sig(),
&py_name,
implicit_self,
attr_doc_expr(
args.context.self_ty_subst.as_ref(),
&py_name,
args.attrs.doc(),
rust_doc.clone(),
),
args.context.self_ty_subst.as_ref(),
None,
);
let doc_body_pending =
doc_body_pending_expr(args.context.self_ty_subst.as_ref(), &py_name, rust_doc);
args.context.method_items.add_item(MethodNurseryItem {
py_name,
cfgs: args.cfgs.to_vec(),
ident: ident.to_owned(),
doc,
doc_body_pending,
raw,
coexist,
attr_name: self.inner.attr_name,
Expand Down Expand Up @@ -1438,6 +1484,7 @@ struct MethodNurseryItem {
raw: bool,
coexist: bool,
doc: TokenStream,
doc_body_pending: TokenStream,
attr_name: AttrName,
call_flags: TokenStream,
}
Expand Down Expand Up @@ -1466,6 +1513,7 @@ impl ToTokens for MethodNursery {
let ident = &item.ident;
let cfgs = &item.cfgs;
let doc = &item.doc;
let doc_body_pending = &item.doc_body_pending;
let binding_flags = match &item.attr_name {
AttrName::Method => {
quote! { rustpython_vm::function::PyMethodFlags::METHOD }
Expand Down Expand Up @@ -1503,12 +1551,19 @@ impl ToTokens for MethodNursery {
};
inner_tokens.extend(quote! [
#(#cfgs)*
rustpython_vm::function::PyMethodDef::#method_new(
#py_name,
Self::#ident,
#flags,
#doc,
),
{
let mut def = rustpython_vm::function::PyMethodDef::#method_new(
#py_name,
Self::#ident,
#flags,
#doc,
);
#[cfg(feature = "doc")]
{
def.doc_body_pending = #doc_body_pending;
}
def
},
]);
}
let array: TokenTree = Group::new(Delimiter::Bracket, inner_tokens).into();
Expand Down
79 changes: 44 additions & 35 deletions crates/derive-impl/src/pymodule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,13 @@ impl FromStr for AttrName {
#[derive(Default)]
struct ModuleContext {
name: String,
/// `#[pymodule(sub)]` bodies are merged into the parent module, so the
/// Rust module name is not the Python module name.
/// `#[pymodule(sub)]` is merged into its parent. Doc lookup uses `name`
/// only when the attribute sets `name`. Without `name`, the Rust module
/// name is not a Python module, so the doc DB is not consulted. A `///`
/// doc on an item still applies.
is_sub: bool,
/// `true` when `#[pymodule(name = "...")]` set the Python module name.
explicit_name: bool,
function_items: FunctionNursery,
attribute_items: ItemNursery,
has_module_exec: bool,
Expand All @@ -164,6 +168,7 @@ pub(crate) fn impl_pymodule(args: PyModuleArgs, module_item: Item) -> Result<Tok
let mut context = ModuleContext {
name: module_meta.simple_name()?,
is_sub: module_meta.sub()?,
explicit_name: module_meta.optional_name().is_some(),
..Default::default()
};
let items = module_item.items_mut().ok_or_else(|| {
Expand Down Expand Up @@ -256,7 +261,7 @@ pub(crate) fn impl_pymodule(args: PyModuleArgs, module_item: Item) -> Result<Tok
let function_items = context.function_items.validate()?;
let attribute_items = context.attribute_items.validate()?;
let rust_doc = doc.filter(|doc| !doc.is_empty());
let db_doc = if rust_doc.is_some() {
let db_doc = if rust_doc.is_some() || (context.is_sub && !context.explicit_name) {
None
} else {
rustpython_doc::get(module_name)
Expand Down Expand Up @@ -643,36 +648,21 @@ struct ModuleItemArgs<'a> {
}

impl<'a> ModuleItemArgs<'a> {
fn module_name(&'a self) -> &'a str {
self.context.name.as_str()
/// Python module consulted in the doc DB. `None` for `#[pymodule(sub)]`
/// without `name`.
fn doc_module(&'a self) -> Option<&'a str> {
if self.context.is_sub && !self.context.explicit_name {
None
} else {
Some(self.context.name.as_str())
}
}
}

trait ModuleItem: ContentItem {
fn gen_module_item(&self, args: ModuleItemArgs<'_>) -> Result<()>;
}

/// Doc for `module.func` when this body is a `#[pymodule(sub)]` and the Rust
/// module name is not the Python module. Used only when exactly one module
/// owns that function name.
fn unique_submodule_func_doc(name: &str) -> Option<rustpython_doc::DocRef> {
let suffix = format!(".{name}");
let mut found = None;
for (key, doc) in rustpython_doc::DB {
let Some(module) = key.strip_suffix(&suffix) else {
continue;
};
if module.is_empty() || module.contains('.') {
continue;
}
if found.is_some() {
return None;
}
found = Some(*doc);
}
found.filter(|doc| doc.len != 0)
}

impl ModuleItem for FunctionItem {
fn gen_module_item(&self, args: ModuleItemArgs<'_>) -> Result<()> {
let func = args
Expand Down Expand Up @@ -705,20 +695,16 @@ impl ModuleItem for FunctionItem {
args.context.errors.ok_or_push(r);
}

let module = args.module_name();
let module = args.doc_module();
let rust_doc = args.attrs.doc().filter(|doc| !doc.is_empty());
let docs = py_names
.iter()
.map(|py_name| {
let db_doc = if rust_doc.is_some() {
None
} else {
rustpython_doc::get_qualified(module, py_name, None, false).or_else(|| {
if args.context.is_sub {
unique_submodule_func_doc(py_name)
} else {
None
}
module.and_then(|module| {
rustpython_doc::get_qualified(module, py_name, None, false)
})
};
let doc = crate::class_docs::item_doc_tokens(db_doc, rust_doc.clone());
Expand Down Expand Up @@ -759,10 +745,11 @@ impl ModuleItem for ClassItem {
let is_use = matches!(&args.item, syn::Item::Use(_));

let class_meta = ClassItemMeta::from_attr(ident.clone(), class_attr)?;
let module_name = args.context.name.clone();
let module_name = if let Some(class_module_name) = class_meta.module().ok().flatten() {
let explicit_module = class_meta.module().ok().flatten();
let module_name = if let Some(class_module_name) = explicit_module.clone() {
class_module_name
} else {
let module_name = args.context.name.clone();
class_attr.fill_nested_meta("module", || {
parse_quote! {module = #module_name}
})?;
Expand All @@ -773,6 +760,27 @@ impl ModuleItem for ClassItem {
} else {
class_meta.class_name()?
};
// The class's own module first, then the module it is published on.
// A `sub` module without `name` has no Python module to consult.
let mut doc_modules = Vec::new();
if let Some(module) = explicit_module.as_deref() {
doc_modules.push(module);
}
if let Some(module) = args.doc_module()
&& !doc_modules.contains(&module)
{
doc_modules.push(module);
}
let class_doc = doc_modules
.into_iter()
.find_map(|module| {
rustpython_doc::get_qualified(module, &class_name, None, true)
.filter(|doc| doc.len != 0)
})
.map(|doc| {
let doc = crate::class_docs::item_doc_tokens(Some(doc), None);
quote!(::rustpython_vm::class::assign_missing_doc(vm, &new_class, #doc);)
});
let class_new = quote_spanned!(ident.span() =>
let new_class = <#ident as ::rustpython_vm::class::PyClassImpl>::make_static_type();
// Only set __module__ string if the class doesn't already have a
Expand All @@ -787,6 +795,7 @@ impl ModuleItem for ClassItem {
new_class.set_attr(module_key, vm.new_pyobj(#module_name));
}
}
#class_doc
);
(class_name, class_new)
};
Expand Down
11 changes: 5 additions & 6 deletions crates/derive-impl/src/pystructseq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,12 +524,11 @@ pub(crate) fn impl_pystruct_sequence(
} else {
class_name.clone()
};
let db_doc = rustpython_doc::get(module_class_name.as_str()).or_else(|| {
// os re-exports the posix/nt struct sequences.
let class_name = module_class_name.strip_prefix("os.")?;
rustpython_doc::get(&format!("posix.{class_name}"))
.or_else(|| rustpython_doc::get(&format!("nt.{class_name}")))
});
let db_doc = if let Some(module) = module_name.as_deref() {
rustpython_doc::get_qualified(module, &class_name, None, false)
} else {
rustpython_doc::get(&class_name)
};
let rust_doc = struct_item.attrs.doc().filter(|doc| !doc.is_empty());
let db = if rust_doc.is_some() { None } else { db_doc };
let doc = crate::class_docs::item_doc_tokens(db, rust_doc);
Expand Down
6 changes: 4 additions & 2 deletions crates/derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,11 @@ pub fn pyexception(attr: TokenStream, item: TokenStream) -> TokenStream {
/// mod module {
/// }
/// ```
/// - `sub`: declare the module as a submodule of another module.
/// - `sub`: declare the module as a submodule merged into another module.
/// `name` is that Python module. Doc lookup uses it; without `name`, the
/// doc DB is not consulted.
/// ```rust, ignore
/// #[pymodule(sub)]
/// #[pymodule(sub, name = "my_module")]
/// mod submodule {
/// }
///
Expand Down
Loading
Loading