Skip to content

Commit 72348e0

Browse files
authored
Support for @unstable WIT features (#110)
* Add test for current behavior * Allow for binding generation of multiple unstable features * Test features are properly enabled via componentize
1 parent f19009e commit 72348e0

File tree

5 files changed

+221
-13
lines changed

5 files changed

+221
-13
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ semver = "1.0.23"
5151
[dev-dependencies]
5252
proptest = "1.5.0"
5353
hex = "0.4.3"
54+
tempfile = "3.13.0"
5455

5556
[build-dependencies]
5657
anyhow = "1.0.89"

src/command.rs

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub struct Options {
2222
pub command: Command,
2323
}
2424

25-
#[derive(clap::Args, Debug)]
25+
#[derive(clap::Args, Clone, Debug)]
2626
pub struct Common {
2727
/// File or directory containing WIT document(s)
2828
#[arg(short = 'd', long)]
@@ -35,6 +35,19 @@ pub struct Common {
3535
/// Disable non-error output
3636
#[arg(short = 'q', long)]
3737
pub quiet: bool,
38+
39+
/// Comma-separated list of features that should be enabled when processing
40+
/// WIT files.
41+
///
42+
/// This enables using `@unstable` annotations in WIT files.
43+
#[clap(long)]
44+
features: Vec<String>,
45+
46+
/// Whether or not to activate all WIT features when processing WIT files.
47+
///
48+
/// This enables using `@unstable` annotations in WIT files.
49+
#[clap(long)]
50+
all_features: bool,
3851
}
3952

4053
#[derive(clap::Subcommand, Debug)]
@@ -120,6 +133,8 @@ fn generate_bindings(common: Common, bindings: Bindings) -> Result<()> {
120133
.wit_path
121134
.unwrap_or_else(|| Path::new("wit").to_owned()),
122135
common.world.as_deref(),
136+
&common.features,
137+
common.all_features,
123138
bindings.world_module.as_deref(),
124139
&bindings.output_dir,
125140
)
@@ -140,6 +155,8 @@ fn componentize(common: Common, componentize: Componentize) -> Result<()> {
140155
Runtime::new()?.block_on(crate::componentize(
141156
common.wit_path.as_deref(),
142157
common.world.as_deref(),
158+
&common.features,
159+
common.all_features,
143160
&python_path.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
144161
&componentize
145162
.module_worlds
@@ -241,3 +258,156 @@ fn find_dir(name: &str, path: &Path) -> Result<Option<PathBuf>> {
241258

242259
Ok(None)
243260
}
261+
262+
#[cfg(test)]
263+
mod tests {
264+
use std::io::Write;
265+
266+
use super::*;
267+
268+
/// Generates a WIT file which has unstable feature "x"
269+
fn gated_x_wit_file() -> Result<tempfile::NamedTempFile, anyhow::Error> {
270+
let mut wit = tempfile::Builder::new()
271+
.prefix("gated")
272+
.suffix(".wit")
273+
.tempfile()?;
274+
write!(
275+
wit,
276+
r#"
277+
package foo:bar@1.2.3;
278+
279+
world bindings {{
280+
@unstable(feature = x)
281+
import x: func();
282+
@since(version = 1.2.3)
283+
export y: func();
284+
}}
285+
"#,
286+
)?;
287+
Ok(wit)
288+
}
289+
290+
#[test]
291+
fn unstable_bindings_not_generated() -> Result<()> {
292+
// Given a WIT file with gated features
293+
let wit = gated_x_wit_file()?;
294+
let out_dir = tempfile::tempdir()?;
295+
296+
// When generating the bindings for this WIT world
297+
let common = Common {
298+
wit_path: Some(wit.path().into()),
299+
world: None,
300+
quiet: false,
301+
features: vec![],
302+
all_features: false,
303+
};
304+
let bindings = Bindings {
305+
output_dir: out_dir.path().into(),
306+
world_module: None,
307+
};
308+
generate_bindings(common, bindings)?;
309+
310+
// Then the gated feature doesn't appear
311+
let generated = fs::read_to_string(out_dir.path().join("bindings/__init__.py"))?;
312+
313+
assert!(!generated.contains("def x() -> None:"));
314+
315+
Ok(())
316+
}
317+
318+
#[test]
319+
fn unstable_bindings_generated_with_feature_flag() -> Result<()> {
320+
// Given a WIT file with gated features
321+
let wit = gated_x_wit_file()?;
322+
let out_dir = tempfile::tempdir()?;
323+
324+
// When generating the bindings for this WIT world
325+
let common = Common {
326+
wit_path: Some(wit.path().into()),
327+
world: None,
328+
quiet: false,
329+
features: vec!["x".to_owned()],
330+
all_features: false,
331+
};
332+
let bindings = Bindings {
333+
output_dir: out_dir.path().into(),
334+
world_module: None,
335+
};
336+
generate_bindings(common, bindings)?;
337+
338+
// Then the gated feature doesn't appear
339+
let generated = fs::read_to_string(out_dir.path().join("bindings/__init__.py"))?;
340+
341+
assert!(generated.contains("def x() -> None:"));
342+
343+
Ok(())
344+
}
345+
346+
#[test]
347+
fn unstable_bindings_generated_for_all_features() -> Result<()> {
348+
// Given a WIT file with gated features
349+
let wit = gated_x_wit_file()?;
350+
let out_dir = tempfile::tempdir()?;
351+
352+
// When generating the bindings for this WIT world
353+
let common = Common {
354+
wit_path: Some(wit.path().into()),
355+
world: None,
356+
quiet: false,
357+
features: vec![],
358+
all_features: true,
359+
};
360+
let bindings = Bindings {
361+
output_dir: out_dir.path().into(),
362+
world_module: None,
363+
};
364+
generate_bindings(common, bindings)?;
365+
366+
// Then the gated feature doesn't appear
367+
let generated = fs::read_to_string(out_dir.path().join("bindings/__init__.py"))?;
368+
369+
assert!(generated.contains("def x() -> None:"));
370+
371+
Ok(())
372+
}
373+
374+
#[test]
375+
fn unstable_features_used_in_componentize() -> Result<()> {
376+
// Given bindings to a WIT file with gated features and a Python file that uses them
377+
let wit = gated_x_wit_file()?;
378+
let out_dir = tempfile::tempdir()?;
379+
let common = Common {
380+
wit_path: Some(wit.path().into()),
381+
world: None,
382+
quiet: false,
383+
features: vec!["x".to_owned()],
384+
all_features: false,
385+
};
386+
let bindings = Bindings {
387+
output_dir: out_dir.path().into(),
388+
world_module: None,
389+
};
390+
generate_bindings(common.clone(), bindings)?;
391+
fs::write(
392+
out_dir.path().join("app.py"),
393+
r#"
394+
import bindings
395+
from bindings import x
396+
397+
class Bindings(bindings.Bindings):
398+
def y(self) -> None:
399+
x()
400+
"#,
401+
)?;
402+
403+
// Building the component succeeds
404+
let componentize_opts = Componentize {
405+
app_name: "app".to_owned(),
406+
python_path: vec![out_dir.path().to_string_lossy().into()],
407+
module_worlds: vec![],
408+
output: out_dir.path().join("app.wasm"),
409+
stub_wasi: false,
410+
};
411+
componentize(common, componentize_opts)
412+
}
413+
}

src/lib.rs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,15 @@ impl Invoker for MyInvoker {
170170
pub fn generate_bindings(
171171
wit_path: &Path,
172172
world: Option<&str>,
173+
features: &[String],
174+
all_features: bool,
173175
world_module: Option<&str>,
174176
output_dir: &Path,
175177
) -> Result<()> {
176178
// TODO: Split out and reuse the code responsible for finding and using componentize-py.toml files in the
177179
// `componentize` function below, since that can affect the bindings we should be generating.
178180

179-
let (resolve, world) = parse_wit(wit_path, world)?;
181+
let (resolve, world) = parse_wit(wit_path, world, features, all_features)?;
180182
let summary = Summary::try_new(&resolve, &iter::once(world).collect())?;
181183
let world_name = resolve.worlds[world].name.to_snake_case().escape();
182184
let world_module = world_module.unwrap_or(&world_name);
@@ -197,6 +199,8 @@ pub fn generate_bindings(
197199
pub async fn componentize(
198200
wit_path: Option<&Path>,
199201
world: Option<&str>,
202+
features: &[String],
203+
all_features: bool,
200204
python_path: &[&str],
201205
module_worlds: &[(&str, &str)],
202206
app_name: &str,
@@ -219,7 +223,7 @@ pub async fn componentize(
219223
// Next, iterate over all the WIT directories, merging them into a single `Resolve`, and matching Python
220224
// packages to `WorldId`s.
221225
let (mut resolve, mut main_world) = if let Some(path) = wit_path {
222-
let (resolve, world) = parse_wit(path, world)?;
226+
let (resolve, world) = parse_wit(path, world, features, all_features)?;
223227
(Some(resolve), Some(world))
224228
} else {
225229
(None, None)
@@ -230,7 +234,7 @@ pub async fn componentize(
230234
.map(|(module, (config, world))| {
231235
Ok((module, match (world, config.config.wit_directory.as_deref()) {
232236
(_, Some(wit_path)) => {
233-
let (my_resolve, mut world) = parse_wit(&config.path.join(wit_path), *world)?;
237+
let (my_resolve, mut world) = parse_wit(&config.path.join(wit_path), *world, features, all_features)?;
234238

235239
if let Some(resolve) = &mut resolve {
236240
let remap = resolve.merge(my_resolve)?;
@@ -254,10 +258,11 @@ pub async fn componentize(
254258
} else {
255259
// If no WIT directory was provided as a parameter and none were referenced by Python packages, use ./wit
256260
// by default.
257-
let (my_resolve, world) = parse_wit(Path::new("wit"), world).context(
258-
"no WIT files found; please specify the directory or file \
261+
let (my_resolve, world) = parse_wit(Path::new("wit"), world, features, all_features)
262+
.context(
263+
"no WIT files found; please specify the directory or file \
259264
containing the WIT world you wish to target",
260-
)?;
265+
)?;
261266
main_world = Some(world);
262267
my_resolve
263268
};
@@ -558,8 +563,25 @@ pub async fn componentize(
558563
Ok(())
559564
}
560565

561-
fn parse_wit(path: &Path, world: Option<&str>) -> Result<(Resolve, WorldId)> {
562-
let mut resolve = Resolve::default();
566+
fn parse_wit(
567+
path: &Path,
568+
world: Option<&str>,
569+
features: &[String],
570+
all_features: bool,
571+
) -> Result<(Resolve, WorldId)> {
572+
let mut resolve = Resolve {
573+
all_features,
574+
..Default::default()
575+
};
576+
for features in features {
577+
for feature in features
578+
.split(',')
579+
.flat_map(|s| s.split_whitespace())
580+
.filter(|f| !f.is_empty())
581+
{
582+
resolve.features.insert(feature.to_string());
583+
}
584+
}
563585
let pkg = if path.is_dir() {
564586
resolve.push_dir(path)?.0
565587
} else {

src/python.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ use {
77
#[allow(clippy::too_many_arguments)]
88
#[pyo3::pyfunction]
99
#[pyo3(name = "componentize")]
10-
#[pyo3(signature = (wit_path, world, python_path, module_worlds, app_name, output_path, stub_wasi))]
10+
#[pyo3(signature = (wit_path, world, features, all_features, python_path, module_worlds, app_name, output_path, stub_wasi))]
1111
fn python_componentize(
1212
wit_path: Option<PathBuf>,
1313
world: Option<&str>,
14+
features: Vec<String>,
15+
all_features: bool,
1416
python_path: Vec<&str>,
1517
module_worlds: Vec<(&str, &str)>,
1618
app_name: &str,
@@ -21,6 +23,8 @@ fn python_componentize(
2123
Runtime::new()?.block_on(crate::componentize(
2224
wit_path.as_deref(),
2325
world,
26+
&features,
27+
all_features,
2428
&python_path,
2529
&module_worlds,
2630
app_name,
@@ -34,15 +38,24 @@ fn python_componentize(
3438

3539
#[pyo3::pyfunction]
3640
#[pyo3(name = "generate_bindings")]
37-
#[pyo3(signature = (wit_path, world, world_module, output_dir))]
41+
#[pyo3(signature = (wit_path, world, features, all_features, world_module, output_dir))]
3842
fn python_generate_bindings(
3943
wit_path: PathBuf,
4044
world: Option<&str>,
45+
features: Vec<String>,
46+
all_features: bool,
4147
world_module: Option<&str>,
4248
output_dir: PathBuf,
4349
) -> PyResult<()> {
44-
crate::generate_bindings(&wit_path, world, world_module, &output_dir)
45-
.map_err(|e| PyAssertionError::new_err(format!("{e:?}")))
50+
crate::generate_bindings(
51+
&wit_path,
52+
world,
53+
&features,
54+
all_features,
55+
world_module,
56+
&output_dir,
57+
)
58+
.map_err(|e| PyAssertionError::new_err(format!("{e:?}")))
4659
}
4760

4861
#[pyo3::pyfunction]

src/test.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ async fn make_component(
6363
crate::componentize(
6464
Some(&tempdir.path().join("app.wit")),
6565
None,
66+
&[],
67+
false,
6668
&python_path
6769
.iter()
6870
.copied()

0 commit comments

Comments
 (0)