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
1 change: 1 addition & 0 deletions newsfragments/5150.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add support for module associated consts introspection.
23 changes: 21 additions & 2 deletions pyo3-introspection/src/introspection.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::model::{Argument, Arguments, Class, Function, Module, VariableLengthArgument};
use crate::model::{Argument, Arguments, Class, Const, Function, Module, VariableLengthArgument};
use anyhow::{bail, ensure, Context, Result};
use goblin::elf::Elf;
use goblin::mach::load_command::CommandVariant;
Expand Down Expand Up @@ -44,11 +44,12 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result<Module> {
if let Chunk::Module {
name,
members,
consts,
id: _,
} = chunk
{
if name == main_module_name {
return convert_module(name, members, &chunks_by_id, &chunks_by_parent);
return convert_module(name, members, consts, &chunks_by_id, &chunks_by_parent);
}
}
}
Expand All @@ -58,6 +59,7 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result<Module> {
fn convert_module(
name: &str,
members: &[String],
consts: &[ConstChunk],
chunks_by_id: &HashMap<&str, &Chunk>,
chunks_by_parent: &HashMap<&str, Vec<&Chunk>>,
) -> Result<Module> {
Expand All @@ -69,11 +71,19 @@ fn convert_module(
chunks_by_id,
chunks_by_parent,
)?;

Ok(Module {
name: name.into(),
modules,
classes,
functions,
consts: consts
.iter()
.map(|c| Const {
name: c.name.clone(),
value: c.value.clone(),
})
.collect(),
})
}

Expand All @@ -91,11 +101,13 @@ fn convert_members(
Chunk::Module {
name,
members,
consts,
id: _,
} => {
modules.push(convert_module(
name,
members,
consts,
chunks_by_id,
chunks_by_parent,
)?);
Expand Down Expand Up @@ -354,6 +366,7 @@ enum Chunk {
id: String,
name: String,
members: Vec<String>,
consts: Vec<ConstChunk>,
},
Class {
id: String,
Expand All @@ -371,6 +384,12 @@ enum Chunk {
},
}

#[derive(Deserialize)]
struct ConstChunk {
name: String,
value: String,
}

#[derive(Deserialize)]
struct ChunkArguments {
#[serde(default)]
Expand Down
7 changes: 7 additions & 0 deletions pyo3-introspection/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub struct Module {
pub modules: Vec<Module>,
pub classes: Vec<Class>,
pub functions: Vec<Function>,
pub consts: Vec<Const>,
}

#[derive(Debug, Eq, PartialEq, Clone, Hash)]
Expand All @@ -20,6 +21,12 @@ pub struct Function {
pub arguments: Arguments,
}

#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub struct Const {
pub name: String,
pub value: String,
}

#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub struct Arguments {
/// Arguments before /
Expand Down
26 changes: 23 additions & 3 deletions pyo3-introspection/src/stubs.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::model::{Argument, Class, Function, Module, VariableLengthArgument};
use std::collections::HashMap;
use crate::model::{Argument, Class, Const, Function, Module, VariableLengthArgument};
use std::collections::{BTreeSet, HashMap};
use std::path::{Path, PathBuf};

/// Generates the [type stubs](https://typing.readthedocs.io/en/latest/source/stubs.html) of a given module.
Expand Down Expand Up @@ -32,16 +32,29 @@ fn add_module_stub_files(

/// Generates the module stubs to a String, not including submodules
fn module_stubs(module: &Module) -> String {
let mut modules_to_import = BTreeSet::new();
let mut elements = Vec::new();
for class in &module.classes {
elements.push(class_stubs(class));
}
for function in &module.functions {
elements.push(function_stubs(function));
}
for konst in &module.consts {
elements.push(const_stubs(konst, &mut modules_to_import));
}

// We insert two line jumps (i.e. empty strings) only above and below multiple line elements (classes with methods, functions with decorators)
let mut output = String::new();

for module_to_import in &modules_to_import {
output.push_str(&format!("import {module_to_import}\n"));
}

if !modules_to_import.is_empty() {
output.push('\n')
}

// We insert two line jumps (i.e. empty strings) only above and below multiple line elements (classes with methods, functions with decorators)
for element in elements {
let is_multiline = element.contains('\n');
if is_multiline && !output.is_empty() && !output.ends_with("\n\n") {
Expand All @@ -53,6 +66,7 @@ fn module_stubs(module: &Module) -> String {
output.push('\n');
}
}

// We remove a line jump at the end if they are two
if output.ends_with("\n\n") {
output.pop();
Expand Down Expand Up @@ -111,6 +125,12 @@ fn function_stubs(function: &Function) -> String {
buffer
}

fn const_stubs(konst: &Const, modules_to_import: &mut BTreeSet<String>) -> String {
modules_to_import.insert("typing".to_string());
let Const { name, value } = konst;
format!("{name}: typing.Final = {value}")
}

fn argument_stub(argument: &Argument) -> String {
let mut output = argument.name.clone();
if let Some(default_value) = &argument.default_value {
Expand Down
35 changes: 35 additions & 0 deletions pyo3-macros-backend/src/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::mem::take;
use std::sync::atomic::{AtomicUsize, Ordering};
use syn::ext::IdentExt;
use syn::{Attribute, Ident, Type, TypePath};

static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0);
Expand All @@ -28,6 +29,9 @@ pub fn module_introspection_code<'a>(
name: &str,
members: impl IntoIterator<Item = &'a Ident>,
members_cfg_attrs: impl IntoIterator<Item = &'a Vec<Attribute>>,
consts: impl IntoIterator<Item = &'a Ident>,
consts_values: impl IntoIterator<Item = &'a String>,
consts_cfg_attrs: impl IntoIterator<Item = &'a Vec<Attribute>>,
) -> TokenStream {
IntrospectionNode::Map(
[
Expand All @@ -52,6 +56,23 @@ pub fn module_introspection_code<'a>(
.collect(),
),
),
(
"consts",
IntrospectionNode::List(
consts
.into_iter()
.zip(consts_values)
.zip(consts_cfg_attrs)
.filter_map(|((ident, value), attributes)| {
if attributes.is_empty() {
Some(const_introspection_code(ident, value))
} else {
None // TODO: properly interpret cfg attributes
}
})
.collect(),
),
),
]
.into(),
)
Expand Down Expand Up @@ -116,6 +137,20 @@ pub fn function_introspection_code(
IntrospectionNode::Map(desc).emit(pyo3_crate_path)
}

fn const_introspection_code<'a>(ident: &'a Ident, value: &'a String) -> IntrospectionNode<'a> {
IntrospectionNode::Map(
[
("type", IntrospectionNode::String("const".into())),
(
"name",
IntrospectionNode::String(ident.unraw().to_string().into()),
),
("value", IntrospectionNode::String(value.into())),
]
.into(),
)
}

fn arguments_introspection_data<'a>(
signature: &'a FunctionSignature<'a>,
first_argument: Option<&'a str>,
Expand Down
28 changes: 2 additions & 26 deletions pyo3-macros-backend/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::{ext::IdentExt, spanned::Spanned, Ident, Result};

use crate::pyversions::is_abi3_before;
use crate::utils::{Ctx, LitCStr};
use crate::utils::{expr_to_python, Ctx, LitCStr};
use crate::{
attributes::{FromPyWithAttribute, TextSignatureAttribute, TextSignatureAttributeValue},
params::{impl_arg_params, Holders},
Expand All @@ -34,31 +34,7 @@ impl RegularArg<'_> {
..
} = self
{
match arg_default {
// literal values
syn::Expr::Lit(syn::ExprLit { lit, .. }) => match lit {
syn::Lit::Str(s) => s.token().to_string(),
syn::Lit::Char(c) => c.token().to_string(),
syn::Lit::Int(i) => i.base10_digits().to_string(),
syn::Lit::Float(f) => f.base10_digits().to_string(),
syn::Lit::Bool(b) => {
if b.value() {
"True".to_string()
} else {
"False".to_string()
}
}
_ => "...".to_string(),
},
// None
syn::Expr::Path(syn::ExprPath { qself, path, .. })
if qself.is_none() && path.is_ident("None") =>
{
"None".to_string()
}
// others, unsupported yet so defaults to `...`
_ => "...".to_string(),
}
expr_to_python(arg_default)
} else if let RegularArg {
option_wrapped_type: Some(..),
..
Expand Down
10 changes: 8 additions & 2 deletions pyo3-macros-backend/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#[cfg(feature = "experimental-inspect")]
use crate::introspection::{introspection_id_const, module_introspection_code};
use crate::utils::expr_to_python;
use crate::{
attributes::{
self, kw, take_attributes, take_pyo3_options, CrateAttribute, GILUsedAttribute,
Expand Down Expand Up @@ -150,6 +151,7 @@ pub fn pymodule_module_impl(

let mut pymodule_init = None;
let mut module_consts = Vec::new();
let mut module_consts_values = Vec::new();
let mut module_consts_cfg_attrs = Vec::new();

for item in &mut *items {
Expand Down Expand Up @@ -293,8 +295,8 @@ pub fn pymodule_module_impl(
if !find_and_remove_attribute(&mut item.attrs, "pymodule_export") {
continue;
}

module_consts.push(item.ident.clone());
module_consts_values.push(expr_to_python(&item.expr));
module_consts_cfg_attrs.push(get_cfg_attributes(&item.attrs));
}
Item::Static(item) => {
Expand Down Expand Up @@ -349,6 +351,9 @@ pub fn pymodule_module_impl(
&name.to_string(),
&module_items,
&module_items_cfg_attrs,
&module_consts,
&module_consts_values,
&module_consts_cfg_attrs,
);
#[cfg(not(feature = "experimental-inspect"))]
let introspection = quote! {};
Expand Down Expand Up @@ -432,7 +437,8 @@ pub fn pymodule_function_impl(
);

#[cfg(feature = "experimental-inspect")]
let introspection = module_introspection_code(pyo3_path, &name.to_string(), &[], &[]);
let introspection =
module_introspection_code(pyo3_path, &name.to_string(), &[], &[], &[], &[], &[]);
#[cfg(not(feature = "experimental-inspect"))]
let introspection = quote! {};
#[cfg(feature = "experimental-inspect")]
Expand Down
28 changes: 28 additions & 0 deletions pyo3-macros-backend/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,3 +393,31 @@ impl TypeExt for syn::Type {
self
}
}

pub fn expr_to_python(expr: &syn::Expr) -> String {
match expr {
// literal values
syn::Expr::Lit(syn::ExprLit { lit, .. }) => match lit {
syn::Lit::Str(s) => s.token().to_string(),
syn::Lit::Char(c) => c.token().to_string(),
syn::Lit::Int(i) => i.base10_digits().to_string(),
syn::Lit::Float(f) => f.base10_digits().to_string(),
syn::Lit::Bool(b) => {
if b.value() {
"True".to_string()
} else {
"False".to_string()
}
}
_ => "...".to_string(),
},
// None
syn::Expr::Path(syn::ExprPath { qself, path, .. })
if qself.is_none() && path.is_ident("None") =>
{
"None".to_string()
}
// others, unsupported yet so defaults to `...`
_ => "...".to_string(),
}
}
10 changes: 10 additions & 0 deletions pytests/src/consts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use pyo3::pymodule;

#[pymodule]
pub mod consts {
#[pymodule_export]
pub const PI: f64 = std::f64::consts::PI; // Exports PI constant as part of the module

#[pymodule_export]
pub const SIMPLE: &str = "SIMPLE";
}
3 changes: 2 additions & 1 deletion pytests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use pyo3::wrap_pymodule;
pub mod awaitable;
pub mod buf_and_str;
pub mod comparisons;
mod consts;
pub mod datetime;
pub mod dict_iter;
pub mod enums;
Expand All @@ -22,7 +23,7 @@ mod pyo3_pytests {
use super::*;

#[pymodule_export]
use {pyclasses::pyclasses, pyfunctions::pyfunctions};
use {consts::consts, pyclasses::pyclasses, pyfunctions::pyfunctions};

// Inserting to sys.modules allows importing submodules nicely from Python
// e.g. import pyo3_pytests.buf_and_str as bas
Expand Down
4 changes: 4 additions & 0 deletions pytests/stubs/consts.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import typing

PI: typing.Final = ...
SIMPLE: typing.Final = "SIMPLE"
Loading