diff --git a/vm/src/macros.rs b/vm/src/macros.rs index de94fc4415..a05972961a 100644 --- a/vm/src/macros.rs +++ b/vm/src/macros.rs @@ -111,6 +111,7 @@ macro_rules! no_kwargs { }; } +#[macro_export] macro_rules! py_module { ( $ctx:expr, $module_name:expr, { $($name:expr => $value:expr),* $(,)* }) => { { diff --git a/vm/src/pyobject.rs b/vm/src/pyobject.rs index 0158028d02..dd1f839ff7 100644 --- a/vm/src/pyobject.rs +++ b/vm/src/pyobject.rs @@ -920,6 +920,29 @@ impl PyFuncArgs { } None } + + pub fn get_optional_kwarg_with_type( + &self, + key: &str, + ty: PyObjectRef, + vm: &mut VirtualMachine, + ) -> Result, PyObjectRef> { + match self.get_optional_kwarg(key) { + Some(kwarg) => { + if objtype::isinstance(&kwarg, &ty) { + Ok(Some(kwarg)) + } else { + let expected_ty_name = vm.to_pystr(&ty)?; + let actual_ty_name = vm.to_pystr(&kwarg.typ())?; + Err(vm.new_type_error(format!( + "argument of type {} is required for named parameter `{}` (got: {})", + expected_ty_name, key, actual_ty_name + ))) + } + } + None => Ok(None), + } + } } /// Rather than determining the type of a python object, this enum is more diff --git a/wasm/demo/snippets/fetch.py b/wasm/demo/snippets/fetch.py new file mode 100644 index 0000000000..63b21b69d8 --- /dev/null +++ b/wasm/demo/snippets/fetch.py @@ -0,0 +1,12 @@ +from browser import fetch + +def fetch_handler(res): + print(f"headers: {res['headers']}") + +fetch( + "https://httpbin.org/get", + fetch_handler, + lambda err: print(f"error: {err}"), + response_format="json", + headers={"X-Header-Thing": "rustpython is neat!"}, +) diff --git a/wasm/lib/src/browser_module.rs b/wasm/lib/src/browser_module.rs new file mode 100644 index 0000000000..93b6ec5cde --- /dev/null +++ b/wasm/lib/src/browser_module.rs @@ -0,0 +1,138 @@ +use crate::{convert, vm_class::AccessibleVM, wasm_builtins::window}; +use futures::{future, Future}; +use js_sys::Promise; +use rustpython_vm::obj::objstr; +use rustpython_vm::pyobject::{PyContext, PyFuncArgs, PyObjectRef, PyResult, TypeProtocol}; +use rustpython_vm::VirtualMachine; +use wasm_bindgen::{prelude::*, JsCast}; +use wasm_bindgen_futures::{future_to_promise, JsFuture}; + +enum FetchResponseFormat { + Json, + Text, + ArrayBuffer, +} + +impl FetchResponseFormat { + fn from_str(vm: &mut VirtualMachine, s: &str) -> Result { + match s { + "json" => Ok(FetchResponseFormat::Json), + "text" => Ok(FetchResponseFormat::Text), + "array_buffer" => Ok(FetchResponseFormat::ArrayBuffer), + _ => Err(vm.new_type_error("Unkown fetch response_format".into())), + } + } + fn get_response(&self, response: &web_sys::Response) -> Result { + match self { + FetchResponseFormat::Json => response.json(), + FetchResponseFormat::Text => response.text(), + FetchResponseFormat::ArrayBuffer => response.array_buffer(), + } + } +} + +fn browser_fetch(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { + arg_check!( + vm, + args, + required = [ + (url, Some(vm.ctx.str_type())), + (handler, Some(vm.ctx.function_type())) + ], + optional = [(reject_handler, Some(vm.ctx.function_type()))] + ); + let response_format = + args.get_optional_kwarg_with_type("response_format", vm.ctx.str_type(), vm)?; + let method = args.get_optional_kwarg_with_type("method", vm.ctx.str_type(), vm)?; + let headers = args.get_optional_kwarg_with_type("headers", vm.ctx.dict_type(), vm)?; + let body = args.get_optional_kwarg("body"); + let content_type = args.get_optional_kwarg_with_type("content_type", vm.ctx.str_type(), vm)?; + + let response_format = match response_format { + Some(s) => FetchResponseFormat::from_str(vm, &objstr::get_value(&s))?, + None => FetchResponseFormat::Text, + }; + + let mut opts = web_sys::RequestInit::new(); + + match method { + Some(s) => opts.method(&objstr::get_value(&s)), + None => opts.method("GET"), + }; + + if let Some(body) = body { + opts.body(Some(&convert::py_to_js(vm, body))); + } + + let request = web_sys::Request::new_with_str_and_init(&objstr::get_value(url), &opts) + .map_err(|err| convert::js_py_typeerror(vm, err))?; + + if let Some(headers) = headers { + let h = request.headers(); + for (key, value) in rustpython_vm::obj::objdict::get_key_value_pairs(&headers) { + let key = objstr::get_value(&vm.to_str(&key)?); + let value = objstr::get_value(&vm.to_str(&value)?); + h.set(&key, &value) + .map_err(|err| convert::js_py_typeerror(vm, err))?; + } + } + + if let Some(content_type) = content_type { + request + .headers() + .set("Content-Type", &objstr::get_value(&content_type)) + .map_err(|err| convert::js_py_typeerror(vm, err))?; + } + + let window = window(); + let request_prom = window.fetch_with_request(&request); + + let handler = handler.clone(); + let reject_handler = reject_handler.cloned(); + + let acc_vm = AccessibleVM::from_vm(vm); + + let future = JsFuture::from(request_prom) + .and_then(move |val| { + let response = val + .dyn_into::() + .expect("val to be of type Response"); + response_format.get_response(&response) + }) + .and_then(JsFuture::from) + .then(move |val| { + let vm = &mut acc_vm + .upgrade() + .expect("that the VM *not* be destroyed while promise is being resolved"); + match val { + Ok(val) => { + let val = convert::js_to_py(vm, val); + let args = PyFuncArgs::new(vec![val], vec![]); + let _ = vm.invoke(handler, args); + } + Err(val) => { + if let Some(reject_handler) = reject_handler { + let val = convert::js_to_py(vm, val); + let args = PyFuncArgs::new(vec![val], vec![]); + let _ = vm.invoke(reject_handler, args); + } + } + } + future::ok(JsValue::UNDEFINED) + }); + future_to_promise(future); + + Ok(vm.get_none()) +} + +const BROWSER_NAME: &str = "browser"; + +pub fn mk_module(ctx: &PyContext) -> PyObjectRef { + py_module!(ctx, BROWSER_NAME, { + "fetch" => ctx.new_rustfunc(browser_fetch) + }) +} + +pub fn setup_browser_module(vm: &mut VirtualMachine) { + vm.stdlib_inits.insert(BROWSER_NAME.to_string(), mk_module); +} diff --git a/wasm/lib/src/lib.rs b/wasm/lib/src/lib.rs index 2c69866e90..1aff0e6de3 100644 --- a/wasm/lib/src/lib.rs +++ b/wasm/lib/src/lib.rs @@ -1,11 +1,14 @@ +pub mod browser_module; pub mod convert; pub mod vm_class; pub mod wasm_builtins; extern crate futures; extern crate js_sys; +#[macro_use] extern crate rustpython_vm; extern crate wasm_bindgen; +extern crate wasm_bindgen_futures; extern crate web_sys; use js_sys::{Object, Reflect, TypeError}; diff --git a/wasm/lib/src/vm_class.rs b/wasm/lib/src/vm_class.rs index 5af38ef39b..f803d2d776 100644 --- a/wasm/lib/src/vm_class.rs +++ b/wasm/lib/src/vm_class.rs @@ -1,5 +1,6 @@ +use crate::browser_module::setup_browser_module; use crate::convert; -use crate::wasm_builtins::{self, setup_wasm_builtins}; +use crate::wasm_builtins; use js_sys::{SyntaxError, TypeError}; use rustpython_vm::{ compile, @@ -17,12 +18,12 @@ pub(crate) struct StoredVirtualMachine { } impl StoredVirtualMachine { - fn new(id: String, inject_builtins: bool) -> StoredVirtualMachine { + fn new(id: String, inject_browser_module: bool) -> StoredVirtualMachine { let mut vm = VirtualMachine::new(); let builtin = vm.get_builtin_scope(); let scope = vm.context().new_scope(Some(builtin)); - if inject_builtins { - setup_wasm_builtins(&mut vm, &scope); + if inject_browser_module { + setup_browser_module(&mut vm); } vm.wasm_id = Some(id); StoredVirtualMachine { vm, scope } @@ -42,12 +43,12 @@ pub struct VMStore; #[wasm_bindgen(js_class = vmStore)] impl VMStore { - pub fn init(id: String, inject_builtins: Option) -> WASMVirtualMachine { + pub fn init(id: String, inject_browser_module: Option) -> WASMVirtualMachine { STORED_VMS.with(|cell| { let mut vms = cell.borrow_mut(); if !vms.contains_key(&id) { let stored_vm = - StoredVirtualMachine::new(id.clone(), inject_builtins.unwrap_or(true)); + StoredVirtualMachine::new(id.clone(), inject_browser_module.unwrap_or(true)); vms.insert(id.clone(), Rc::new(RefCell::new(stored_vm))); } }); diff --git a/wasm/lib/src/wasm_builtins.rs b/wasm/lib/src/wasm_builtins.rs index 211eedb415..b8f4e121ff 100644 --- a/wasm/lib/src/wasm_builtins.rs +++ b/wasm/lib/src/wasm_builtins.rs @@ -2,23 +2,17 @@ //! //! This is required because some feature like I/O works differently in the browser comparing to //! desktop. -//! Implements functions listed here: https://docs.python.org/3/library/builtins.html and some -//! others. - -extern crate futures; -extern crate js_sys; -extern crate wasm_bindgen; -extern crate web_sys; +//! Implements functions listed here: https://docs.python.org/3/library/builtins.html. use crate::convert; -use js_sys::Array; +use js_sys::{self, Array}; use rustpython_vm::obj::{objstr, objtype}; use rustpython_vm::pyobject::{IdProtocol, PyFuncArgs, PyObjectRef, PyResult, TypeProtocol}; use rustpython_vm::VirtualMachine; use wasm_bindgen::{prelude::*, JsCast}; -use web_sys::{console, HtmlTextAreaElement}; +use web_sys::{self, console, HtmlTextAreaElement}; -fn window() -> web_sys::Window { +pub(crate) fn window() -> web_sys::Window { web_sys::window().expect("Window to be available") } @@ -103,8 +97,3 @@ pub fn builtin_print_console(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyRes console::log(&arr); Ok(vm.get_none()) } - -pub fn setup_wasm_builtins(vm: &mut VirtualMachine, scope: &PyObjectRef) { - let ctx = vm.context(); - ctx.set_attr(scope, "print", ctx.new_rustfunc(builtin_print_console)); -}