Skip to content

Commit dd2200e

Browse files
authored
Python: improve default values and use dataclasses to keep mypy happy. (#2552)
Improved how default values are handled in function signatures etc, and more canonical use of Python dataclasses for records and enums, all towards making mypy happier
1 parent d15c779 commit dd2200e

File tree

13 files changed

+111
-95
lines changed

13 files changed

+111
-95
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
* Support throwing external error ([#2629](https://github.com/mozilla/uniffi-rs/pull/2629))
3030
* The `NoPointer` placeholder object used to create fake interface instances has been renamed to `NoHandle`
3131
- Python:
32+
* Improved how default values are handled in function signatures etc and more canonical use of Python dataclasses, all towards making mypy happier ([#2552](https://github.com/mozilla/uniffi-rs/pull/2552))
3233
* Methods now have typing annotations for return values ([#2625](https://github.com/mozilla/uniffi-rs/issues/2625))
3334
* Fix relative imports ([#2657](https://github.com/mozilla/uniffi-rs/pull/2657))
3435
* Fix shadowing param names with internal variables in Python (#2628/#2645)

fixtures/keywords/rust/Cargo.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ name = "uniffi_keywords_rust"
1212
thiserror = "2"
1313
uniffi = { workspace = true }
1414

15-
[build-dependencies]
16-
uniffi = { workspace = true, features = ["build"] }
17-
1815
[dev-dependencies]
1916
uniffi = { workspace = true, features = ["bindgen-tests"] }
2017

fixtures/keywords/rust/build.rs

Lines changed: 0 additions & 7 deletions
This file was deleted.

fixtures/keywords/rust/src/keywords.udl

Lines changed: 0 additions & 50 deletions
This file was deleted.

fixtures/keywords/rust/src/lib.rs

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5+
//! A combined Rust + Python keywords test, ensuring we overlap enough so we have
6+
//! good tests for both.
7+
58
use std::{collections::HashMap, sync::Arc};
69

10+
#[uniffi::export]
711
pub fn r#if(_async: u8) {}
812

913
#[allow(non_camel_case_types)]
14+
#[derive(uniffi::Object)]
1015
pub struct r#break {}
1116

1217
impl r#break {
@@ -16,17 +21,31 @@ impl r#break {
1621
pub fn r#break(&self, v: HashMap<u8, Arc<r#break>>) -> Option<HashMap<u8, Arc<r#break>>> {
1722
Some(v)
1823
}
19-
fn r#continue(&self, _v: Vec<Box<dyn r#continue>>) {}
24+
pub fn r#continue(&self, _v: Vec<Box<dyn r#continue>>) {}
2025
pub fn r#yield(&self, _async: u8) {}
2126
pub fn r#async(&self, _yield: Option<u8>) {}
2227
}
2328

2429
#[allow(non_camel_case_types)]
25-
pub trait r#continue {
30+
#[uniffi::export]
31+
pub trait r#continue: Send + Sync {
32+
fn r#return(&self, _v: r#return) -> r#return;
33+
fn r#continue(&self) -> Option<Arc<dyn r#continue>>;
34+
fn r#break(&self, _v: Option<Arc<r#break>>) -> HashMap<u8, Arc<r#break>>;
35+
fn r#while(&self, _v: Vec<r#while>) -> r#while;
36+
fn r#yield(&self, _v: HashMap<u8, Vec<r#yield>>) -> Option<HashMap<u8, Vec<r#yield>>>;
37+
}
38+
39+
#[allow(non_camel_case_types)]
40+
#[derive(uniffi::Object)]
41+
pub struct r#in {}
42+
43+
#[uniffi::export]
44+
impl r#continue for r#in {
2645
fn r#return(&self, _v: r#return) -> r#return {
2746
unimplemented!()
2847
}
29-
fn r#continue(&self) -> Option<Box<dyn r#continue>> {
48+
fn r#continue(&self) -> Option<Arc<dyn r#continue>> {
3049
unimplemented!()
3150
}
3251
fn r#break(&self, _v: Option<Arc<r#break>>) -> HashMap<u8, Arc<r#break>> {
@@ -41,20 +60,19 @@ pub trait r#continue {
4160
}
4261

4362
#[allow(non_camel_case_types)]
44-
#[derive(uniffi::Object)]
45-
pub struct r#in {}
46-
47-
#[uniffi::export]
48-
impl r#continue for r#in {}
49-
50-
#[allow(non_camel_case_types)]
51-
#[derive(Debug)]
63+
#[derive(uniffi::Record, Debug)]
5264
pub struct r#return {
65+
#[uniffi(default)]
5366
r#yield: u8,
67+
#[uniffi(default)]
5468
r#async: Option<u8>,
69+
// bool for python/mypy - #2552
70+
#[uniffi(default)]
71+
bool: bool,
5572
}
5673

5774
#[allow(non_camel_case_types)]
75+
#[derive(uniffi::Record)]
5876
pub struct r#while {
5977
r#return: r#return,
6078
r#yield: Vec<r#yield>,
@@ -64,19 +82,21 @@ pub struct r#while {
6482
}
6583

6684
#[allow(non_camel_case_types)]
85+
#[derive(uniffi::Enum)]
6786
pub enum r#async {
6887
#[allow(non_camel_case_types)]
69-
r#as { r#async: u8 },
88+
// bool for python/mypy, #2552
89+
r#as { r#async: u8, bool: bool },
7090
}
7191

7292
#[allow(non_camel_case_types)]
73-
#[derive(Debug)]
93+
#[derive(uniffi::Enum, Debug)]
7494
pub enum r#yield {
7595
r#async,
7696
}
7797

