Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 1a95786

Browse filesBrowse files
authored
Generate python stub files in proc macros (#730)
1 parent 61603e6 commit 1a95786
Copy full SHA for 1a95786

File tree

9 files changed

+333
-26
lines changed
Filter options

9 files changed

+333
-26
lines changed

‎pgml-sdks/rust/pgml-macros/src/lib.rs

Copy file name to clipboardExpand all lines: pgml-sdks/rust/pgml-macros/src/lib.rs
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use syn::{parse_macro_input, DeriveInput, ItemImpl};
22

33
mod common;
4-
mod types;
54
mod python;
5+
mod types;
66

77
#[proc_macro_derive(custom_derive)]
88
pub fn custom_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {

‎pgml-sdks/rust/pgml-macros/src/python.rs

Copy file name to clipboardExpand all lines: pgml-sdks/rust/pgml-macros/src/python.rs
+147-14Lines changed: 147 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
use quote::{format_ident, quote, ToTokens};
2+
use std::fs::OpenOptions;
3+
use std::io::{Read, Write};
24
use syn::{visit::Visit, DeriveInput, ItemImpl, Type};
35

46
use crate::common::{AttributeArgs, GetImplMethod};
57
use crate::types::{GetSupportedType, OutputType, SupportedType};
68

9+
const STUB_TOP: &str = r#"
10+
# Top of file key: A12BECOD!
11+
from typing import List, Dict, Optional, Self, Any
12+
13+
"#;
14+
715
pub fn generate_into_py(parsed: DeriveInput) -> proc_macro::TokenStream {
816
let name = parsed.ident;
917
let fields_named = match parsed.data {
@@ -29,6 +37,23 @@ pub fn generate_into_py(parsed: DeriveInput) -> proc_macro::TokenStream {
2937
}
3038
}).collect();
3139

40+
let stub = format!("\n{} = dict[str, Any]\n", name);
41+
42+
let mut file = OpenOptions::new()
43+
.create(true)
44+
.write(true)
45+
.append(true)
46+
.read(true)
47+
.open("python/pgml/pgml.pyi")
48+
.unwrap();
49+
let mut contents = String::new();
50+
file.read_to_string(&mut contents)
51+
.expect("Unable to read stubs file for python");
52+
if !contents.contains(&stub) {
53+
file.write_all(stub.as_bytes())
54+
.expect("Unable to write stubs file for python");
55+
}
56+
3257
let expanded = quote! {
3358
impl IntoPy<PyObject> for #name {
3459
fn into_py(self, py: Python<'_>) -> PyObject {
@@ -76,6 +101,9 @@ pub fn generate_python_methods(
76101
};
77102
let name_ident = format_ident!("{}Python", wrapped_type_ident);
78103

104+
let python_class_name = wrapped_type_ident.to_string();
105+
let mut stubs = format!("\nclass {}:\n", python_class_name);
106+
79107
// Iterate over the items - see: https://docs.rs/syn/latest/syn/enum.ImplItem.html
80108
for item in parsed.items {
81109
// We only create methods for functions listed in the attribute args
@@ -105,26 +133,61 @@ pub fn generate_python_methods(
105133
OutputType::Default => (None, None),
106134
};
107135

136+
let signature = quote! {
137+
pub fn #method_ident<'a>(#(#method_arguments),*) -> #output_type
138+
};
139+
140+
let p1 = if method.is_async { "async def" } else { "def" };
141+
let p2 = match method_ident.to_string().as_str() {
142+
"new" => "__init__".to_string(),
143+
_ => method_ident.to_string(),
144+
};
145+
let p3 = method
146+
.method_arguments
147+
.iter()
148+
.map(|a| format!("{}: {}", a.0, get_python_type(&a.1)))
149+
.collect::<Vec<String>>()
150+
.join(", ");
151+
let p4 = match &method.output_type {
152+
OutputType::Result(v) | OutputType::Other(v) => get_python_type(v),
153+
OutputType::Default => "None".to_string(),
154+
};
155+
stubs.push_str(&format!("\t{} {}(self, {}) -> {}", p1, p2, p3, p4));
156+
stubs.push_str("\n\t\t...\n");
157+
108158
// The new function for pyO3 requires some unique syntax
109159
let (signature, middle) = if method_ident == "new" {
110160
let signature = quote! {
111161
#[new]
112-
pub fn new<'a>(#(#method_arguments),*) -> #output_type
162+
#signature
113163
};
114-
let middle = quote! {
115-
let runtime = get_or_set_runtime();
116-
let x = match runtime.block_on(#wrapped_type_ident::new(#(#wrapper_arguments),*)) {
164+
let middle = if method.is_async {
165+
quote! {
166+
get_or_set_runtime().block_on(#wrapped_type_ident::new(#(#wrapper_arguments),*))
167+
}
168+
} else {
169+
quote! {
170+
#wrapped_type_ident::new(#(#wrapper_arguments),*)
171+
}
172+
};
173+
let middle = if let OutputType::Result(_r) = method.output_type {
174+
quote! {
175+
let x = match #middle {
117176
Ok(m) => m,
118177
Err(e) => return Err(PyErr::new::<pyo3::exceptions::PyException, _>(e.to_string()))
119-
120-
};
121-
Ok(#name_ident::from(x))
122-
};
178+
};
179+
}
180+
} else {
181+
quote! {
182+
let x = #middle;
183+
}
184+
};
185+
let middle = quote! {
186+
#middle
187+
Ok(#name_ident::from(x))
188+
};
123189
(signature, middle)
124190
} else {
125-
let signature = quote! {
126-
pub fn #method_ident<'a>(#(#method_arguments),*) -> #output_type
127-
};
128191
let middle = quote! {
129192
#method_ident(#(#wrapper_arguments),*)
130193
};
@@ -146,7 +209,7 @@ pub fn generate_python_methods(
146209
}
147210
} else {
148211
quote! {
149-
let x = middle;
212+
let x = #middle;
150213
}
151214
};
152215
let middle = if let Some(convert) = convert_from {
@@ -181,6 +244,25 @@ pub fn generate_python_methods(
181244
});
182245
}
183246

247+
let mut file = OpenOptions::new()
248+
.create(true)
249+
.write(true)
250+
.append(true)
251+
.read(true)
252+
.open("python/pgml/pgml.pyi")
253+
.unwrap();
254+
let mut contents = String::new();
255+
file.read_to_string(&mut contents)
256+
.expect("Unable to read stubs file for python");
257+
if !contents.contains("A12BECOD") {
258+
file.write_all(STUB_TOP.as_bytes())
259+
.expect("Unable to write stubs file for python");
260+
}
261+
if !contents.contains(&format!("class {}:", python_class_name)) {
262+
file.write_all(stubs.as_bytes())
263+
.expect("Unable to write stubs file for python");
264+
}
265+
184266
proc_macro::TokenStream::from(quote! {
185267
#[pymethods]
186268
impl #name_ident {
@@ -241,7 +323,7 @@ fn convert_method_wrapper_arguments(
241323
}
242324
}
243325

244-
pub fn convert_output_type_convert_from_python(
326+
fn convert_output_type_convert_from_python(
245327
ty: &SupportedType,
246328
method: &GetImplMethod,
247329
) -> (
@@ -251,7 +333,7 @@ pub fn convert_output_type_convert_from_python(
251333
let (output_type, convert_from) = match ty {
252334
SupportedType::S => (
253335
Some(quote! {PyResult<Self>}),
254-
Some(format_ident!("{}", method.method_ident).into_token_stream()),
336+
Some(format_ident!("Self").into_token_stream()),
255337
),
256338
t @ SupportedType::Database | t @ SupportedType::Collection => (
257339
Some(quote! {PyResult<&'a PyAny>}),
@@ -271,3 +353,54 @@ pub fn convert_output_type_convert_from_python(
271353
(output_type, convert_from)
272354
}
273355
}
356+
357+
fn get_python_type(ty: &SupportedType) -> String {
358+
match ty {
359+
SupportedType::Reference(r) => get_python_type(r),
360+
SupportedType::S => "Self".to_string(),
361+
SupportedType::str | SupportedType::String => "str".to_string(),
362+
SupportedType::Option(o) => format!(
363+
"Optional[{}] = {}",
364+
get_python_type(o),
365+
get_type_for_optional(o)
366+
),
367+
SupportedType::Vec(v) => format!("List[{}]", get_python_type(v)),
368+
SupportedType::HashMap((k, v)) => {
369+
format!("Dict[{}, {}]", get_python_type(k), get_python_type(v))
370+
}
371+
SupportedType::Tuple(t) => {
372+
let mut types = Vec::new();
373+
for ty in t {
374+
types.push(get_python_type(ty));
375+
}
376+
// Rust's unit type is represented as an empty tuple
377+
if types.is_empty() {
378+
"None".to_string()
379+
} else {
380+
format!("tuple[{}]", types.join(", "))
381+
}
382+
}
383+
SupportedType::i64 => "int".to_string(),
384+
SupportedType::f64 => "float".to_string(),
385+
// Our own types
386+
t @ SupportedType::Database
387+
| t @ SupportedType::Collection
388+
| t @ SupportedType::Splitter => t.to_string(),
389+
// Add more types as required
390+
_ => "Any".to_string(),
391+
}
392+
}
393+
394+
fn get_type_for_optional(ty: &SupportedType) -> String {
395+
match ty {
396+
SupportedType::Reference(r) => get_type_for_optional(r),
397+
SupportedType::str | SupportedType::String => {
398+
"\"Default set in Rust. Please check the documentation.\"".to_string()
399+
}
400+
SupportedType::HashMap(_) => "{}".to_string(),
401+
SupportedType::Vec(_) => "[]".to_string(),
402+
SupportedType::i64 => 1.to_string(),
403+
SupportedType::f64 => 1.0.to_string(),
404+
_ => panic!("Type not yet supported for optional python stub: {:?}", ty),
405+
}
406+
}

‎pgml-sdks/rust/pgml-macros/src/types.rs

Copy file name to clipboardExpand all lines: pgml-sdks/rust/pgml-macros/src/types.rs
+6-6Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ impl ToString for SupportedType {
3636
SupportedType::HashMap((k, v)) => {
3737
format!("HashMap<{},{}>", k.to_string(), v.to_string())
3838
}
39-
SupportedType::Tuple(v) => {
40-
let mut output = String::new();
41-
v.iter().for_each(|ty| {
42-
output.push_str(&format!("{},", ty.to_string()));
43-
});
44-
format!("({})", output)
39+
SupportedType::Tuple(t) => {
40+
let mut types = Vec::new();
41+
for ty in t {
42+
types.push(ty.to_string());
43+
}
44+
format!("({})", types.join(","))
4545
}
4646
SupportedType::S => "Self".to_string(),
4747
SupportedType::Option(v) => format!("Option<{}>", v.to_string()),

‎pgml-sdks/rust/pgml/README.md

Copy file name to clipboard
+135Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,136 @@
11
# Open Source Alternative for Building End-to-End Vector Search Applications without OpenAI & Pinecone
2+
# How to use this crate
3+
4+
Here is a brief outline of how to use this crate and specifically add new Python classes.
5+
6+
There are three main macros to know about:
7+
- `custom_derive`
8+
- `custom_methods`
9+
- `custom_into_py`
10+
11+
## custom_derive
12+
`custom_derive` is used when defining a new struct that you want to be available as a Python class. This macro automatically creates a wrapper for the struct postfixing the name with `Python`. For example, the following code:
13+
```
14+
#[derive(custom_derive, Debug, Clone)]
15+
pub struct TestStruct {
16+
pub name: String
17+
}
18+
```
19+
20+
Creates another struct:
21+
22+
```
23+
pub struct TestStructPython {
24+
pub wrapped: TestStruct
25+
}
26+
```
27+
28+
You must currently implement `Debug` and `Clone` on the structs you use `custom_derive` on.
29+
30+
## custom_methods
31+
`custom_methods` is used on the impl block for a struct you want to be available as a Python class. This macro automatically creates methods that work seamlessly with pyO3. For example, the following code:
32+
```
33+
#[custom_methods(new, get_name)]
34+
impl TestStruct {
35+
pub fn new(name: String) -> Self {
36+
Self { name }
37+
}
38+
pub fn get_name(&self) -> String {
39+
self.name.clone()
40+
}
41+
}
42+
```
43+
44+
Produces similar code to the following:
45+
```
46+
impl TestStruct {
47+
pub fn new(name: String) -> Self {
48+
Self { name }
49+
}
50+
pub fn get_name(&self) -> String {
51+
self.name.clone()
52+
}
53+
}
54+
55+
impl TestStructPython {
56+
pub fn new<'a>(name: String, py: Python<'a>) -> PyResult<Self> {
57+
let x = TestStruct::new(name);
58+
Ok(TestStructPython::from(x))
59+
}
60+
pub fn get_name<'a>(&self, py: Python<'a>) -> PyResult<String> {
61+
let x = self.wrapped.get_name();
62+
Ok(x)
63+
}
64+
}
65+
```
66+
67+
Note that the macro only works on methods marked with `pub`;
68+
69+
## custom_into_py
70+
`custom_into_py` is used when we want to seamlessly return Rust structs as Python dictionaries. For example, let's say we have the following code:
71+
```
72+
#[derive(custom_into_py, FromRow, Debug, Clone)]
73+
pub struct Splitter {
74+
pub id: i64,
75+
pub created_at: DateTime<Utc>,
76+
pub name: String,
77+
pub parameters: Json<HashMap<String, String>>,
78+
}
79+
80+
pub async fn get_text_splitters(&self) -> anyhow::Result<Vec<Splitter>> {
81+
Ok(sqlx::query_as(&query_builder!(
82+
"SELECT * from %s",
83+
self.splitters_table_name
84+
))
85+
.fetch_all(self.pool.borrow())
86+
.await?)
87+
}
88+
89+
```
90+
91+
The `custom_into_py` macro automatically generates the following code for us:
92+
```
93+
impl IntoPy<PyObject> for Splitter {
94+
fn into_py(self, py: Python<'_>) -> PyObject {
95+
let dict = PyDict::new(py);
96+
dict.set_item("id", self.id)
97+
.expect("Error setting python value in custom_into_py proc_macro");
98+
dict.set_item("created_at", self.created_at.timestamp())
99+
.expect("Error setting python value in custom_into_py proc_macro");
100+
dict.set_item("name", self.name)
101+
.expect("Error setting python value in custom_into_py proc_macro");
102+
dict.set_item("parameters", self.parameters.0)
103+
.expect("Error setting python value in custom_into_py proc_macro");
104+
dict.into()
105+
}
106+
}
107+
```
108+
109+
Implementing `IntoPy` allows pyo3 to seamlessly convert between Rust and python. Note that Python users calling `get_text_splitters` will receive a list of dictionaries.
110+
111+
## Other Noteworthy Things
112+
113+
Be aware that the only pyo3 specific code in this crate is the `pymodule` invocation in `lib.rs`. Everything else is handled by `pgml-macros`. If you want to expose a Python Class directly on the Python module you have to add it in the `pymodule` invocation. For example, if you wanted to expose `TestStruct` so Python module users could access it directly on `pgml`, you could do the following:
114+
```
115+
#[pymodule]
116+
fn pgml(_py: Python, m: &PyModule) -> PyResult<()> {
117+
m.add_class::<TestStructPython>()?;
118+
Ok(())
119+
}
120+
```
121+
122+
Now Python users can access it like so:
123+
```
124+
import pgml
125+
126+
t = pgml.TestStruct("test")
127+
print(t.get_name())
128+
129+
```
130+
131+
For local development, install [maturin](https://github.com/PyO3/maturin) and run:
132+
```
133+
maturin develop
134+
```
135+
136+
You can now run the tests in `python/test.py`.

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.