mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Split pyscript into multiple files (#1338)
In the future this should help us leak fewer names into the pyscript namespace. Rather than assigning to the pyscript module from JavaScript, we mount a separate private JS module with the extra names needed by PyScript. I moved a bit more interpeter intialization into remote_interpreter. I added a deprecation warning for `pyscript.js`: the proper way to access `js` is `import js`. --------- Co-authored-by: Antonio Cuni <anto.cuni@gmail.com>
This commit is contained in:
@@ -60,7 +60,7 @@ export class InterpreterClient extends Object {
|
||||
* serializable.
|
||||
*/
|
||||
async run(code: string, id?: string): Promise<{ result: any }> {
|
||||
return this._remote.pyscript_py._run_pyscript(code, id);
|
||||
return this._remote.pyscript_internal.run_pyscript(code, id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,8 +3,7 @@ import './styles/pyscript_base.css';
|
||||
import { loadConfigFromElement } from './pyconfig';
|
||||
import type { AppConfig } from './pyconfig';
|
||||
import { InterpreterClient } from './interpreter_client';
|
||||
import { version } from './version';
|
||||
import { PluginManager, define_custom_element, Plugin, PythonPlugin } from './plugin';
|
||||
import { PluginManager, Plugin, PythonPlugin } from './plugin';
|
||||
import { make_PyScript, initHandlers, mountElements } from './components/pyscript';
|
||||
import { getLogger } from './logger';
|
||||
import { showWarning, globalExport, createLock } from './utils';
|
||||
@@ -261,24 +260,6 @@ export class PyScriptApp {
|
||||
// XXX: maybe the following calls could be parallelized, instead of
|
||||
// await()ing immediately. For now I'm using await to be 100%
|
||||
// compatible with the old behavior.
|
||||
|
||||
// inject `define_custom_element` and showWarning it into the PyScript
|
||||
// module scope
|
||||
// eventually replace the setHandler calls with interpreter._remote.setHandler i.e. the ones mentioned below
|
||||
// await interpreter._remote.setHandler('define_custom_element', Synclink.proxy(define_custom_element));
|
||||
// await interpreter._remote.setHandler('showWarning', Synclink.proxy(showWarning));
|
||||
interpreter._unwrapped_remote.setHandler('define_custom_element', define_custom_element);
|
||||
interpreter._unwrapped_remote.setHandler('showWarning', showWarning);
|
||||
await interpreter._remote.pyscript_py._set_version_info(version);
|
||||
|
||||
// import some carefully selected names into the global namespace
|
||||
await interpreter.run(`
|
||||
import js
|
||||
import pyscript
|
||||
from pyscript import Element, display, HTML
|
||||
pyscript._install_deprecated_globals_2022_12_1(globals())
|
||||
`);
|
||||
|
||||
await Promise.all([this.installPackages(), this.fetchPaths(interpreter)]);
|
||||
|
||||
//This may be unnecessary - only useful if plugins try to import files fetch'd in fetchPaths()
|
||||
@@ -414,7 +395,7 @@ modules must contain a "plugin" attribute. For more information check the plugin
|
||||
this.incrementPendingTags();
|
||||
this.decrementPendingTags();
|
||||
await this.scriptTagsPromise;
|
||||
await this.interpreter._remote.pyscript_py._schedule_deferred_tasks();
|
||||
await this.interpreter._remote.pyscript_internal.schedule_deferred_tasks();
|
||||
}
|
||||
|
||||
// ================= registraton API ====================
|
||||
@@ -443,4 +424,4 @@ if (typeof jest === 'undefined') {
|
||||
globalApp.readyPromise = globalApp.main();
|
||||
}
|
||||
|
||||
export { version };
|
||||
export { version } from './version';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import html
|
||||
|
||||
from pyscript import Plugin, js
|
||||
import js
|
||||
from pyscript import Plugin
|
||||
|
||||
js.console.warn(
|
||||
"WARNING: This plugin is still in a very experimental phase and will likely change"
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function pyExec(
|
||||
outElem: HTMLElement,
|
||||
): Promise<{ result: any }> {
|
||||
ensureUniqueId(outElem);
|
||||
if (await interpreter._remote.pyscript_py.uses_top_level_await(pysrc)) {
|
||||
if (await interpreter._remote.pyscript_internal.uses_top_level_await(pysrc)) {
|
||||
const err = new UserError(
|
||||
ErrorCode.TOP_LEVEL_AWAIT,
|
||||
'The use of top-level "await", "async for", and ' +
|
||||
|
||||
@@ -1,808 +1,54 @@
|
||||
import ast
|
||||
import asyncio
|
||||
import base64
|
||||
import contextvars
|
||||
import html
|
||||
import io
|
||||
import re
|
||||
import time
|
||||
from collections import namedtuple
|
||||
from collections.abc import Callable
|
||||
from contextlib import contextmanager
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
|
||||
import js
|
||||
from js import setTimeout
|
||||
from pyodide.webloop import WebLoop
|
||||
|
||||
try:
|
||||
from pyodide.code import eval_code
|
||||
from pyodide.ffi import JsProxy, create_once_callable, create_proxy
|
||||
except ImportError:
|
||||
from pyodide import JsProxy, create_once_callable, create_proxy, eval_code
|
||||
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
MIME_METHODS = {
|
||||
"__repr__": "text/plain",
|
||||
"_repr_html_": "text/html",
|
||||
"_repr_markdown_": "text/markdown",
|
||||
"_repr_svg_": "image/svg+xml",
|
||||
"_repr_png_": "image/png",
|
||||
"_repr_pdf_": "application/pdf",
|
||||
"_repr_jpeg_": "image/jpeg",
|
||||
"_repr_latex": "text/latex",
|
||||
"_repr_json_": "application/json",
|
||||
"_repr_javascript_": "application/javascript",
|
||||
"savefig": "image/png",
|
||||
}
|
||||
|
||||
|
||||
def render_image(mime, value, meta):
|
||||
# If the image value is using bytes we should convert it to base64
|
||||
# otherwise it will return raw bytes and the browser will not be able to
|
||||
# render it.
|
||||
if isinstance(value, bytes):
|
||||
value = base64.b64encode(value).decode("utf-8")
|
||||
|
||||
# This is the pattern of base64 strings
|
||||
base64_pattern = re.compile(
|
||||
r"^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$"
|
||||
)
|
||||
# If value doesn't match the base64 pattern we should encode it to base64
|
||||
if len(value) > 0 and not base64_pattern.match(value):
|
||||
value = base64.b64encode(value.encode("utf-8")).decode("utf-8")
|
||||
|
||||
data = f"data:{mime};charset=utf-8;base64,{value}"
|
||||
attrs = " ".join(['{k}="{v}"' for k, v in meta.items()])
|
||||
return f'<img src="{data}" {attrs}></img>'
|
||||
|
||||
|
||||
def identity(value, meta):
|
||||
return value
|
||||
|
||||
|
||||
MIME_RENDERERS = {
|
||||
"text/plain": html.escape,
|
||||
"text/html": identity,
|
||||
"image/png": lambda value, meta: render_image("image/png", value, meta),
|
||||
"image/jpeg": lambda value, meta: render_image("image/jpeg", value, meta),
|
||||
"image/svg+xml": identity,
|
||||
"application/json": identity,
|
||||
"application/javascript": lambda value, meta: f"<script>{value}</script>",
|
||||
}
|
||||
from _pyscript_js import showWarning
|
||||
|
||||
from ._event_loop import LOOP as loop
|
||||
from ._event_loop import run_until_complete
|
||||
from ._html import (
|
||||
HTML,
|
||||
Element,
|
||||
PyItemTemplate,
|
||||
PyListTemplate,
|
||||
PyWidgetTheme,
|
||||
add_classes,
|
||||
create,
|
||||
display,
|
||||
write,
|
||||
)
|
||||
from ._plugin import Plugin
|
||||
|
||||
# these are set by _set_version_info
|
||||
__version__ = None
|
||||
version_info = None
|
||||
|
||||
|
||||
def _set_version_info(version_from_interpreter: str):
|
||||
"""Sets the __version__ and version_info properties from provided JSON data
|
||||
Args:
|
||||
version_from_interpreter (str): A "dotted" representation of the version:
|
||||
YYYY.MM.m(m).releaselevel
|
||||
Year, Month, and Minor should be integers; releaselevel can be any string
|
||||
"""
|
||||
global __version__
|
||||
global version_info
|
||||
def __getattr__(attr):
|
||||
if attr == "js":
|
||||
global js
|
||||
import js
|
||||
from _pyscript_js import showWarning
|
||||
|
||||
__version__ = version_from_interpreter
|
||||
|
||||
version_parts = version_from_interpreter.split(".")
|
||||
year = int(version_parts[0])
|
||||
month = int(version_parts[1])
|
||||
minor = int(version_parts[2])
|
||||
if len(version_parts) > 3:
|
||||
releaselevel = version_parts[3]
|
||||
else:
|
||||
releaselevel = ""
|
||||
|
||||
VersionInfo = namedtuple("version_info", ("year", "month", "minor", "releaselevel"))
|
||||
version_info = VersionInfo(year, month, minor, releaselevel)
|
||||
|
||||
# we ALSO set PyScript.__version__ and version_info for backwards
|
||||
# compatibility. Should be killed eventually.
|
||||
PyScript.__version__ = __version__
|
||||
PyScript.version_info = version_info
|
||||
|
||||
|
||||
class HTML:
|
||||
"""
|
||||
Wrap a string so that display() can render it as plain HTML
|
||||
"""
|
||||
|
||||
def __init__(self, html):
|
||||
self._html = html
|
||||
|
||||
def _repr_html_(self):
|
||||
return self._html
|
||||
|
||||
|
||||
def eval_formatter(obj, print_method):
|
||||
"""
|
||||
Evaluates a formatter method.
|
||||
"""
|
||||
if print_method == "__repr__":
|
||||
return repr(obj)
|
||||
elif hasattr(obj, print_method):
|
||||
if print_method == "savefig":
|
||||
buf = io.BytesIO()
|
||||
obj.savefig(buf, format="png")
|
||||
buf.seek(0)
|
||||
return base64.b64encode(buf.read()).decode("utf-8")
|
||||
return getattr(obj, print_method)()
|
||||
elif print_method == "_repr_mimebundle_":
|
||||
return {}, {}
|
||||
return None
|
||||
|
||||
|
||||
def format_mime(obj):
|
||||
"""
|
||||
Formats object using _repr_x_ methods.
|
||||
"""
|
||||
if isinstance(obj, str):
|
||||
return html.escape(obj), "text/plain"
|
||||
|
||||
mimebundle = eval_formatter(obj, "_repr_mimebundle_")
|
||||
if isinstance(mimebundle, tuple):
|
||||
format_dict, _ = mimebundle
|
||||
else:
|
||||
format_dict = mimebundle
|
||||
|
||||
output, not_available = None, []
|
||||
for method, mime_type in reversed(MIME_METHODS.items()):
|
||||
if mime_type in format_dict:
|
||||
output = format_dict[mime_type]
|
||||
else:
|
||||
output = eval_formatter(obj, method)
|
||||
|
||||
if output is None:
|
||||
continue
|
||||
elif mime_type not in MIME_RENDERERS:
|
||||
not_available.append(mime_type)
|
||||
continue
|
||||
break
|
||||
if output is None:
|
||||
if not_available:
|
||||
js.console.warn(
|
||||
f"Rendered object requested unavailable MIME renderers: {not_available}"
|
||||
)
|
||||
output = repr(output)
|
||||
mime_type = "text/plain"
|
||||
elif isinstance(output, tuple):
|
||||
output, meta = output
|
||||
else:
|
||||
meta = {}
|
||||
return MIME_RENDERERS[mime_type](output, meta), mime_type
|
||||
|
||||
|
||||
def run_until_complete(f):
|
||||
_ = loop.run_until_complete(f)
|
||||
|
||||
|
||||
def write(element_id, value, append=False, exec_id=0):
|
||||
"""Writes value to the element with id "element_id"""
|
||||
Element(element_id).write(value=value, append=append)
|
||||
js.console.warn(
|
||||
dedent(
|
||||
"""PyScript Deprecation Warning: PyScript.write is
|
||||
marked as deprecated and will be removed sometime soon. Please, use
|
||||
Element(<id>).write instead."""
|
||||
# Deprecated after 2023.03.1
|
||||
showWarning(
|
||||
"<code>pyscript.js</code> is deprecated, please use <code>import js</code> instead.",
|
||||
"html",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _display_target(target_id):
|
||||
get_current_display_target._id = target_id
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
get_current_display_target._id = None
|
||||
|
||||
|
||||
def get_current_display_target():
|
||||
return get_current_display_target._id
|
||||
|
||||
|
||||
get_current_display_target._id = None
|
||||
|
||||
|
||||
def display(*values, target=None, append=True):
|
||||
if target is None:
|
||||
target = get_current_display_target()
|
||||
if target is None:
|
||||
raise Exception(
|
||||
"Implicit target not allowed here. Please use display(..., target=...)"
|
||||
)
|
||||
for v in values:
|
||||
Element(target).write(v, append=append)
|
||||
|
||||
|
||||
class Element:
|
||||
def __init__(self, element_id, element=None):
|
||||
self._id = element_id
|
||||
self._element = element
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def element(self):
|
||||
"""Return the dom element"""
|
||||
if not self._element:
|
||||
self._element = js.document.querySelector(f"#{self._id}")
|
||||
return self._element
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.element.value
|
||||
|
||||
@property
|
||||
def innerHtml(self):
|
||||
return self.element.innerHTML
|
||||
|
||||
def write(self, value, append=False):
|
||||
html, mime_type = format_mime(value)
|
||||
if html == "\n":
|
||||
return
|
||||
|
||||
if append:
|
||||
child = js.document.createElement("div")
|
||||
self.element.appendChild(child)
|
||||
|
||||
if append and self.element.children:
|
||||
out_element = self.element.children[-1]
|
||||
else:
|
||||
out_element = self.element
|
||||
|
||||
if mime_type in ("application/javascript", "text/html"):
|
||||
script_element = js.document.createRange().createContextualFragment(html)
|
||||
out_element.appendChild(script_element)
|
||||
else:
|
||||
out_element.innerHTML = html
|
||||
|
||||
def clear(self):
|
||||
if hasattr(self.element, "value"):
|
||||
self.element.value = ""
|
||||
else:
|
||||
self.write("", append=False)
|
||||
|
||||
def select(self, query, from_content=False):
|
||||
el = self.element
|
||||
|
||||
if from_content:
|
||||
el = el.content
|
||||
|
||||
_el = el.querySelector(query)
|
||||
if _el:
|
||||
return Element(_el.id, _el)
|
||||
else:
|
||||
js.console.warn(f"WARNING: can't find element matching query {query}")
|
||||
|
||||
def clone(self, new_id=None, to=None):
|
||||
if new_id is None:
|
||||
new_id = self.element.id
|
||||
|
||||
clone = self.element.cloneNode(True)
|
||||
clone.id = new_id
|
||||
|
||||
if to:
|
||||
to.element.appendChild(clone)
|
||||
# Inject it into the DOM
|
||||
to.element.after(clone)
|
||||
else:
|
||||
# Inject it into the DOM
|
||||
self.element.after(clone)
|
||||
|
||||
return Element(clone.id, clone)
|
||||
|
||||
def remove_class(self, classname):
|
||||
if isinstance(classname, list):
|
||||
for cl in classname:
|
||||
self.remove_class(cl)
|
||||
else:
|
||||
self.element.classList.remove(classname)
|
||||
|
||||
def add_class(self, classname):
|
||||
if isinstance(classname, list):
|
||||
for cl in classname:
|
||||
self.element.classList.add(cl)
|
||||
else:
|
||||
self.element.classList.add(classname)
|
||||
|
||||
|
||||
def add_classes(element, class_list):
|
||||
for klass in class_list.split(" "):
|
||||
element.classList.add(klass)
|
||||
|
||||
|
||||
def create(what, id_=None, classes=""):
|
||||
element = js.document.createElement(what)
|
||||
if id_:
|
||||
element.id = id_
|
||||
add_classes(element, classes)
|
||||
return Element(id_, element)
|
||||
|
||||
|
||||
class PyWidgetTheme:
|
||||
def __init__(self, main_style_classes):
|
||||
self.main_style_classes = main_style_classes
|
||||
|
||||
def theme_it(self, widget):
|
||||
for klass in self.main_style_classes.split(" "):
|
||||
widget.classList.add(klass)
|
||||
|
||||
|
||||
class PyItemTemplate(Element):
|
||||
label_fields = None
|
||||
|
||||
def __init__(self, data, labels=None, state_key=None, parent=None):
|
||||
self.data = data
|
||||
|
||||
self.register_parent(parent)
|
||||
|
||||
if not labels:
|
||||
labels = list(self.data.keys())
|
||||
self.labels = labels
|
||||
|
||||
self.state_key = state_key
|
||||
|
||||
super().__init__(self._id)
|
||||
|
||||
def register_parent(self, parent):
|
||||
self._parent = parent
|
||||
if parent:
|
||||
self._id = f"{self._parent._id}-c-{len(self._parent._children)}"
|
||||
self.data["id"] = self._id
|
||||
else:
|
||||
self._id = None
|
||||
|
||||
def create(self):
|
||||
new_child = create("div", self._id, "py-li-element")
|
||||
new_child._element.innerHTML = dedent(
|
||||
f"""
|
||||
<label id="{self._id}" for="flex items-center p-2 ">
|
||||
<input class="mr-2" type="checkbox" class="task-check">
|
||||
<p>{self.render_content()}</p>
|
||||
</label>
|
||||
"""
|
||||
)
|
||||
return new_child
|
||||
|
||||
def on_click(self, evt):
|
||||
pass
|
||||
|
||||
def pre_append(self):
|
||||
pass
|
||||
|
||||
def post_append(self):
|
||||
self.element.click = self.on_click
|
||||
self.element.onclick = self.on_click
|
||||
|
||||
self._post_append()
|
||||
|
||||
def _post_append(self):
|
||||
pass
|
||||
|
||||
def strike(self, value, extra=None):
|
||||
if value:
|
||||
self.add_class("line-through")
|
||||
else:
|
||||
self.remove_class("line-through")
|
||||
|
||||
def render_content(self):
|
||||
return " - ".join([self.data[f] for f in self.labels])
|
||||
|
||||
|
||||
class PyListTemplate:
|
||||
theme = PyWidgetTheme("py-li-element")
|
||||
item_class = PyItemTemplate
|
||||
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self._children = []
|
||||
self._id = self.parent.id
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
return self._children
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return [c.data for c in self._children]
|
||||
|
||||
def render_children(self):
|
||||
binds = {}
|
||||
for i, c in enumerate(self._children):
|
||||
txt = c.element.innerHTML
|
||||
rnd = str(time.time()).replace(".", "")[-5:]
|
||||
new_id = f"{c.element.id}-{i}-{rnd}"
|
||||
binds[new_id] = c.element.id
|
||||
txt = txt.replace(">", f" id='{new_id}'>")
|
||||
print(txt)
|
||||
|
||||
def foo(evt):
|
||||
evtEl = evt.srcElement
|
||||
srcEl = Element(binds[evtEl.id])
|
||||
srcEl.element.onclick()
|
||||
evtEl.classList = srcEl.element.classList
|
||||
|
||||
for new_id in binds:
|
||||
Element(new_id).element.onclick = foo
|
||||
|
||||
def connect(self):
|
||||
self.md = main_div = js.document.createElement("div")
|
||||
main_div.id = self._id + "-list-tasks-container"
|
||||
|
||||
if self.theme:
|
||||
self.theme.theme_it(main_div)
|
||||
|
||||
self.parent.appendChild(main_div)
|
||||
|
||||
def add(self, *args, **kws):
|
||||
if not isinstance(args[0], self.item_class):
|
||||
child = self.item_class(*args, **kws)
|
||||
else:
|
||||
child = args[0]
|
||||
child.register_parent(self)
|
||||
return self._add(child)
|
||||
|
||||
def _add(self, child_elem):
|
||||
self.pre_child_append(child_elem)
|
||||
child_elem.pre_append()
|
||||
self._children.append(child_elem)
|
||||
self.md.appendChild(child_elem.create().element)
|
||||
child_elem.post_append()
|
||||
self.child_appended(child_elem)
|
||||
return child_elem
|
||||
|
||||
def pre_child_append(self, child):
|
||||
pass
|
||||
|
||||
def child_appended(self, child):
|
||||
"""Overwrite me to define logic"""
|
||||
pass
|
||||
|
||||
|
||||
class TopLevelAsyncFinder(ast.NodeVisitor):
|
||||
def is_source_top_level_await(self, source):
|
||||
self.async_found = False
|
||||
node = ast.parse(source)
|
||||
self.generic_visit(node)
|
||||
return self.async_found
|
||||
|
||||
def visit_Await(self, node):
|
||||
self.async_found = True
|
||||
|
||||
def visit_AsyncFor(self, node):
|
||||
self.async_found = True
|
||||
|
||||
def visit_AsyncWith(self, node):
|
||||
self.async_found = True
|
||||
|
||||
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
|
||||
pass # Do not visit children of async function defs
|
||||
|
||||
|
||||
def uses_top_level_await(source: str) -> bool:
|
||||
return TopLevelAsyncFinder().is_source_top_level_await(source)
|
||||
|
||||
|
||||
class Plugin:
|
||||
def __init__(self, name=None):
|
||||
if not name:
|
||||
name = self.__class__.__name__
|
||||
|
||||
self.name = name
|
||||
self._custom_elements = []
|
||||
self.app = None
|
||||
|
||||
def init(self, app):
|
||||
self.app = app
|
||||
|
||||
def configure(self, config):
|
||||
pass
|
||||
|
||||
def afterSetup(self, interpreter):
|
||||
pass
|
||||
|
||||
def afterStartup(self, interpreter):
|
||||
pass
|
||||
|
||||
def beforePyScriptExec(self, interpreter, src, pyScriptTag):
|
||||
pass
|
||||
|
||||
def afterPyScriptExec(self, interpreter, src, pyScriptTag, result):
|
||||
pass
|
||||
|
||||
def beforePyReplExec(self, interpreter, src, outEl, pyReplTag):
|
||||
pass
|
||||
|
||||
def afterPyReplExec(self, interpreter, src, outEl, pyReplTag, result):
|
||||
pass
|
||||
|
||||
def onUserError(self, error):
|
||||
pass
|
||||
|
||||
def register_custom_element(self, tag):
|
||||
"""
|
||||
Decorator to register a new custom element as part of a Plugin and associate
|
||||
tag to it. Internally, it delegates the registration to the PyScript internal
|
||||
[JS] plugin manager, who actually creates the JS custom element that can be
|
||||
attached to the page and instantiate an instance of the class passing the custom
|
||||
element to the plugin constructor.
|
||||
|
||||
Exammple:
|
||||
>> plugin = Plugin("PyTutorial")
|
||||
>> @plugin.register_custom_element("py-tutor")
|
||||
>> class PyTutor:
|
||||
>> def __init__(self, element):
|
||||
>> self.element = element
|
||||
"""
|
||||
# TODO: Ideally would be better to use the logger.
|
||||
js.console.info(f"Defining new custom element {tag}")
|
||||
|
||||
def wrapper(class_):
|
||||
# TODO: this is very pyodide specific but will have to do
|
||||
# until we have JS interface that works across interpreters
|
||||
define_custom_element(tag, create_proxy(class_)) # noqa: F821
|
||||
|
||||
self._custom_elements.append(tag)
|
||||
return create_proxy(wrapper)
|
||||
|
||||
|
||||
class DeprecatedGlobal:
|
||||
"""
|
||||
Proxy for globals which are deprecated.
|
||||
|
||||
The intendend usage is as follows:
|
||||
|
||||
# in the global namespace
|
||||
Element = pyscript.DeprecatedGlobal('Element', pyscript.Element, "...")
|
||||
console = pyscript.DeprecatedGlobal('console', js.console, "...")
|
||||
...
|
||||
|
||||
The proxy forwards __getattr__ and __call__ to the underlying object, and
|
||||
emit a warning on the first usage.
|
||||
|
||||
This way users see a warning only if they actually access the top-level
|
||||
name.
|
||||
"""
|
||||
|
||||
def __init__(self, name, obj, message):
|
||||
self.__name = name
|
||||
self.__obj = obj
|
||||
self.__message = message
|
||||
self.__warning_already_shown = False
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DeprecatedGlobal({self.__name!r})>"
|
||||
|
||||
def _show_warning(self, message):
|
||||
"""
|
||||
NOTE: this is overridden by unit tests
|
||||
"""
|
||||
# this showWarning is implemented in js and injected into this
|
||||
# namespace by main.ts
|
||||
showWarning(message, "html") # noqa: F821
|
||||
|
||||
def _show_warning_maybe(self):
|
||||
if self.__warning_already_shown:
|
||||
return
|
||||
self._show_warning(self.__message)
|
||||
self.__warning_already_shown = True
|
||||
|
||||
def __getattr__(self, attr):
|
||||
self._show_warning_maybe()
|
||||
return getattr(self.__obj, attr)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
self._show_warning_maybe()
|
||||
return self.__obj(*args, **kwargs)
|
||||
|
||||
def __iter__(self):
|
||||
self._show_warning_maybe()
|
||||
return iter(self.__obj)
|
||||
|
||||
def __getitem__(self, key):
|
||||
self._show_warning_maybe()
|
||||
return self.__obj[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._show_warning_maybe()
|
||||
self.__obj[key] = value
|
||||
|
||||
|
||||
class PyScript:
|
||||
"""
|
||||
This class is deprecated since 2022.12.1.
|
||||
|
||||
All its old functionalities are available as module-level functions. This
|
||||
class should be killed eventually.
|
||||
"""
|
||||
|
||||
loop = loop
|
||||
|
||||
@staticmethod
|
||||
def run_until_complete(f):
|
||||
run_until_complete(f)
|
||||
|
||||
@staticmethod
|
||||
def write(element_id, value, append=False, exec_id=0):
|
||||
write(element_id, value, append, exec_id)
|
||||
|
||||
|
||||
def _install_deprecated_globals_2022_12_1(ns):
|
||||
"""
|
||||
Install into the given namespace all the globals which have been
|
||||
deprecated since the 2022.12.1 release. Eventually they should be killed.
|
||||
"""
|
||||
|
||||
def deprecate(name, obj, instead):
|
||||
message = f"Direct usage of <code>{name}</code> is deprecated. " + instead
|
||||
ns[name] = DeprecatedGlobal(name, obj, message)
|
||||
|
||||
# function/classes defined in pyscript.py ===> pyscript.XXX
|
||||
pyscript_names = [
|
||||
"PyItemTemplate",
|
||||
"PyListTemplate",
|
||||
"PyWidgetTheme",
|
||||
"add_classes",
|
||||
"create",
|
||||
"loop",
|
||||
]
|
||||
for name in pyscript_names:
|
||||
deprecate(
|
||||
name, globals()[name], f"Please use <code>pyscript.{name}</code> instead."
|
||||
)
|
||||
|
||||
# stdlib modules ===> import XXX
|
||||
stdlib_names = [
|
||||
"asyncio",
|
||||
"base64",
|
||||
"io",
|
||||
"sys",
|
||||
"time",
|
||||
"datetime",
|
||||
"pyodide",
|
||||
"micropip",
|
||||
]
|
||||
for name in stdlib_names:
|
||||
obj = __import__(name)
|
||||
deprecate(name, obj, f"Please use <code>import {name}</code> instead.")
|
||||
|
||||
# special case
|
||||
deprecate(
|
||||
"dedent", dedent, "Please use <code>from textwrap import dedent</code> instead."
|
||||
)
|
||||
|
||||
# these are names that used to leak in the globals but they are just
|
||||
# implementation details. People should not use them.
|
||||
private_names = [
|
||||
"eval_formatter",
|
||||
"format_mime",
|
||||
"identity",
|
||||
"render_image",
|
||||
"MIME_RENDERERS",
|
||||
"MIME_METHODS",
|
||||
]
|
||||
for name in private_names:
|
||||
obj = globals()[name]
|
||||
message = (
|
||||
f"<code>{name}</code> is deprecated. "
|
||||
"This is a private implementation detail of pyscript. "
|
||||
"You should not use it."
|
||||
)
|
||||
ns[name] = DeprecatedGlobal(name, obj, message)
|
||||
|
||||
# these names are available as js.XXX
|
||||
for name in ["document", "console"]:
|
||||
obj = getattr(js, name)
|
||||
deprecate(name, obj, f"Please use <code>js.{name}</code> instead.")
|
||||
|
||||
# PyScript is special, use a different message
|
||||
message = (
|
||||
"The <code>PyScript</code> object is deprecated. "
|
||||
"Please use <code>pyscript</code> instead."
|
||||
)
|
||||
ns["PyScript"] = DeprecatedGlobal("PyScript", PyScript, message)
|
||||
|
||||
|
||||
class _PyscriptWebLoop(WebLoop):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._ready = False
|
||||
self._usercode = False
|
||||
self._deferred_handles = []
|
||||
|
||||
def call_later(
|
||||
self,
|
||||
delay: float,
|
||||
callback: Callable[..., Any],
|
||||
*args: Any,
|
||||
context: contextvars.Context | None = None,
|
||||
) -> asyncio.Handle:
|
||||
"""Based on call_later from Pyodide's webloop
|
||||
|
||||
With some unneeded stuff removed and a mechanism for deferring tasks
|
||||
scheduled from user code.
|
||||
"""
|
||||
if delay < 0:
|
||||
raise ValueError("Can't schedule in the past")
|
||||
h = asyncio.Handle(callback, args, self, context=context)
|
||||
|
||||
def run_handle():
|
||||
if h.cancelled():
|
||||
return
|
||||
h._run()
|
||||
|
||||
if self._ready or not self._usercode:
|
||||
setTimeout(create_once_callable(run_handle), delay * 1000)
|
||||
else:
|
||||
self._deferred_handles.append((run_handle, self.time() + delay))
|
||||
return h
|
||||
|
||||
def _schedule_deferred_tasks(self):
|
||||
asyncio._set_running_loop(self)
|
||||
t = self.time()
|
||||
for [run_handle, delay] in self._deferred_handles:
|
||||
delay = delay - t
|
||||
if delay < 0:
|
||||
delay = 0
|
||||
setTimeout(create_once_callable(run_handle), delay * 1000)
|
||||
self._ready = True
|
||||
self._deferred_handles = []
|
||||
|
||||
|
||||
def _install_pyscript_loop():
|
||||
global _LOOP
|
||||
_LOOP = _PyscriptWebLoop()
|
||||
asyncio.set_event_loop(_LOOP)
|
||||
|
||||
|
||||
def _schedule_deferred_tasks():
|
||||
_LOOP._schedule_deferred_tasks()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _defer_user_asyncio():
|
||||
_LOOP._usercode = True
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_LOOP._usercode = False
|
||||
|
||||
|
||||
def _run_pyscript(code: str, id: str = None) -> JsProxy:
|
||||
"""Execute user code inside context managers.
|
||||
|
||||
Uses the __main__ global namespace.
|
||||
|
||||
The output is wrapped inside a JavaScript object since an object is not
|
||||
thenable. This is so we do not accidentally `await` the result of the python
|
||||
execution, even if it's awaitable (Future, Task, etc.)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
code :
|
||||
The code to run
|
||||
|
||||
id :
|
||||
The id for the default display target (or None if no default display target).
|
||||
|
||||
Returns
|
||||
-------
|
||||
A Js Object of the form {result: the_result}
|
||||
"""
|
||||
import __main__
|
||||
|
||||
with _display_target(id), _defer_user_asyncio():
|
||||
result = eval_code(code, globals=__main__.__dict__)
|
||||
|
||||
return js.Object.new(result=result)
|
||||
return js
|
||||
raise AttributeError(f"module 'pyscript' has no attribute '{attr}'")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"HTML",
|
||||
"write",
|
||||
"display",
|
||||
"Element",
|
||||
"add_classes",
|
||||
"create",
|
||||
"PyWidgetTheme",
|
||||
"PyItemTemplate",
|
||||
"PyListTemplate",
|
||||
"run_until_complete",
|
||||
"loop",
|
||||
"Plugin",
|
||||
"__version__",
|
||||
"version_info",
|
||||
"showWarning",
|
||||
]
|
||||
|
||||
61
pyscriptjs/src/python/pyscript/_deprecated_globals.py
Normal file
61
pyscriptjs/src/python/pyscript/_deprecated_globals.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from _pyscript_js import showWarning
|
||||
|
||||
|
||||
class DeprecatedGlobal:
|
||||
"""
|
||||
Proxy for globals which are deprecated.
|
||||
|
||||
The intendend usage is as follows:
|
||||
|
||||
# in the global namespace
|
||||
Element = pyscript.DeprecatedGlobal('Element', pyscript.Element, "...")
|
||||
console = pyscript.DeprecatedGlobal('console', js.console, "...")
|
||||
...
|
||||
|
||||
The proxy forwards __getattr__ and __call__ to the underlying object, and
|
||||
emit a warning on the first usage.
|
||||
|
||||
This way users see a warning only if they actually access the top-level
|
||||
name.
|
||||
"""
|
||||
|
||||
def __init__(self, name, obj, message):
|
||||
self.__name = name
|
||||
self.__obj = obj
|
||||
self.__message = message
|
||||
self.__warning_already_shown = False
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DeprecatedGlobal({self.__name!r})>"
|
||||
|
||||
def _show_warning(self, message):
|
||||
"""
|
||||
NOTE: this is overridden by unit tests
|
||||
"""
|
||||
showWarning(message, "html") # noqa: F821
|
||||
|
||||
def _show_warning_maybe(self):
|
||||
if self.__warning_already_shown:
|
||||
return
|
||||
self._show_warning(self.__message)
|
||||
self.__warning_already_shown = True
|
||||
|
||||
def __getattr__(self, attr):
|
||||
self._show_warning_maybe()
|
||||
return getattr(self.__obj, attr)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
self._show_warning_maybe()
|
||||
return self.__obj(*args, **kwargs)
|
||||
|
||||
def __iter__(self):
|
||||
self._show_warning_maybe()
|
||||
return iter(self.__obj)
|
||||
|
||||
def __getitem__(self, key):
|
||||
self._show_warning_maybe()
|
||||
return self.__obj[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._show_warning_maybe()
|
||||
self.__obj[key] = value
|
||||
81
pyscriptjs/src/python/pyscript/_event_loop.py
Normal file
81
pyscriptjs/src/python/pyscript/_event_loop.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import asyncio
|
||||
import contextvars
|
||||
from collections.abc import Callable
|
||||
from contextlib import contextmanager
|
||||
from typing import Any
|
||||
|
||||
from js import setTimeout
|
||||
from pyodide.ffi import create_once_callable
|
||||
from pyodide.webloop import WebLoop
|
||||
|
||||
|
||||
class PyscriptWebLoop(WebLoop):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._ready = False
|
||||
self._usercode = False
|
||||
self._deferred_handles = []
|
||||
|
||||
def call_later(
|
||||
self,
|
||||
delay: float,
|
||||
callback: Callable[..., Any],
|
||||
*args: Any,
|
||||
context: contextvars.Context | None = None,
|
||||
) -> asyncio.Handle:
|
||||
"""Based on call_later from Pyodide's webloop
|
||||
|
||||
With some unneeded stuff removed and a mechanism for deferring tasks
|
||||
scheduled from user code.
|
||||
"""
|
||||
if delay < 0:
|
||||
raise ValueError("Can't schedule in the past")
|
||||
h = asyncio.Handle(callback, args, self, context=context)
|
||||
|
||||
def run_handle():
|
||||
if h.cancelled():
|
||||
return
|
||||
h._run()
|
||||
|
||||
if self._ready or not self._usercode:
|
||||
setTimeout(create_once_callable(run_handle), delay * 1000)
|
||||
else:
|
||||
self._deferred_handles.append((run_handle, self.time() + delay))
|
||||
return h
|
||||
|
||||
def _schedule_deferred_tasks(self):
|
||||
asyncio._set_running_loop(self)
|
||||
t = self.time()
|
||||
for [run_handle, delay] in self._deferred_handles:
|
||||
delay = delay - t
|
||||
if delay < 0:
|
||||
delay = 0
|
||||
setTimeout(create_once_callable(run_handle), delay * 1000)
|
||||
self._ready = True
|
||||
self._deferred_handles = []
|
||||
|
||||
|
||||
LOOP = None
|
||||
|
||||
|
||||
def install_pyscript_loop():
|
||||
global LOOP
|
||||
LOOP = PyscriptWebLoop()
|
||||
asyncio.set_event_loop(LOOP)
|
||||
|
||||
|
||||
def schedule_deferred_tasks():
|
||||
LOOP._schedule_deferred_tasks()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def defer_user_asyncio():
|
||||
LOOP._usercode = True
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
LOOP._usercode = False
|
||||
|
||||
|
||||
def run_until_complete(f):
|
||||
return LOOP.run_until_complete(f)
|
||||
289
pyscriptjs/src/python/pyscript/_html.py
Normal file
289
pyscriptjs/src/python/pyscript/_html.py
Normal file
@@ -0,0 +1,289 @@
|
||||
import time
|
||||
from textwrap import dedent
|
||||
|
||||
from js import console, document
|
||||
|
||||
from . import _internal
|
||||
from ._mime import format_mime as _format_mime
|
||||
|
||||
|
||||
class HTML:
|
||||
"""
|
||||
Wrap a string so that display() can render it as plain HTML
|
||||
"""
|
||||
|
||||
def __init__(self, html):
|
||||
self._html = html
|
||||
|
||||
def _repr_html_(self):
|
||||
return self._html
|
||||
|
||||
|
||||
def write(element_id, value, append=False, exec_id=0):
|
||||
"""Writes value to the element with id "element_id"""
|
||||
Element(element_id).write(value=value, append=append)
|
||||
console.warn(
|
||||
dedent(
|
||||
"""PyScript Deprecation Warning: PyScript.write is
|
||||
marked as deprecated and will be removed sometime soon. Please, use
|
||||
Element(<id>).write instead."""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def display(*values, target=None, append=True):
|
||||
if target is None:
|
||||
target = _internal.DISPLAY_TARGET
|
||||
if target is None:
|
||||
raise Exception(
|
||||
"Implicit target not allowed here. Please use display(..., target=...)"
|
||||
)
|
||||
for v in values:
|
||||
Element(target).write(v, append=append)
|
||||
|
||||
|
||||
class Element:
|
||||
def __init__(self, element_id, element=None):
|
||||
self._id = element_id
|
||||
self._element = element
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def element(self):
|
||||
"""Return the dom element"""
|
||||
if not self._element:
|
||||
self._element = document.querySelector(f"#{self._id}")
|
||||
return self._element
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.element.value
|
||||
|
||||
@property
|
||||
def innerHtml(self):
|
||||
return self.element.innerHTML
|
||||
|
||||
def write(self, value, append=False):
|
||||
html, mime_type = _format_mime(value)
|
||||
if html == "\n":
|
||||
return
|
||||
|
||||
if append:
|
||||
child = document.createElement("div")
|
||||
self.element.appendChild(child)
|
||||
|
||||
if append and self.element.children:
|
||||
out_element = self.element.children[-1]
|
||||
else:
|
||||
out_element = self.element
|
||||
|
||||
if mime_type in ("application/javascript", "text/html"):
|
||||
script_element = document.createRange().createContextualFragment(html)
|
||||
out_element.appendChild(script_element)
|
||||
else:
|
||||
out_element.innerHTML = html
|
||||
|
||||
def clear(self):
|
||||
if hasattr(self.element, "value"):
|
||||
self.element.value = ""
|
||||
else:
|
||||
self.write("", append=False)
|
||||
|
||||
def select(self, query, from_content=False):
|
||||
el = self.element
|
||||
|
||||
if from_content:
|
||||
el = el.content
|
||||
|
||||
_el = el.querySelector(query)
|
||||
if _el:
|
||||
return Element(_el.id, _el)
|
||||
else:
|
||||
console.warn(f"WARNING: can't find element matching query {query}")
|
||||
|
||||
def clone(self, new_id=None, to=None):
|
||||
if new_id is None:
|
||||
new_id = self.element.id
|
||||
|
||||
clone = self.element.cloneNode(True)
|
||||
clone.id = new_id
|
||||
|
||||
if to:
|
||||
to.element.appendChild(clone)
|
||||
# Inject it into the DOM
|
||||
to.element.after(clone)
|
||||
else:
|
||||
# Inject it into the DOM
|
||||
self.element.after(clone)
|
||||
|
||||
return Element(clone.id, clone)
|
||||
|
||||
def remove_class(self, classname):
|
||||
if isinstance(classname, list):
|
||||
for cl in classname:
|
||||
self.remove_class(cl)
|
||||
else:
|
||||
self.element.classList.remove(classname)
|
||||
|
||||
def add_class(self, classname):
|
||||
if isinstance(classname, list):
|
||||
for cl in classname:
|
||||
self.element.classList.add(cl)
|
||||
else:
|
||||
self.element.classList.add(classname)
|
||||
|
||||
|
||||
def add_classes(element, class_list):
|
||||
for klass in class_list.split(" "):
|
||||
element.classList.add(klass)
|
||||
|
||||
|
||||
def create(what, id_=None, classes=""):
|
||||
element = document.createElement(what)
|
||||
if id_:
|
||||
element.id = id_
|
||||
add_classes(element, classes)
|
||||
return Element(id_, element)
|
||||
|
||||
|
||||
class PyWidgetTheme:
|
||||
def __init__(self, main_style_classes):
|
||||
self.main_style_classes = main_style_classes
|
||||
|
||||
def theme_it(self, widget):
|
||||
for klass in self.main_style_classes.split(" "):
|
||||
widget.classList.add(klass)
|
||||
|
||||
|
||||
class PyItemTemplate(Element):
|
||||
label_fields = None
|
||||
|
||||
def __init__(self, data, labels=None, state_key=None, parent=None):
|
||||
self.data = data
|
||||
|
||||
self.register_parent(parent)
|
||||
|
||||
if not labels:
|
||||
labels = list(self.data.keys())
|
||||
self.labels = labels
|
||||
|
||||
self.state_key = state_key
|
||||
|
||||
super().__init__(self._id)
|
||||
|
||||
def register_parent(self, parent):
|
||||
self._parent = parent
|
||||
if parent:
|
||||
self._id = f"{self._parent._id}-c-{len(self._parent._children)}"
|
||||
self.data["id"] = self._id
|
||||
else:
|
||||
self._id = None
|
||||
|
||||
def create(self):
|
||||
new_child = create("div", self._id, "py-li-element")
|
||||
new_child._element.innerHTML = dedent(
|
||||
f"""
|
||||
<label id="{self._id}" for="flex items-center p-2 ">
|
||||
<input class="mr-2" type="checkbox" class="task-check">
|
||||
<p>{self.render_content()}</p>
|
||||
</label>
|
||||
"""
|
||||
)
|
||||
return new_child
|
||||
|
||||
def on_click(self, evt):
|
||||
pass
|
||||
|
||||
def pre_append(self):
|
||||
pass
|
||||
|
||||
def post_append(self):
|
||||
self.element.click = self.on_click
|
||||
self.element.onclick = self.on_click
|
||||
|
||||
self._post_append()
|
||||
|
||||
def _post_append(self):
|
||||
pass
|
||||
|
||||
def strike(self, value, extra=None):
|
||||
if value:
|
||||
self.add_class("line-through")
|
||||
else:
|
||||
self.remove_class("line-through")
|
||||
|
||||
def render_content(self):
|
||||
return " - ".join([self.data[f] for f in self.labels])
|
||||
|
||||
|
||||
class PyListTemplate:
|
||||
theme = PyWidgetTheme("py-li-element")
|
||||
item_class = PyItemTemplate
|
||||
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self._children = []
|
||||
self._id = self.parent.id
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
return self._children
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return [c.data for c in self._children]
|
||||
|
||||
def render_children(self):
|
||||
binds = {}
|
||||
for i, c in enumerate(self._children):
|
||||
txt = c.element.innerHTML
|
||||
rnd = str(time.time()).replace(".", "")[-5:]
|
||||
new_id = f"{c.element.id}-{i}-{rnd}"
|
||||
binds[new_id] = c.element.id
|
||||
txt = txt.replace(">", f" id='{new_id}'>")
|
||||
print(txt)
|
||||
|
||||
def foo(evt):
|
||||
evtEl = evt.srcElement
|
||||
srcEl = Element(binds[evtEl.id])
|
||||
srcEl.element.onclick()
|
||||
evtEl.classList = srcEl.element.classList
|
||||
|
||||
for new_id in binds:
|
||||
Element(new_id).element.onclick = foo
|
||||
|
||||
def connect(self):
|
||||
self.md = main_div = document.createElement("div")
|
||||
main_div.id = self._id + "-list-tasks-container"
|
||||
|
||||
if self.theme:
|
||||
self.theme.theme_it(main_div)
|
||||
|
||||
self.parent.appendChild(main_div)
|
||||
|
||||
def add(self, *args, **kws):
|
||||
if not isinstance(args[0], self.item_class):
|
||||
child = self.item_class(*args, **kws)
|
||||
else:
|
||||
child = args[0]
|
||||
child.register_parent(self)
|
||||
return self._add(child)
|
||||
|
||||
def _add(self, child_elem):
|
||||
self.pre_child_append(child_elem)
|
||||
child_elem.pre_append()
|
||||
self._children.append(child_elem)
|
||||
self.md.appendChild(child_elem.create().element)
|
||||
child_elem.post_append()
|
||||
self.child_appended(child_elem)
|
||||
return child_elem
|
||||
|
||||
def pre_child_append(self, child):
|
||||
pass
|
||||
|
||||
def child_appended(self, child):
|
||||
"""Overwrite me to define logic"""
|
||||
pass
|
||||
115
pyscriptjs/src/python/pyscript/_internal.py
Normal file
115
pyscriptjs/src/python/pyscript/_internal.py
Normal file
@@ -0,0 +1,115 @@
|
||||
import ast
|
||||
from collections import namedtuple
|
||||
from contextlib import contextmanager
|
||||
|
||||
from js import Object
|
||||
from pyodide.code import eval_code
|
||||
from pyodide.ffi import JsProxy
|
||||
|
||||
from ._event_loop import (
|
||||
defer_user_asyncio,
|
||||
install_pyscript_loop,
|
||||
schedule_deferred_tasks,
|
||||
)
|
||||
|
||||
VersionInfo = namedtuple("version_info", ("year", "month", "minor", "releaselevel"))
|
||||
|
||||
|
||||
def set_version_info(version_from_interpreter: str):
|
||||
from . import __dict__ as pyscript_dict
|
||||
|
||||
"""Sets the __version__ and version_info properties from provided JSON data
|
||||
Args:
|
||||
version_from_interpreter (str): A "dotted" representation of the version:
|
||||
YYYY.MM.m(m).releaselevel
|
||||
Year, Month, and Minor should be integers; releaselevel can be any string
|
||||
"""
|
||||
|
||||
version_parts = version_from_interpreter.split(".")
|
||||
year = int(version_parts[0])
|
||||
month = int(version_parts[1])
|
||||
minor = int(version_parts[2])
|
||||
if len(version_parts) > 3:
|
||||
releaselevel = version_parts[3]
|
||||
else:
|
||||
releaselevel = ""
|
||||
|
||||
version_info = VersionInfo(year, month, minor, releaselevel)
|
||||
|
||||
pyscript_dict["__version__"] = version_from_interpreter
|
||||
pyscript_dict["version_info"] = version_info
|
||||
|
||||
|
||||
class TopLevelAwaitFinder(ast.NodeVisitor):
|
||||
def is_source_top_level_await(self, source):
|
||||
self.async_found = False
|
||||
node = ast.parse(source)
|
||||
self.generic_visit(node)
|
||||
return self.async_found
|
||||
|
||||
def visit_Await(self, node):
|
||||
self.async_found = True
|
||||
|
||||
def visit_AsyncFor(self, node):
|
||||
self.async_found = True
|
||||
|
||||
def visit_AsyncWith(self, node):
|
||||
self.async_found = True
|
||||
|
||||
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
|
||||
pass # Do not visit children of async function defs
|
||||
|
||||
|
||||
def uses_top_level_await(source: str) -> bool:
|
||||
return TopLevelAwaitFinder().is_source_top_level_await(source)
|
||||
|
||||
|
||||
DISPLAY_TARGET = None
|
||||
|
||||
|
||||
@contextmanager
|
||||
def display_target(target_id):
|
||||
global DISPLAY_TARGET
|
||||
DISPLAY_TARGET = target_id
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
DISPLAY_TARGET = None
|
||||
|
||||
|
||||
def run_pyscript(code: str, id: str = None) -> JsProxy:
|
||||
"""Execute user code inside context managers.
|
||||
|
||||
Uses the __main__ global namespace.
|
||||
|
||||
The output is wrapped inside a JavaScript object since an object is not
|
||||
thenable. This is so we do not accidentally `await` the result of the python
|
||||
execution, even if it's awaitable (Future, Task, etc.)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
code :
|
||||
The code to run
|
||||
|
||||
id :
|
||||
The id for the default display target (or None if no default display target).
|
||||
|
||||
Returns
|
||||
-------
|
||||
A Js Object of the form {result: the_result}
|
||||
"""
|
||||
import __main__
|
||||
|
||||
with display_target(id), defer_user_asyncio():
|
||||
result = eval_code(code, globals=__main__.__dict__)
|
||||
|
||||
return Object.new(result=result)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"set_version_info",
|
||||
"uses_top_level_await",
|
||||
"run_pyscript",
|
||||
"install_pyscript_loop",
|
||||
"schedule_deferred_tasks",
|
||||
]
|
||||
113
pyscriptjs/src/python/pyscript/_mime.py
Normal file
113
pyscriptjs/src/python/pyscript/_mime.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import base64
|
||||
import html
|
||||
import io
|
||||
import re
|
||||
|
||||
from js import console
|
||||
|
||||
MIME_METHODS = {
|
||||
"__repr__": "text/plain",
|
||||
"_repr_html_": "text/html",
|
||||
"_repr_markdown_": "text/markdown",
|
||||
"_repr_svg_": "image/svg+xml",
|
||||
"_repr_png_": "image/png",
|
||||
"_repr_pdf_": "application/pdf",
|
||||
"_repr_jpeg_": "image/jpeg",
|
||||
"_repr_latex": "text/latex",
|
||||
"_repr_json_": "application/json",
|
||||
"_repr_javascript_": "application/javascript",
|
||||
"savefig": "image/png",
|
||||
}
|
||||
|
||||
|
||||
def render_image(mime, value, meta):
|
||||
# If the image value is using bytes we should convert it to base64
|
||||
# otherwise it will return raw bytes and the browser will not be able to
|
||||
# render it.
|
||||
if isinstance(value, bytes):
|
||||
value = base64.b64encode(value).decode("utf-8")
|
||||
|
||||
# This is the pattern of base64 strings
|
||||
base64_pattern = re.compile(
|
||||
r"^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$"
|
||||
)
|
||||
# If value doesn't match the base64 pattern we should encode it to base64
|
||||
if len(value) > 0 and not base64_pattern.match(value):
|
||||
value = base64.b64encode(value.encode("utf-8")).decode("utf-8")
|
||||
|
||||
data = f"data:{mime};charset=utf-8;base64,{value}"
|
||||
attrs = " ".join(['{k}="{v}"' for k, v in meta.items()])
|
||||
return f'<img src="{data}" {attrs}></img>'
|
||||
|
||||
|
||||
def identity(value, meta):
|
||||
return value
|
||||
|
||||
|
||||
MIME_RENDERERS = {
|
||||
"text/plain": html.escape,
|
||||
"text/html": identity,
|
||||
"image/png": lambda value, meta: render_image("image/png", value, meta),
|
||||
"image/jpeg": lambda value, meta: render_image("image/jpeg", value, meta),
|
||||
"image/svg+xml": identity,
|
||||
"application/json": identity,
|
||||
"application/javascript": lambda value, meta: f"<script>{value}</script>",
|
||||
}
|
||||
|
||||
|
||||
def eval_formatter(obj, print_method):
|
||||
"""
|
||||
Evaluates a formatter method.
|
||||
"""
|
||||
if print_method == "__repr__":
|
||||
return repr(obj)
|
||||
elif hasattr(obj, print_method):
|
||||
if print_method == "savefig":
|
||||
buf = io.BytesIO()
|
||||
obj.savefig(buf, format="png")
|
||||
buf.seek(0)
|
||||
return base64.b64encode(buf.read()).decode("utf-8")
|
||||
return getattr(obj, print_method)()
|
||||
elif print_method == "_repr_mimebundle_":
|
||||
return {}, {}
|
||||
return None
|
||||
|
||||
|
||||
def format_mime(obj):
|
||||
"""
|
||||
Formats object using _repr_x_ methods.
|
||||
"""
|
||||
if isinstance(obj, str):
|
||||
return html.escape(obj), "text/plain"
|
||||
|
||||
mimebundle = eval_formatter(obj, "_repr_mimebundle_")
|
||||
if isinstance(mimebundle, tuple):
|
||||
format_dict, _ = mimebundle
|
||||
else:
|
||||
format_dict = mimebundle
|
||||
|
||||
output, not_available = None, []
|
||||
for method, mime_type in reversed(MIME_METHODS.items()):
|
||||
if mime_type in format_dict:
|
||||
output = format_dict[mime_type]
|
||||
else:
|
||||
output = eval_formatter(obj, method)
|
||||
|
||||
if output is None:
|
||||
continue
|
||||
elif mime_type not in MIME_RENDERERS:
|
||||
not_available.append(mime_type)
|
||||
continue
|
||||
break
|
||||
if output is None:
|
||||
if not_available:
|
||||
console.warn(
|
||||
f"Rendered object requested unavailable MIME renderers: {not_available}"
|
||||
)
|
||||
output = repr(output)
|
||||
mime_type = "text/plain"
|
||||
elif isinstance(output, tuple):
|
||||
output, meta = output
|
||||
else:
|
||||
meta = {}
|
||||
return MIME_RENDERERS[mime_type](output, meta), mime_type
|
||||
66
pyscriptjs/src/python/pyscript/_plugin.py
Normal file
66
pyscriptjs/src/python/pyscript/_plugin.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from _pyscript_js import define_custom_element
|
||||
from js import console
|
||||
from pyodide.ffi import create_proxy
|
||||
|
||||
|
||||
class Plugin:
|
||||
def __init__(self, name=None):
|
||||
if not name:
|
||||
name = self.__class__.__name__
|
||||
|
||||
self.name = name
|
||||
self._custom_elements = []
|
||||
self.app = None
|
||||
|
||||
def init(self, app):
|
||||
self.app = app
|
||||
|
||||
def configure(self, config):
|
||||
pass
|
||||
|
||||
def afterSetup(self, interpreter):
|
||||
pass
|
||||
|
||||
def afterStartup(self, interpreter):
|
||||
pass
|
||||
|
||||
def beforePyScriptExec(self, interpreter, src, pyScriptTag):
|
||||
pass
|
||||
|
||||
def afterPyScriptExec(self, interpreter, src, pyScriptTag, result):
|
||||
pass
|
||||
|
||||
def beforePyReplExec(self, interpreter, src, outEl, pyReplTag):
|
||||
pass
|
||||
|
||||
def afterPyReplExec(self, interpreter, src, outEl, pyReplTag, result):
|
||||
pass
|
||||
|
||||
def onUserError(self, error):
|
||||
pass
|
||||
|
||||
def register_custom_element(self, tag):
|
||||
"""
|
||||
Decorator to register a new custom element as part of a Plugin and associate
|
||||
tag to it. Internally, it delegates the registration to the PyScript internal
|
||||
[JS] plugin manager, who actually creates the JS custom element that can be
|
||||
attached to the page and instantiate an instance of the class passing the custom
|
||||
element to the plugin constructor.
|
||||
|
||||
Exammple:
|
||||
>> plugin = Plugin("PyTutorial")
|
||||
>> @plugin.register_custom_element("py-tutor")
|
||||
>> class PyTutor:
|
||||
>> def __init__(self, element):
|
||||
>> self.element = element
|
||||
"""
|
||||
# TODO: Ideally would be better to use the logger.
|
||||
console.info(f"Defining new custom element {tag}")
|
||||
|
||||
def wrapper(class_):
|
||||
# TODO: this is very pyodide specific but will have to do
|
||||
# until we have JS interface that works across interpreters
|
||||
define_custom_element(tag, create_proxy(class_)) # noqa: F821
|
||||
|
||||
self._custom_elements.append(tag)
|
||||
return create_proxy(wrapper)
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AppConfig } from './pyconfig';
|
||||
import { version } from './version';
|
||||
import { getLogger } from './logger';
|
||||
import { Stdio } from './stdio';
|
||||
import { InstallError, ErrorCode } from './exceptions';
|
||||
@@ -6,6 +7,8 @@ import { robustFetch } from './fetch';
|
||||
import type { loadPyodide as loadPyodideDeclaration, PyodideInterface, PyProxy, PyProxyDict } from 'pyodide';
|
||||
import type { ProxyMarked } from 'synclink';
|
||||
import * as Synclink from 'synclink';
|
||||
import { showWarning } from './utils';
|
||||
import { define_custom_element } from './plugin';
|
||||
|
||||
import { python_package } from './python_package';
|
||||
|
||||
@@ -32,12 +35,13 @@ type PATHInterface = {
|
||||
dirname(path: string): string;
|
||||
} & ProxyMarked;
|
||||
|
||||
type PyScriptPyModule = ProxyMarked & {
|
||||
_set_version_info(ver: string): void;
|
||||
type PyScriptInternalModule = ProxyMarked & {
|
||||
set_version_info(ver: string): void;
|
||||
uses_top_level_await(code: string): boolean;
|
||||
_run_pyscript(code: string, display_target_id?: string): { result: any };
|
||||
_install_pyscript_loop(): void;
|
||||
_schedule_deferred_tasks(): void;
|
||||
run_pyscript(code: string, display_target_id?: string): { result: any };
|
||||
install_pyscript_loop(): void;
|
||||
start_loop(): void;
|
||||
schedule_deferred_tasks(): void;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -61,7 +65,7 @@ export class RemoteInterpreter extends Object {
|
||||
FS: FSInterface;
|
||||
PATH: PATHInterface;
|
||||
PATH_FS: PATHFSInterface;
|
||||
pyscript_py: PyScriptPyModule;
|
||||
pyscript_internal: PyScriptInternalModule;
|
||||
|
||||
globals: PyProxyDict & ProxyMarked;
|
||||
// TODO: Remove this once `runtimes` is removed!
|
||||
@@ -95,6 +99,9 @@ export class RemoteInterpreter extends Object {
|
||||
* path.
|
||||
*/
|
||||
async loadInterpreter(config: AppConfig, stdio: Synclink.Remote<Stdio & ProxyMarked>): Promise<void> {
|
||||
// TODO: move this to "main thread"!
|
||||
const _pyscript_js_main = { define_custom_element, showWarning };
|
||||
|
||||
this.interface = Synclink.proxy(
|
||||
await loadPyodide({
|
||||
stdout: (msg: string) => {
|
||||
@@ -115,6 +122,7 @@ export class RemoteInterpreter extends Object {
|
||||
|
||||
// TODO: Remove this once `runtimes` is removed!
|
||||
this.interpreter = this.interface;
|
||||
this.interface.registerJsModule('_pyscript_js', _pyscript_js_main);
|
||||
|
||||
// Write pyscript package into file system
|
||||
for (const dir of python_package.dirs) {
|
||||
@@ -128,13 +136,22 @@ export class RemoteInterpreter extends Object {
|
||||
|
||||
this.globals = Synclink.proxy(this.interface.globals as PyProxyDict);
|
||||
logger.info('importing pyscript');
|
||||
this.pyscript_py = Synclink.proxy(this.interface.pyimport('pyscript')) as PyProxy & typeof this.pyscript_py;
|
||||
this.pyscript_py._install_pyscript_loop();
|
||||
this.pyscript_internal = Synclink.proxy(this.interface.pyimport('pyscript._internal')) as PyProxy &
|
||||
typeof this.pyscript_internal;
|
||||
this.pyscript_internal.set_version_info(version);
|
||||
this.pyscript_internal.install_pyscript_loop();
|
||||
|
||||
if (config.packages) {
|
||||
logger.info('Found packages in configuration to install. Loading micropip...');
|
||||
await this.loadPackage('micropip');
|
||||
}
|
||||
// import some carefully selected names into the global namespace
|
||||
this.interface.runPython(`
|
||||
import js
|
||||
import pyscript
|
||||
from pyscript import Element, display, HTML
|
||||
`);
|
||||
|
||||
logger.info('pyodide loaded and initialized');
|
||||
}
|
||||
|
||||
|
||||
@@ -247,44 +247,15 @@ class TestBasic(PyScriptTest):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
import pyscript
|
||||
pyscript.showWarning("hello")
|
||||
pyscript.showWarning("world")
|
||||
from _pyscript_js import showWarning
|
||||
showWarning("hello")
|
||||
showWarning("world")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
with pytest.raises(AssertionError, match="Found 2 alert banners"):
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_deprecated_globals(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
# trigger various warnings
|
||||
create("div", classes="a b c")
|
||||
assert sys.__name__ == 'sys'
|
||||
dedent("")
|
||||
format_mime("")
|
||||
assert MIME_RENDERERS['text/html'] is not None
|
||||
console.log("hello")
|
||||
PyScript.loop
|
||||
</py-script>
|
||||
|
||||
<div id="mydiv"></div>
|
||||
"""
|
||||
)
|
||||
banner = self.page.locator(".py-warning")
|
||||
messages = banner.all_inner_texts()
|
||||
assert messages == [
|
||||
"The PyScript object is deprecated. Please use pyscript instead.",
|
||||
"Direct usage of console is deprecated. Please use js.console instead.",
|
||||
"MIME_RENDERERS is deprecated. This is a private implementation detail of pyscript. You should not use it.", # noqa: E501
|
||||
"format_mime is deprecated. This is a private implementation detail of pyscript. You should not use it.", # noqa: E501
|
||||
"Direct usage of dedent is deprecated. Please use from textwrap import dedent instead.",
|
||||
"Direct usage of sys is deprecated. Please use import sys instead.",
|
||||
"Direct usage of create is deprecated. Please use pyscript.create instead.",
|
||||
]
|
||||
|
||||
def test_getPySrc_returns_source_code(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
|
||||
@@ -330,8 +330,8 @@ class TestOutput(PyScriptTest):
|
||||
"""
|
||||
<py-script>
|
||||
print('print from python')
|
||||
console.log('print from js')
|
||||
console.error('error from js');
|
||||
js.console.log('print from js')
|
||||
js.console.error('error from js');
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
@@ -348,8 +348,8 @@ class TestOutput(PyScriptTest):
|
||||
<py-script>
|
||||
display('this goes to the DOM')
|
||||
print('print from python')
|
||||
console.log('print from js')
|
||||
console.error('error from js');
|
||||
js.console.log('print from js')
|
||||
js.console.error('error from js');
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import os
|
||||
import tarfile
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
URL = "https://github.com/pyodide/pyodide/releases/download/0.20.0/pyodide-build-0.20.0.tar.bz2"
|
||||
TAR_NAME = "pyodide-build-0.20.0.tar.bz2"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tar_location(request):
|
||||
val = request.config.cache.get("pyodide-0.20-tar", None)
|
||||
def pyodide_0_22_0_tar(request):
|
||||
"""
|
||||
Fixture which returns a local copy of pyodide. It uses pytest-cache to
|
||||
avoid re-downloading it between runs.
|
||||
"""
|
||||
URL = "https://github.com/pyodide/pyodide/releases/download/0.22.0/pyodide-core-0.22.0.tar.bz2"
|
||||
tar_name = Path(URL).name
|
||||
|
||||
val = request.config.cache.get(tar_name, None)
|
||||
if val is None:
|
||||
response = requests.get(URL, stream=True)
|
||||
TMP_DIR = tempfile.mkdtemp()
|
||||
TMP_TAR_LOCATION = os.path.join(TMP_DIR, TAR_NAME)
|
||||
TMP_TAR_LOCATION = os.path.join(TMP_DIR, tar_name)
|
||||
with open(TMP_TAR_LOCATION, "wb") as f:
|
||||
f.write(response.raw.read())
|
||||
val = TMP_TAR_LOCATION
|
||||
request.config.cache.set("pyodide-0.20-tar", val)
|
||||
request.config.cache.set(tar_name, val)
|
||||
return val
|
||||
|
||||
|
||||
@@ -68,24 +73,20 @@ class TestConfig(PyScriptTest):
|
||||
|
||||
# The default pyodide version is 0.22.1 as of writing
|
||||
# this test which is newer than the one we are loading below
|
||||
# (after downloading locally) -- which is 0.20.0
|
||||
# (after downloading locally) -- which is 0.22.0
|
||||
|
||||
# The test checks if loading a different interpreter is possible
|
||||
# and that too from a locally downloaded file without needing
|
||||
# the use of explicit `indexURL` calculation.
|
||||
def test_interpreter_config(self, tar_location):
|
||||
unzip(
|
||||
location=tar_location,
|
||||
extract_to=self.tmpdir,
|
||||
)
|
||||
|
||||
def test_interpreter_config(self, pyodide_0_22_0_tar):
|
||||
unzip(pyodide_0_22_0_tar, extract_to=self.tmpdir)
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config type="json">
|
||||
{
|
||||
"interpreters": [{
|
||||
"src": "/pyodide/pyodide.js",
|
||||
"name": "pyodide-0.20.0",
|
||||
"name": "my-own-pyodide",
|
||||
"lang": "python"
|
||||
}]
|
||||
}
|
||||
@@ -100,23 +101,21 @@ class TestConfig(PyScriptTest):
|
||||
""",
|
||||
)
|
||||
|
||||
assert self.console.log.lines[-1] == "version 0.20.0"
|
||||
assert self.console.log.lines[-1] == "version 0.22.0"
|
||||
version = self.page.locator("py-script").inner_text()
|
||||
assert version == "0.20.0"
|
||||
|
||||
def test_runtime_still_works_but_shows_deprecation_warning(self, tar_location):
|
||||
unzip(
|
||||
location=tar_location,
|
||||
extract_to=self.tmpdir,
|
||||
)
|
||||
assert version == "0.22.0"
|
||||
|
||||
def test_runtime_still_works_but_shows_deprecation_warning(
|
||||
self, pyodide_0_22_0_tar
|
||||
):
|
||||
unzip(pyodide_0_22_0_tar, extract_to=self.tmpdir)
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config type="json">
|
||||
{
|
||||
"runtimes": [{
|
||||
"src": "/pyodide/pyodide.js",
|
||||
"name": "pyodide-0.20.0",
|
||||
"name": "my-own-pyodide",
|
||||
"lang": "python"
|
||||
}]
|
||||
}
|
||||
@@ -131,9 +130,9 @@ class TestConfig(PyScriptTest):
|
||||
""",
|
||||
)
|
||||
|
||||
assert self.console.log.lines[-1] == "version 0.20.0"
|
||||
assert self.console.log.lines[-1] == "version 0.22.0"
|
||||
version = self.page.locator("py-script").inner_text()
|
||||
assert version == "0.20.0"
|
||||
assert version == "0.22.0"
|
||||
|
||||
deprecation_banner = self.page.wait_for_selector(".alert-banner")
|
||||
expected_message = (
|
||||
|
||||
4
pyscriptjs/tests/py-unit/_pyscript_js.py
Normal file
4
pyscriptjs/tests/py-unit/_pyscript_js.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
showWarning = Mock()
|
||||
define_custom_element = Mock()
|
||||
@@ -16,10 +16,10 @@ sys.path.append(str(python_plugins_source))
|
||||
|
||||
|
||||
# patch pyscript module where needed
|
||||
import pyscript # noqa: E402
|
||||
import pyscript_plugins_tester as ppt # noqa: E402
|
||||
from pyscript import _plugin # noqa: E402
|
||||
|
||||
pyscript.define_custom_element = ppt.define_custom_element
|
||||
_plugin.define_custom_element = ppt.define_custom_element
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
||||
@@ -4,3 +4,4 @@ from unittest.mock import Mock
|
||||
document = Mock()
|
||||
console = Mock()
|
||||
setTimeout = Mock()
|
||||
Object = Mock()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import xml.dom
|
||||
from xml.dom.minidom import Node # nosec
|
||||
|
||||
import js
|
||||
import pyscript
|
||||
|
||||
|
||||
@@ -114,6 +115,6 @@ class Document(Node):
|
||||
self._el = impl.createDocument(None, "document", None)
|
||||
|
||||
|
||||
pyscript.js.document = doc = Document()
|
||||
pyscript.js.document.head = doc.createElement("head")
|
||||
pyscript.js.document.body = doc.createElement("body")
|
||||
js.document = doc = Document()
|
||||
js.document.head = doc.createElement("head")
|
||||
js.document.body = doc.createElement("body")
|
||||
|
||||
@@ -3,51 +3,54 @@ import textwrap
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pyscript
|
||||
from pyscript import HTML, Element, _html
|
||||
from pyscript._deprecated_globals import DeprecatedGlobal
|
||||
from pyscript._internal import set_version_info, uses_top_level_await
|
||||
from pyscript._mime import format_mime
|
||||
|
||||
|
||||
class TestElement:
|
||||
def test_id_is_correct(self):
|
||||
el = pyscript.Element("something")
|
||||
el = Element("something")
|
||||
assert el.id == "something"
|
||||
|
||||
def test_element(self, monkeypatch):
|
||||
el = pyscript.Element("something")
|
||||
js_mock = Mock()
|
||||
js_mock.document = Mock()
|
||||
el = Element("something")
|
||||
document = Mock()
|
||||
call_result = "some_result"
|
||||
js_mock.document.querySelector = Mock(return_value=call_result)
|
||||
monkeypatch.setattr(pyscript, "js", js_mock)
|
||||
document.querySelector = Mock(return_value=call_result)
|
||||
monkeypatch.setattr(_html, "document", document)
|
||||
assert not el._element
|
||||
real_element = el.element
|
||||
assert real_element
|
||||
assert js_mock.document.querySelector.call_count == 1
|
||||
js_mock.document.querySelector.assert_called_with("#something")
|
||||
assert document.querySelector.call_count == 1
|
||||
document.querySelector.assert_called_with("#something")
|
||||
assert real_element == call_result
|
||||
|
||||
|
||||
def test_format_mime_str():
|
||||
obj = "just a string"
|
||||
out, mime = pyscript.format_mime(obj)
|
||||
out, mime = format_mime(obj)
|
||||
assert out == obj
|
||||
assert mime == "text/plain"
|
||||
|
||||
|
||||
def test_format_mime_str_escaping():
|
||||
obj = "<p>hello</p>"
|
||||
out, mime = pyscript.format_mime(obj)
|
||||
out, mime = format_mime(obj)
|
||||
assert out == "<p>hello</p>"
|
||||
assert mime == "text/plain"
|
||||
|
||||
|
||||
def test_format_mime_repr_escaping():
|
||||
out, mime = pyscript.format_mime(sys)
|
||||
out, mime = format_mime(sys)
|
||||
assert out == "<module 'sys' (built-in)>"
|
||||
assert mime == "text/plain"
|
||||
|
||||
|
||||
def test_format_mime_HTML():
|
||||
obj = pyscript.HTML("<p>hello</p>")
|
||||
out, mime = pyscript.format_mime(obj)
|
||||
obj = HTML("<p>hello</p>")
|
||||
out, mime = format_mime(obj)
|
||||
assert out == "<p>hello</p>"
|
||||
assert mime == "text/html"
|
||||
|
||||
@@ -55,7 +58,7 @@ def test_format_mime_HTML():
|
||||
def test_uses_top_level_await():
|
||||
# Basic Case
|
||||
src = "x = 1"
|
||||
assert pyscript.uses_top_level_await(src) is False
|
||||
assert uses_top_level_await(src) is False
|
||||
|
||||
# Comments are not top-level await
|
||||
src = textwrap.dedent(
|
||||
@@ -64,7 +67,7 @@ def test_uses_top_level_await():
|
||||
"""
|
||||
)
|
||||
|
||||
assert pyscript.uses_top_level_await(src) is False
|
||||
assert uses_top_level_await(src) is False
|
||||
|
||||
# Top-level-await cases
|
||||
src = textwrap.dedent(
|
||||
@@ -74,7 +77,7 @@ def test_uses_top_level_await():
|
||||
await foo
|
||||
"""
|
||||
)
|
||||
assert pyscript.uses_top_level_await(src) is True
|
||||
assert uses_top_level_await(src) is True
|
||||
|
||||
src = textwrap.dedent(
|
||||
"""
|
||||
@@ -82,7 +85,7 @@ def test_uses_top_level_await():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
assert pyscript.uses_top_level_await(src) is True
|
||||
assert uses_top_level_await(src) is True
|
||||
|
||||
src = textwrap.dedent(
|
||||
"""
|
||||
@@ -90,7 +93,7 @@ def test_uses_top_level_await():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
assert pyscript.uses_top_level_await(src) is True
|
||||
assert uses_top_level_await(src) is True
|
||||
|
||||
# Acceptable await/async for/async with cases
|
||||
src = textwrap.dedent(
|
||||
@@ -99,7 +102,7 @@ def test_uses_top_level_await():
|
||||
await foo()
|
||||
"""
|
||||
)
|
||||
assert pyscript.uses_top_level_await(src) is False
|
||||
assert uses_top_level_await(src) is False
|
||||
|
||||
src = textwrap.dedent(
|
||||
"""
|
||||
@@ -108,7 +111,7 @@ def test_uses_top_level_await():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
assert pyscript.uses_top_level_await(src) is False
|
||||
assert uses_top_level_await(src) is False
|
||||
|
||||
src = textwrap.dedent(
|
||||
"""
|
||||
@@ -117,21 +120,17 @@ def test_uses_top_level_await():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
assert pyscript.uses_top_level_await(src) is False
|
||||
assert uses_top_level_await(src) is False
|
||||
|
||||
|
||||
def test_set_version_info():
|
||||
version_string = "1234.56.78.ABCD"
|
||||
pyscript._set_version_info(version_string)
|
||||
set_version_info(version_string)
|
||||
assert pyscript.__version__ == version_string
|
||||
assert pyscript.version_info == (1234, 56, 78, "ABCD")
|
||||
#
|
||||
# for backwards compatibility, should be killed eventually
|
||||
assert pyscript.PyScript.__version__ == pyscript.__version__
|
||||
assert pyscript.PyScript.version_info == pyscript.version_info
|
||||
|
||||
|
||||
class MyDeprecatedGlobal(pyscript.DeprecatedGlobal):
|
||||
class MyDeprecatedGlobal(DeprecatedGlobal):
|
||||
"""
|
||||
A subclass of DeprecatedGlobal, for tests.
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import html
|
||||
from unittest.mock import Mock
|
||||
|
||||
import js
|
||||
import py_markdown
|
||||
import py_tutor
|
||||
import pyscript
|
||||
import pyscript_plugins_tester as ppt
|
||||
|
||||
TUTOR_SOURCE = """
|
||||
@@ -52,7 +52,7 @@ class TestPyTutor:
|
||||
related prism assets have been added to the page head
|
||||
"""
|
||||
# GIVEN a previous call to py_tutor.plugin.append_script_to_page
|
||||
head = pyscript.js.document.head
|
||||
head = js.document.head
|
||||
|
||||
# EXPECT the head to contain a link element pointing to the prism.min.css
|
||||
links = head.getElementsByTagName("link")
|
||||
@@ -76,7 +76,7 @@ class TestPyTutor:
|
||||
to the page body
|
||||
"""
|
||||
# GIVEN a previous call to py_tutor.plugin.append_script_to_page
|
||||
body = pyscript.js.document.body
|
||||
body = js.document.body
|
||||
|
||||
# EXPECT the body of the page to contain a script of type text/javascript
|
||||
# and that contains the py_tutor.PAGE_SCRIPT script
|
||||
@@ -108,7 +108,7 @@ class TestPyTutor:
|
||||
console.info.assert_any_call("Creating new code section element.")
|
||||
|
||||
# EXPECT the page body to contain a section with the input source code
|
||||
body = pyscript.js.document.body
|
||||
body = js.document.body
|
||||
sections = body.getElementsByTagName("section")
|
||||
section = sections[0]
|
||||
assert "code" in section.classList._classes
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('RemoteInterpreter', () => {
|
||||
const { port1, port2 } = new Synclink.FakeMessageChannel() as unknown as MessageChannel;
|
||||
beforeAll(async () => {
|
||||
const SRC = '../pyscriptjs/node_modules/pyodide/pyodide.js';
|
||||
const config: AppConfig = { interpreters: [{ src: SRC }] };
|
||||
const config: AppConfig = { interpreters: [{ src: SRC }], packages: [] };
|
||||
// Dynamic import of RemoteInterpreter sees our mocked Python package.
|
||||
({ RemoteInterpreter } = await import('../../src/remote_interpreter'));
|
||||
const remote_interpreter = new RemoteInterpreter(SRC);
|
||||
|
||||
Reference in New Issue
Block a user