7898
#[allow(non_camel_case_types)]
79-
#[derive(Debug, thiserror::Error)]
99+
#[derive(uniffi::Error, Debug, thiserror::Error)]
80100
pub enum r#for {
81101
#[error("return")]
82102
r#return { r#return: r#return },
@@ -94,4 +114,4 @@ pub fn get_else(e: r#else) -> r#else {
94114
e
95115
}
96116

97-
uniffi::include_scaffolding!("keywords");
117+
uniffi::setup_scaffolding!("keywords_rust");

fixtures/keywords/rust/tests/bindings/test_keywords.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@
99
import keywords_rust
1010
# but might as well call something.
1111
keywords_rust._if(0)
12+
13+
# python/mypy tests - not keywords, but type names in strange places.
14+
assert(keywords_rust.Return().bool == False)

uniffi_bindgen/src/bindings/python/pipeline/default.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,53 @@ pub fn pass(namespace: &mut Namespace) -> Result<()> {
1414

1515
namespace.try_visit_mut(|default: &mut DefaultValueNode| {
1616
default.py_default = render_default(&default.default)?;
17+
// Many types can use the appropriate literal directly.
18+
default.arg_literal = match &default.default {
19+
DefaultValue::Default(tn) => match &tn.ty {
20+
Type::UInt8
21+
| Type::UInt16
22+
| Type::UInt32
23+
| Type::UInt64
24+
| Type::Int8
25+
| Type::Int16
26+
| Type::Int32
27+
| Type::Int64
28+
| Type::Float32
29+
| Type::Float64
30+
| Type::Boolean
31+
| Type::Bytes
32+
| Type::String
33+
| Type::Optional { .. } => default.py_default.clone(),
34+
_ => "_DEFAULT".to_string(),
35+
},
36+
DefaultValue::Literal(ln) => {
37+
if can_use_literal(&ln.lit) {
38+
default.py_default.clone()
39+
} else {
40+
"_DEFAULT".to_string()
41+
}
42+
}
43+
};
44+
default.is_arg_literal = default.py_default == default.arg_literal;
1745
Ok(())
1846
})
1947
}
2048

49+
pub fn can_use_literal(literal: &Literal) -> bool {
50+
match literal {
51+
Literal::Boolean(_)
52+
| Literal::String(_)
53+
| Literal::UInt(_, _, _)
54+
| Literal::Int(_, _, _)
55+
| Literal::Float(_, _)
56+
| Literal::Enum(_, _) => true,
57+
Literal::Some { inner } => match &**inner {
58+
DefaultValue::Literal(inner_lit) => can_use_literal(&inner_lit.lit),
59+
_ => false,
60+
},
61+
_ => false,
62+
}
63+
}
2164
pub(super) fn render_default(default: &DefaultValue) -> Result<String> {
2265
Ok(match default {
2366
DefaultValue::Default(tn) => match &tn.ty {

uniffi_bindgen/src/bindings/python/pipeline/nodes.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ pub struct DefaultValueNode {
177177
pub default: DefaultValue,
178178
/// The default value rendered as a Python string
179179
pub py_default: String,
180+
/// The default value as specified as a literal in function args.
181+
pub arg_literal: String,
182+
/// Convenience - whether the arg literal is our literal.
183+
pub is_arg_literal: bool,
180184
}
181185

182186
#[derive(Debug, Clone, Node, Eq, PartialEq, Hash)]

uniffi_bindgen/src/bindings/python/templates/CallableArgs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{%- for arg in callable.arguments -%}
22
{{ arg.name }}
33
{%- match arg.default %}
4-
{%- when Some(literal) %}: typing.Union[object, {{ arg.ty.type_name }}] = _DEFAULT
4+
{%- when Some(default) %}: {% if !default.is_arg_literal %}typing.Union[object, {{ arg.ty.type_name }}]{% else %}{{ arg.ty.type_name }}{% endif %} = {{ default.arg_literal }}
55
{%- else %}: {{ arg.ty.type_name }}
66
{%- endmatch %}
77
{%- if !loop.last %},{% endif -%}

uniffi_bindgen/src/bindings/python/templates/CallableBody.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
{%- for arg in callable.arguments %}
22
{%- if let Some(default) = arg.default %}
3-
if {{ arg.name }} is _DEFAULT:
3+
{%- if !default.is_arg_literal %}
4+
if {{ arg.name }} is {{ default.arg_literal }}:
45
{{ arg.name }} = {{ default.py_default }}
56
{%- endif %}
7+
{%- endif %}
68
{{ arg.ty.ffi_converter_name }}.check_lower({{ arg.name }})
79
{% endfor -%}
810

0 commit comments

Comments
 (0)