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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ http = "1.5.0"
libc = "0.2.189"
libfmp = { path = "crates/libfmp" }
proc-macro2 = "1.0"
pyo3 = { version = "0.29", features = ["abi3-py310", "chrono", "multiple-pymethods"] }
pyo3 = { version = "0.29", features = ["abi3-py310", "chrono", "experimental-async", "multiple-pymethods"] }
quote = "1.0"
pyo3-stub-gen = { version = "0.23", default-features = false, features = ["infer_signature"] }
reqwest = { version = "0.13.4", default-features = false }
Expand Down
50 changes: 37 additions & 13 deletions crates/fmp-py-gen/src/bin/gen_namespaces/bindings.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
//! The per-module wiring that used to be hand-written in `lib.rs`,
//! `client.rs`, and `facade.rs`: one binding per `fmp._native.<path>`
//! submodule, unioning the model inventory with the registry tree, rendered
//! as `registration.rs`, `namespaces/mod.rs`, `client_namespaces.rs`, and
//! `facade_domains.rs`.
//! as `registration.rs`, `namespaces/mod.rs`, `client_namespaces.rs` (the
//! getters of both `FmpClient` and `AsyncFmpClient`), and `facade_domains.rs`.

use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;

use crate::GENERATED_HEADER;
use crate::models::ModelModules;
use crate::plan::Node;
use crate::plan::{Node, async_struct_name_for};

/// One `fmp._native.<path>` submodule and everything registered into it.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
Expand All @@ -18,7 +18,8 @@ pub(crate) struct Binding {
pub(crate) path: Vec<String>,
/// The generated model structs the module holds, sorted.
pub(crate) models: Vec<String>,
/// The namespace struct name when the registry defines this path.
/// The sync namespace struct name when the registry defines this path;
/// its `Async` twin is registered alongside it.
pub(crate) namespace: Option<String>,
/// The last path segment of every direct child, sorted.
pub(crate) children: BTreeSet<String>,
Expand Down Expand Up @@ -104,10 +105,15 @@ pub(crate) fn render_registration(bindings: &BTreeMap<Vec<String>, Binding>) ->
);
}
if let Some(namespace) = &binding.namespace {
let module = binding.path.join("::");
let _ = writeln!(
out,
" module.add_class::<crate::namespaces::{}::{namespace}>()?;",
binding.path.join("::")
" module.add_class::<crate::namespaces::{module}::{namespace}>()?;"
);
let _ = writeln!(
out,
" module.add_class::<crate::namespaces::{module}::{}>()?;",
async_struct_name_for(&binding.path)
);
}
for child in &binding.children {
Expand Down Expand Up @@ -137,37 +143,55 @@ pub(crate) fn render_namespaces_mod(tree: &BTreeMap<Vec<String>, Node>) -> Strin
out
}

/// Renders `client_namespaces.rs`: the `FmpClient` getter for every domain.
/// Renders `client_namespaces.rs`: the getter for every domain on
/// `FmpClient`, then the same getters on `AsyncFmpClient` returning the
/// `Async` twins.
pub(crate) fn render_client_namespaces(tree: &BTreeMap<Vec<String>, Node>) -> String {
let domains: Vec<&Node> = tree.values().filter(|node| node.path.len() == 1).collect();
let mut out = String::new();
out.push_str(GENERATED_HEADER);
out.push_str("\n\nuse pyo3::prelude::*;\nuse pyo3_stub_gen::derive::gen_stub_pymethods;\n\n");
out.push_str("use crate::client::FmpClient;\n");
out.push_str("use crate::client::{AsyncFmpClient, FmpClient};\n");
for node in &domains {
let _ = writeln!(
out,
"use crate::namespaces::{}::{};",
"use crate::namespaces::{}::{{{}, {}}};",
node.dotted(),
async_struct_name_for(&node.path),
node.struct_name()
);
}
out.push_str("\n#[gen_stub_pymethods]\n#[pymethods]\nimpl FmpClient {\n");
render_getters(&mut out, "FmpClient", &domains, |node| node.struct_name());
render_getters(&mut out, "AsyncFmpClient", &domains, |node| {
async_struct_name_for(&node.path)
});
out
}

