Introduce DeprecatedGlobal and show proper warnings (#1014)

* kill the PyScript class and the weird pyscript instance; from the user point of view its functionalities are still available as pyscript.*, but pyscript is not the module, not the instance of PyScript

* simplify the code in _set_version_info, while I'm at it

* start to implement DeprecatedGlobal

* DeprecatedGlobal.__getattr__

* don't show the same warning twice

* DeprecatedGlobal.__call__

* make it possible to specify a different warning message for every global

* WIP: carefully use DeprecatedGlobal to show reasonable warning messages depending on which name you are accessing to. More names to follow

* deprecate more names

* deprecate private names

* depreacte direct usage of console and document

* deprecate the PyScript class

* use a better error message

* fix test_pyscript.py

* introduce a __repr__ for DeprecatedGlobal

* add an helper to ensure that we don't show any error or warning on the page

* WIP: ensure that examples don't use depreacted features. Many tests are failing

* don't deprecate Element

* don't use the global micropip to install packages, else we trigger a warning

* use a better error message for micropip

* fix test_todo_pylist to avoid using deprecated globals

* fix test_webgl_raycaster

* fix tests

* make HTML globally available

* add MIME_RENDERERS and MIME_METHODS

* fix the typing of Micropip, thanks to @FabioRosado
This commit is contained in:
Antonio Cuni
2022-12-06 14:31:57 +01:00
committed by GitHub
parent 94f2ac6204
commit e8318a98f0
12 changed files with 407 additions and 115 deletions

View File

@@ -1,7 +1,9 @@
from datetime import datetime as dt
import pyscript
class PyItem(PyItemTemplate):
class PyItem(pyscript.PyItemTemplate):
def on_click(self, evt=None):
self.data["done"] = not self.data["done"]
self.strike(self.data["done"])
@@ -9,7 +11,7 @@ class PyItem(PyItemTemplate):
self.select("input").element.checked = self.data["done"]
class PyList(PyListTemplate):
class PyList(pyscript.PyListTemplate):
item_class = PyItem
def add(self, item):

View File

@@ -124,16 +124,17 @@
<p>pylist.py</p>
<pre class="prism-code language-python">
<code class="language-python">
import pyscript
from datetime import datetime as dt
class PyItem(PyItemTemplate):
class PyItem(pyscript.PyItemTemplate):
def on_click(self, evt=None):
self.data["done"] = not self.data["done"]
self.strike(self.data["done"])
self.select("input").element.checked = self.data["done"]
class PyList(PyListTemplate):
class PyList(pyscript.PyListTemplate):
item_class = PyItem
def add(self, item):

View File

@@ -31,6 +31,7 @@ from js import Math
from js import THREE
from js import performance
from js import Object
from js import document
import asyncio

View File

@@ -210,22 +210,21 @@ export class PyScriptApp {
//Refresh the module cache so Python consistently finds pyscript module
runtime.invalidate_module_path_cache()
// inject `define_custom_element` it into the PyScript module scope
// inject `define_custom_element` and showWarning it into the PyScript
// module scope
const pyscript_module = runtime.interpreter.pyimport('pyscript');
pyscript_module.define_custom_element = define_custom_element;
pyscript_module.PyScript.set_version_info(version);
pyscript_module.showWarning = showWarning;
pyscript_module._set_version_info(version);
pyscript_module.destroy();
// TODO: Currently adding the imports for backwards compatibility, we should
// remove it
// import some carefully selected names into the global namespace
await runtime.run(`
from pyscript import *
`);
logger.warn(`DEPRECATION WARNING: 'micropip', 'Element', 'console', 'document' and several other \
objects form the pyscript module (with the exception of 'display') will be \
be removed from the Python global namespace in the following release. \
To avoid errors in future releases use import from pyscript instead. For instance: \
from pyscript import micropip, Element, console, document`);
import js
import pyscript
from pyscript import Element, display, HTML
pyscript._install_deprecated_globals_2022_12_1(globals())
`)
if (this.config.packages) {
logger.info('Packages to install: ', this.config.packages);

View File

@@ -6,15 +6,14 @@ import type { Runtime } from './runtime';
const logger = getLogger('pyexec');
export function pyExec(runtime: Runtime, pysrc: string, outElem: HTMLElement) {
// this is the python function defined in pyscript.py
const set_current_display_target = runtime.globals.get('set_current_display_target');
//This is pyscript.py
const pyscript_py = runtime.interpreter.pyimport('pyscript');
ensureUniqueId(outElem);
set_current_display_target(outElem.id);
//This is the python function defined in pyscript.py
const usesTopLevelAwait = runtime.globals.get('uses_top_level_await');
pyscript_py.set_current_display_target(outElem.id);
try {
try {
if (usesTopLevelAwait(pysrc)) {
if (pyscript_py.uses_top_level_await(pysrc)) {
throw new UserError(
ErrorCode.TOP_LEVEL_AWAIT,
'The use of top-level "await", "async for", and ' +
@@ -33,7 +32,8 @@ export function pyExec(runtime: Runtime, pysrc: string, outElem: HTMLElement) {
displayPyException(err, outElem);
}
} finally {
set_current_display_target(undefined);
pyscript_py.set_current_display_target(undefined);
pyscript_py.destroy();
}
}

View File

@@ -10,7 +10,7 @@ declare const loadPyodide: typeof loadPyodideDeclaration;
const logger = getLogger('pyscript/pyodide');
interface Micropip {
interface Micropip extends PyProxy {
install: (packageName: string | string[]) => Promise<void>;
destroy: () => void;
}
@@ -91,7 +91,8 @@ export class PyodideRuntime extends Runtime {
async installPackage(package_name: string | string[]): Promise<void> {
if (package_name.length > 0) {
logger.info(`micropip install ${package_name.toString()}`);
const micropip = this.globals.get('micropip') as Micropip;
const micropip = this.interpreter.pyimport('micropip') as Micropip;
try {
await micropip.install(package_name);
micropip.destroy();

View File

@@ -8,8 +8,7 @@ import time
from collections import namedtuple
from textwrap import dedent
import micropip # noqa: F401
from js import console, document
import js
try:
from pyodide import create_proxy
@@ -68,6 +67,41 @@ MIME_RENDERERS = {
}
# these are set by _set_version_info
__version__ = None
version_info = None
def _set_version_info(version_from_runtime: str):
"""Sets the __version__ and version_info properties from provided JSON data
Args:
version_from_runtime (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
__version__ = version_from_runtime
version_parts = version_from_runtime.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
@@ -126,7 +160,7 @@ def format_mime(obj):
break
if output is None:
if not_available:
console.warn(
js.console.warn(
f"Rendered object requested unavailable MIME renderers: {not_available}"
)
output = repr(output)
@@ -138,57 +172,22 @@ def format_mime(obj):
return MIME_RENDERERS[mime_type](output, meta), mime_type
class PyScript:
loop = loop
@staticmethod
def run_until_complete(f):
_ = loop.run_until_complete(f)
@staticmethod
def run_until_complete(f):
_ = loop.run_until_complete(f)
@staticmethod
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."""
)
@staticmethod
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."""
)
@classmethod
def set_version_info(cls, version_from_runtime: str):
"""Sets the __version__ and version_info properties from provided JSON data
Args:
version_from_runtime (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__ is the same string from runtime.ts
cls.__version__ = version_from_runtime
# version_info is namedtuple: (year, month, minor, releaselevel)
version_parts = version_from_runtime.split(".")
version_dict = {
"year": int(version_parts[0]),
"month": int(version_parts[1]),
"minor": int(version_parts[2]),
}
# If the version only has three parts (e.g. 2022.09.1), let the releaselevel be ""
try:
version_dict["releaselevel"] = version_parts[3]
except IndexError:
version_dict["releaselevel"] = ""
# Format mimics sys.version_info
_VersionInfo = namedtuple("version_info", version_dict.keys())
cls.version_info = _VersionInfo(**version_dict)
# tidy up class namespace
del cls.set_version_info
)
def set_current_display_target(target_id):
@@ -231,7 +230,7 @@ class Element:
def element(self):
"""Return the dom element"""
if not self._element:
self._element = document.querySelector(f"#{self._id}")
self._element = js.document.querySelector(f"#{self._id}")
return self._element
@property
@@ -248,7 +247,7 @@ class Element:
return
if append:
child = document.createElement("div")
child = js.document.createElement("div")
self.element.appendChild(child)
if self.element.children:
@@ -257,7 +256,7 @@ class Element:
out_element = self.element
if mime_type in ("application/javascript", "text/html"):
script_element = document.createRange().createContextualFragment(html)
script_element = js.document.createRange().createContextualFragment(html)
out_element.appendChild(script_element)
else:
out_element.innerHTML = html
@@ -278,7 +277,7 @@ class Element:
if _el:
return Element(_el.id, _el)
else:
console.warn(f"WARNING: can't find element matching query {query}")
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:
@@ -318,7 +317,7 @@ def add_classes(element, class_list):
def create(what, id_=None, classes=""):
element = document.createElement(what)
element = js.document.createElement(what)
if id_:
element.id = id_
add_classes(element, classes)
@@ -432,7 +431,7 @@ class PyListTemplate:
Element(new_id).element.onclick = foo
def connect(self):
self.md = main_div = document.createElement("div")
self.md = main_div = js.document.createElement("div")
main_div.id = self._id + "-list-tasks-container"
if self.theme:
@@ -502,7 +501,7 @@ class Plugin:
def register_custom_element(self, tag):
# TODO: Ideally would be better to use the logger.
console.info(f"Defining new custom element {tag}")
js.console.info(f"Defining new custom element {tag}")
def wrapper(class_):
# TODO: this is very pyodide specific but will have to do
@@ -512,4 +511,158 @@ class Plugin:
return create_proxy(wrapper)
pyscript = PyScript()
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)

View File

@@ -292,6 +292,17 @@ class PyScriptTest:
elems = [loc.nth(i) for i in range(n)]
return iter(elems)
def assert_no_banners(self):
"""
Ensure that there are no alert banners on the page, which are used for
errors and warnings. Raise AssertionError if any if found.
"""
loc = self.page.locator(".alert-banner")
n = loc.count()
if n > 0:
text = "\n".join(loc.all_inner_texts())
raise AssertionError(f"Found {n} alert banners:\n" + text)
# ============== Helpers and utility functions ==============

View File

@@ -214,8 +214,8 @@ class TestBasic(PyScriptTest):
"""
<py-script>
import js
js.console.log(PyScript.__version__)
js.console.log(str(PyScript.version_info))
js.console.log(pyscript.__version__)
js.console.log(str(pyscript.version_info))
</py-script>
"""
)
@@ -232,27 +232,47 @@ class TestBasic(PyScriptTest):
is not None
)
def test_python_modules_deprecated(self):
# GIVEN a py-script tag
def test_assert_no_banners(self):
"""
Test that the DOM doesn't contain error/warning banners
"""
self.pyscript_run(
"""
<py-script>
print('hello pyscript')
raise Exception('this is an error')
import pyscript
pyscript.showWarning("hello")
pyscript.showWarning("world")
</py-script>
"""
"""
)
# TODO: Adding a quick check that the deprecation warning is logged. Not spending
# to much time to make it perfect since we'll remove this right after the
# release. (Anyone wanting to improve it, please feel free to)
warning_msg = (
"[pyscript/main] DEPRECATION WARNING: 'micropip', 'Element', 'console', 'document' "
"and several other objects form the pyscript module (with the exception of 'display') "
"will be be removed from the Python global namespace in the following release. "
"To avoid errors in future releases use import from pyscript "
"instead. For instance: from pyscript import micropip, Element, "
"console, document"
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>
"""
)
# we EXPECTED to find a deprecation warning about what will be removed from the Python
# global namespace in the next releases
assert warning_msg in self.console.warning.lines
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.",
]

View File

@@ -2,7 +2,8 @@ from .support import PyScriptTest
# Source code of a simple plugin that creates a Custom Element for testing purposes
CE_PLUGIN_CODE = """
from pyscript import Plugin, console
from pyscript import Plugin
from js import console
plugin = Plugin('py-upper')
@@ -20,7 +21,8 @@ class Upper:
# Source of a plugin hooks into the PyScript App lifecycle events
HOOKS_PLUGIN_CODE = """
from pyscript import Plugin, console
from pyscript import Plugin
from js import console
class TestLogger(Plugin):
def configure(self, config):
@@ -44,7 +46,8 @@ plugin = TestLogger()
# Source of a script that doesn't call define a `plugin` attribute
NO_PLUGIN_CODE = """
from pyscript import Plugin, console
from pyscript import Plugin
from js import console
class TestLogger(Plugin):
pass
@@ -52,7 +55,8 @@ class TestLogger(Plugin):
# Source code of a simple plugin that creates a Custom Element for testing purposes
CODE_CE_PLUGIN_BAD_RETURNS = """
from pyscript import Plugin, console
from pyscript import Plugin
from js import console
plugin = Plugin('py-broken')

View File

@@ -60,6 +60,7 @@ class TestExamples(PyScriptTest):
content = self.page.content()
pattern = "\\d+/\\d+/\\d+, \\d+:\\d+:\\d+" # e.g. 08/09/2022 15:57:32
assert re.search(pattern, content)
self.assert_no_banners()
def test_simple_clock(self):
self.goto("examples/simple_clock.html")
@@ -77,6 +78,7 @@ class TestExamples(PyScriptTest):
time.sleep(1)
else:
assert False, "Espresso time not found :("
self.assert_no_banners()
def test_altair(self):
self.goto("examples/altair.html")
@@ -94,6 +96,7 @@ class TestExamples(PyScriptTest):
# Let's confirm that the links are visible now after clicking the menu
assert save_as_png_link.is_visible()
assert see_source_link.is_visible()
self.assert_no_banners()
def test_bokeh(self):
# XXX improve this test
@@ -101,6 +104,7 @@ class TestExamples(PyScriptTest):
self.wait_for_pyscript()
assert self.page.title() == "Bokeh Example"
wait_for_render(self.page, "*", '<div.*class=\\"bk\\".*>')
self.assert_no_banners()
def test_bokeh_interactive(self):
# XXX improve this test
@@ -108,6 +112,7 @@ class TestExamples(PyScriptTest):
self.wait_for_pyscript()
assert self.page.title() == "Bokeh Example"
wait_for_render(self.page, "*", '<div.*?class=\\"bk\\".*?>')
self.assert_no_banners()
@pytest.mark.skip("flaky, see issue 759")
def test_d3(self):
@@ -123,6 +128,7 @@ class TestExamples(PyScriptTest):
# Let's simply assert that the text of the chart is as expected which
# means that the chart rendered successfully and with the right text
assert "🍊21\n🍇13\n🍏8\n🍌5\n🍐3\n🍋2\n🍎1\n🍉1" in pyscript_chart.inner_text()
self.assert_no_banners()
def test_folium(self):
self.goto("examples/folium.html")
@@ -145,6 +151,7 @@ class TestExamples(PyScriptTest):
zoom_out = iframe.locator("[aria-label='Zoom out']")
assert "" in zoom_out.inner_text()
zoom_out.click()
self.assert_no_banners()
def test_matplotlib(self):
self.goto("examples/matplotlib.html")
@@ -171,6 +178,7 @@ class TestExamples(PyScriptTest):
# let's confirm that they are the same
deviation = np.mean(np.abs(img_data - ref_data))
assert deviation == 0.0
self.assert_no_banners()
def test_numpy_canvas_fractals(self):
self.goto("examples/numpy_canvas_fractals.html")
@@ -217,6 +225,7 @@ class TestExamples(PyScriptTest):
assert self.console.log.lines[-2] == "Computing Newton set ..."
# Confirm that changing the input values, triggered a new computation
assert self.console.log.lines[-1] == "Computing Newton set ..."
self.assert_no_banners()
def test_panel(self):
self.goto("examples/panel.html")
@@ -234,6 +243,7 @@ class TestExamples(PyScriptTest):
# Let's confirm that slider title changed
assert slider_title.inner_text() == "Amplitude: 5"
self.assert_no_banners()
def test_panel_deckgl(self):
# XXX improve this test
@@ -241,6 +251,7 @@ class TestExamples(PyScriptTest):
self.wait_for_pyscript()
assert self.page.title() == "PyScript/Panel DeckGL Demo"
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
self.assert_no_banners()
def test_panel_kmeans(self):
# XXX improve this test
@@ -248,6 +259,7 @@ class TestExamples(PyScriptTest):
self.wait_for_pyscript()
assert self.page.title() == "Pyscript/Panel KMeans Demo"
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
self.assert_no_banners()
def test_panel_stream(self):
# XXX improve this test
@@ -255,6 +267,7 @@ class TestExamples(PyScriptTest):
self.wait_for_pyscript()
assert self.page.title() == "PyScript/Panel Streaming Demo"
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
self.assert_no_banners()
def test_repl(self):
self.goto("examples/repl.html")
@@ -274,6 +287,7 @@ class TestExamples(PyScriptTest):
# before looking into the text_content
assert self.page.wait_for_selector("#my-repl-2-2", state="attached")
assert self.page.locator("#my-repl-2-2").text_content() == "4"
self.assert_no_banners()
def test_repl2(self):
self.goto("examples/repl2.html")
@@ -289,6 +303,7 @@ class TestExamples(PyScriptTest):
content = self.page.content()
pattern = "\\d+/\\d+/\\d+, \\d+:\\d+:\\d+" # e.g. 08/09/2022 15:57:32
assert re.search(pattern, content)
self.assert_no_banners()
def test_todo(self):
self.goto("examples/todo.html")
@@ -316,6 +331,7 @@ class TestExamples(PyScriptTest):
'<p class="m-0 inline line-through">Fold laundry</p>'
in first_task.inner_html()
)
self.assert_no_banners()
def test_todo_pylist(self):
# XXX improve this test
@@ -323,6 +339,7 @@ class TestExamples(PyScriptTest):
self.wait_for_pyscript()
assert self.page.title() == "Todo App"
wait_for_render(self.page, "*", "<input.*?id=['\"]new-task-content['\"].*?>")
self.assert_no_banners()
@pytest.mark.xfail(reason="To be moved to collective and updated, see issue #686")
def test_toga_freedom(self):
@@ -340,6 +357,7 @@ class TestExamples(PyScriptTest):
self.page.locator("button#toga_calculate").click()
result = self.page.locator("#toga_c_input")
assert "40.555" in result.input_value()
self.assert_no_banners()
def test_webgl_raycaster_index(self):
# XXX improve this test
@@ -347,3 +365,4 @@ class TestExamples(PyScriptTest):
self.wait_for_pyscript()
assert self.page.title() == "Raycaster"
wait_for_render(self.page, "*", "<canvas.*?>")
self.assert_no_banners()

View File

@@ -12,15 +12,16 @@ class TestElement:
def test_element(self, monkeypatch):
el = pyscript.Element("something")
document_mock = Mock()
js_mock = Mock()
js_mock.document = Mock()
call_result = "some_result"
document_mock.querySelector = Mock(return_value=call_result)
monkeypatch.setattr(pyscript, "document", document_mock)
js_mock.document.querySelector = Mock(return_value=call_result)
monkeypatch.setattr(pyscript, "js", js_mock)
assert not el._element
real_element = el.element
assert real_element
assert pyscript.document.querySelector.call_count == 1
pyscript.document.querySelector.assert_called_with("#something")
assert js_mock.document.querySelector.call_count == 1
js_mock.document.querySelector.assert_called_with("#something")
assert real_element == call_result
@@ -121,6 +122,86 @@ def test_uses_top_level_await():
def test_set_version_info():
version_string = "1234.56.78.ABCD"
pyscript.PyScript.set_version_info(version_string)
assert pyscript.PyScript.__version__ == version_string
assert pyscript.PyScript.version_info == (1234, 56, 78, "ABCD")
pyscript._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):
"""
A subclass of DeprecatedGlobal, for tests.
Instead of showing warnings into the DOM (which we don't have inside unit
tests), we record the warnings into a field.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.warnings = []
def _show_warning(self, message):
self.warnings.append(message)
class TestDeprecatedGlobal:
def test_repr(self):
glob = MyDeprecatedGlobal("foo", None, "my message")
assert repr(glob) == "<DeprecatedGlobal('foo')>"
def test_show_warning_override(self):
"""
Test that our overriding of _show_warning actually works.
"""
glob = MyDeprecatedGlobal("foo", None, "my message")
glob._show_warning("foo")
glob._show_warning("bar")
assert glob.warnings == ["foo", "bar"]
def test_getattr(self):
class MyFakeObject:
name = "FooBar"
glob = MyDeprecatedGlobal("MyFakeObject", MyFakeObject, "this is my warning")
assert glob.name == "FooBar"
assert glob.warnings == ["this is my warning"]
def test_dont_show_warning_twice(self):
class MyFakeObject:
name = "foo"
surname = "bar"
glob = MyDeprecatedGlobal("MyFakeObject", MyFakeObject, "this is my warning")
assert glob.name == "foo"
assert glob.surname == "bar"
assert len(glob.warnings) == 1
def test_call(self):
def foo(x, y):
return x + y
glob = MyDeprecatedGlobal("foo", foo, "this is my warning")
assert glob(1, y=2) == 3
assert glob.warnings == ["this is my warning"]
def test_iter(self):
d = {"a": 1, "b": 2, "c": 3}
glob = MyDeprecatedGlobal("d", d, "this is my warning")
assert list(glob) == ["a", "b", "c"]
assert glob.warnings == ["this is my warning"]
def test_getitem(self):
d = {"a": 1, "b": 2, "c": 3}
glob = MyDeprecatedGlobal("d", d, "this is my warning")
assert glob["a"] == 1
assert glob.warnings == ["this is my warning"]
def test_setitem(self):
d = {"a": 1, "b": 2, "c": 3}
glob = MyDeprecatedGlobal("d", d, "this is my warning")
glob["a"] = 100
assert glob.warnings == ["this is my warning"]
assert glob["a"] == 100