fn render_getters(
out: &mut String,
client: &str,
domains: &[&Node],
struct_name: impl Fn(&Node) -> String,
) {
let _ = writeln!(
out,
"\n#[gen_stub_pymethods]\n#[pymethods]\nimpl {client} {{"
);
let mut first = true;
for node in &domains {
for node in domains {
if !first {
out.push('\n');
}
first = false;
let domain = node.dotted();
let struct_name = node.struct_name();
let struct_name = struct_name(node);
let _ = writeln!(
out,
" /// The `{domain}` endpoints, reached as `client.{domain}`.\n #[getter]\n fn {domain}(&self) -> {struct_name} {{\n {struct_name}::new(self.builder.clone())\n }}"
);
}
out.push_str("}\n");
out
}

/// Renders `facade_domains.rs`: a wildcard `reexport_module_members!` from
Expand Down
77 changes: 54 additions & 23 deletions crates/fmp-py-gen/src/bin/gen_namespaces/emit.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
//! Rendering one namespace node into Rust source.
//!
//! Each node renders two classes in one file: the sync `<Path>Namespace` for
//! `FmpClient` and its `Async<Path>Namespace` twin for `AsyncFmpClient`.
//! Every method follows the same shape:
//! a free `<name>_query` function converts the Python arguments and builds
//! the `libfmp` query (the part an async twin will share), and the
//! `#[pymethods]` body only detaches from the interpreter, runs the call on
//! the shared runtime, and maps rows into the generated models (or a
//! the `libfmp` query (shared by both twins), and the `#[pymethods]` body
//! only hands the call to the shared runtime (detached `block_on`, or an
//! awaited `spawn`) and maps rows into the generated models (or a
//! `BinaryResponse` into the hand-written `BinaryPayload`).

use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;

use crate::GENERATED_HEADER;
use crate::method::{render_method, render_query_fn};
use crate::plan::{Node, struct_name_for};
use crate::method::{render_async_method, render_method, render_query_fn};
use crate::plan::{Node, async_struct_name_for, struct_name_for};

/// Where the types of one verified entry live, as segments under
/// `libfmp::endpoints`: the query type's module and each nested builder's
Expand Down Expand Up @@ -117,42 +119,80 @@ pub(crate) fn render(node: &Node, query_modules: &QueryModules) -> Result<Render
if methods.is_empty() {
out.push_str("use crate::runtime::ClientHandle;\n");
} else {
out.push_str("use crate::runtime::{ClientHandle, block_on};\n");
out.push_str("use crate::runtime::{ClientHandle, block_on, spawn};\n");
}
if !node.children.is_empty() {
out.push('\n');
for child in &node.children {
let mut child_path = node.path.clone();
child_path.push(child.clone());
let _ = writeln!(out, "use self::{child}::{};", struct_name_for(&child_path));
let _ = writeln!(
out,
"use self::{child}::{{{}, {}}};",
async_struct_name_for(&child_path),
struct_name_for(&child_path)
);
}
}

render_class(&mut out, node, Flavor::Sync);
render_class(&mut out, node, Flavor::Async);

for endpoint in methods {
if endpoint.query_type.is_some() {
out.push('\n');
render_query_fn(&mut out, endpoint, &struct_name);
}
}

Ok(Rendered { source: out })
}

/// Which of a node's two classes [`render_class`] emits.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Flavor {
Sync,
Async,
}

/// Renders one namespace class: the struct, its constructor, and the
/// `#[pymethods]` block with every endpoint wrapper plus one getter per child
/// returning the child's class of the same flavor.
fn render_class(out: &mut String, node: &Node, flavor: Flavor) {
let dotted = node.dotted();
let (struct_name, client): (fn(&[String]) -> String, &str) = match flavor {
Flavor::Sync => (struct_name_for, "client"),
Flavor::Async => (async_struct_name_for, "async client"),
};
let name = struct_name(&node.path);
let _ = write!(
out,
"\n/// {} endpoints for a single client, exposed as `client.{dotted}`.\n",
"\n/// {} endpoints for a single {client}, exposed as `client.{dotted}`.\n",
humanize(&node.path)
);
out.push_str("#[gen_stub_pyclass]\n");
let _ = writeln!(out, "#[pyclass(module = \"fmp._native.{dotted}\", frozen)]");
let _ = writeln!(
out,
"pub(crate) struct {struct_name} {{\n builder: Arc<ClientHandle>,\n}}\n"
"pub(crate) struct {name} {{\n builder: Arc<ClientHandle>,\n}}\n"
);
let _ = writeln!(
out,
"impl {struct_name} {{\n pub(crate) fn new(builder: Arc<ClientHandle>) -> Self {{\n Self {{ builder }}\n }}\n}}\n"
"impl {name} {{\n pub(crate) fn new(builder: Arc<ClientHandle>) -> Self {{\n Self {{ builder }}\n }}\n}}\n"
);

out.push_str("#[gen_stub_pymethods]\n#[pymethods]\n");
let _ = writeln!(out, "impl {struct_name} {{");
let _ = writeln!(out, "impl {name} {{");
let mut first = true;
for endpoint in methods {
for endpoint in &node.endpoints {
if !first {
out.push('\n');
}
first = false;
render_method(&mut out, endpoint);
match flavor {
Flavor::Sync => render_method(out, endpoint),
Flavor::Async => render_async_method(out, endpoint),
}
}
for child in &node.children {
if !first {
Expand All @@ -161,7 +201,7 @@ pub(crate) fn render(node: &Node, query_modules: &QueryModules) -> Result<Render
first = false;
let mut child_path = node.path.clone();
child_path.push(child.clone());
let child_struct = struct_name_for(&child_path);
let child_struct = struct_name(&child_path);
let _ = writeln!(
out,
" /// The `{dotted}.{child}` endpoints, reached as `client.{dotted}.{child}`."
Expand All @@ -173,15 +213,6 @@ pub(crate) fn render(node: &Node, query_modules: &QueryModules) -> Result<Render
);
}
out.push_str("}\n");

for endpoint in methods {
if endpoint.query_type.is_some() {
out.push('\n');
render_query_fn(&mut out, endpoint, &struct_name);
}
}

Ok(Rendered { source: out })
}

/// `use <prefix>::<module>::{A, B};` with the braces dropped for one name.
Expand Down
8 changes: 5 additions & 3 deletions crates/fmp-py-gen/src/bin/gen_namespaces/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
//!
//! Reads the endpoint registry under `crates/fmp-py-gen/registry`, validates
//! it against the real `libfmp` signatures (refusing to emit on any error),
//! and writes one frozen `pyclass` namespace per registry path under
//! and writes one frozen `pyclass` namespace per registry path, with its
//! `Async` twin in the same file, under
//! `crates/fmp-py/src/namespaces/`: flat domains as `<domain>.rs`, nested
//! ones as `<domain>/mod.rs` plus `<domain>/<sub>.rs`, each formatted with
//! `rustfmt`. Run it with `cargo run -p fmp-py-gen --bin gen_namespaces`.
Expand All @@ -11,8 +12,9 @@
//! extension, so adding a domain never edits `lib.rs` or `client.rs`:
//! `namespaces/mod.rs`, `registration.rs` (every `fmp._native.<path>`
//! submodule with its models and namespace classes, read back from the
//! generated model files), `client_namespaces.rs` (one `FmpClient` getter
//! per domain), and `facade_domains.rs` (the public `fmp.<path>` re-exports).
//! generated model files), `client_namespaces.rs` (one getter per domain on
//! both `FmpClient` and `AsyncFmpClient`), and `facade_domains.rs` (the
//! public `fmp.<path>` re-exports).

mod bindings;
mod emit;
Expand Down
Loading
Loading