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

View File

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

View File

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

View File

@@ -210,22 +210,21 @@ export class PyScriptApp {
//Refresh the module cache so Python consistently finds pyscript module //Refresh the module cache so Python consistently finds pyscript module
runtime.invalidate_module_path_cache() 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'); const pyscript_module = runtime.interpreter.pyimport('pyscript');
pyscript_module.define_custom_element = define_custom_element; 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(); pyscript_module.destroy();
// TODO: Currently adding the imports for backwards compatibility, we should // import some carefully selected names into the global namespace
// remove it
await runtime.run(` await runtime.run(`
from pyscript import * import js
`); import pyscript
logger.warn(`DEPRECATION WARNING: 'micropip', 'Element', 'console', 'document' and several other \ from pyscript import Element, display, HTML
objects form the pyscript module (with the exception of 'display') will be \ pyscript._install_deprecated_globals_2022_12_1(globals())
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`);
if (this.config.packages) { if (this.config.packages) {
logger.info('Packages to install: ', 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'); const logger = getLogger('pyexec');
export function pyExec(runtime: Runtime, pysrc: string, outElem: HTMLElement) { export function pyExec(runtime: Runtime, pysrc: string, outElem: HTMLElement) {
// this is the python function defined in pyscript.py //This is pyscript.py
const set_current_display_target = runtime.globals.get('set_current_display_target'); const pyscript_py = runtime.interpreter.pyimport('pyscript');
ensureUniqueId(outElem); ensureUniqueId(outElem);
set_current_display_target(outElem.id); pyscript_py.set_current_display_target(outElem.id);
//This is the python function defined in pyscript.py
const usesTopLevelAwait = runtime.globals.get('uses_top_level_await');
try { try {
try { try {
if (usesTopLevelAwait(pysrc)) { if (pyscript_py.uses_top_level_await(pysrc)) {
throw new UserError( throw new UserError(
ErrorCode.TOP_LEVEL_AWAIT, ErrorCode.TOP_LEVEL_AWAIT,
'The use of top-level "await", "async for", and ' + '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); displayPyException(err, outElem);
} }
} finally { } 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'); const logger = getLogger('pyscript/pyodide');
interface Micropip { interface Micropip extends PyProxy {
install: (packageName: string | string[]) => Promise<void>; install: (packageName: string | string[]) => Promise<void>;
destroy: () => void; destroy: () => void;
} }
@@ -91,7 +91,8 @@ export class PyodideRuntime extends Runtime {
async installPackage(package_name: string | string[]): Promise<void> { async installPackage(package_name: string | string[]): Promise<void> {
if (package_name.length > 0) { if (package_name.length > 0) {
logger.info(`micropip install ${package_name.toString()}`); logger.info(`micropip install ${package_name.toString()}`);
const micropip = this.globals.get('micropip') as Micropip;
const micropip = this.interpreter.pyimport('micropip') as Micropip;
try { try {
await micropip.install(package_name); await micropip.install(package_name);
micropip.destroy(); micropip.destroy();

View File

@@ -8,8 +8,7 @@ import time
from collections import namedtuple from collections import namedtuple
from textwrap import dedent from textwrap import dedent
import micropip # noqa: F401 import js
from js import console, document
try: try:
from pyodide import create_proxy 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: class HTML:
""" """
Wrap a string so that display() can render it as plain HTML Wrap a string so that display() can render it as plain HTML
@@ -126,7 +160,7 @@ def format_mime(obj):
break break
if output is None: if output is None:
if not_available: if not_available:
console.warn( js.console.warn(
f"Rendered object requested unavailable MIME renderers: {not_available}" f"Rendered object requested unavailable MIME renderers: {not_available}"
) )
output = repr(output) output = repr(output)
@@ -138,57 +172,22 @@ def format_mime(obj):
return MIME_RENDERERS[mime_type](output, meta), mime_type return MIME_RENDERERS[mime_type](output, meta), mime_type
class PyScript: @staticmethod
loop = loop def run_until_complete(f):
_ = loop.run_until_complete(f)
@staticmethod
def run_until_complete(f):
_ = loop.run_until_complete(f)
@staticmethod @staticmethod
def write(element_id, value, append=False, exec_id=0): def write(element_id, value, append=False, exec_id=0):
"""Writes value to the element with id "element_id""" """Writes value to the element with id "element_id"""
Element(element_id).write(value=value, append=append) Element(element_id).write(value=value, append=append)
console.warn( js.console.warn(
dedent( dedent(
"""PyScript Deprecation Warning: PyScript.write is """PyScript Deprecation Warning: PyScript.write is
marked as deprecated and will be removed sometime soon. Please, use marked as deprecated and will be removed sometime soon. Please, use
Element(<id>).write instead.""" 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): def set_current_display_target(target_id):
@@ -231,7 +230,7 @@ class Element:
def element(self): def element(self):
"""Return the dom element""" """Return the dom element"""
if not self._element: if not self._element:
self._element = document.querySelector(f"#{self._id}") self._element = js.document.querySelector(f"#{self._id}")
return self._element return self._element
@property @property
@@ -248,7 +247,7 @@ class Element:
return return
if append: if append:
child = document.createElement("div") child = js.document.createElement("div")
self.element.appendChild(child) self.element.appendChild(child)
if self.element.children: if self.element.children:
@@ -257,7 +256,7 @@ class Element:
out_element = self.element out_element = self.element
if mime_type in ("application/javascript", "text/html"): 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) out_element.appendChild(script_element)
else: else:
out_element.innerHTML = html out_element.innerHTML = html
@@ -278,7 +277,7 @@ class Element:
if _el: if _el:
return Element(_el.id, _el) return Element(_el.id, _el)
else: 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): def clone(self, new_id=None, to=None):
if new_id is None: if new_id is None:
@@ -318,7 +317,7 @@ def add_classes(element, class_list):
def create(what, id_=None, classes=""): def create(what, id_=None, classes=""):
element = document.createElement(what) element = js.document.createElement(what)
if id_: if id_:
element.id = id_ element.id = id_
add_classes(element, classes) add_classes(element, classes)
@@ -432,7 +431,7 @@ class PyListTemplate:
Element(new_id).element.onclick = foo Element(new_id).element.onclick = foo
def connect(self): 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" main_div.id = self._id + "-list-tasks-container"
if self.theme: if self.theme:
@@ -502,7 +501,7 @@ class Plugin:
def register_custom_element(self, tag): def register_custom_element(self, tag):
# TODO: Ideally would be better to use the logger. # 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_): def wrapper(class_):
# TODO: this is very pyodide specific but will have to do # TODO: this is very pyodide specific but will have to do
@@ -512,4 +511,158 @@ class Plugin:
return create_proxy(wrapper) 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)] elems = [loc.nth(i) for i in range(n)]
return iter(elems) 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 ============== # ============== Helpers and utility functions ==============

View File

@@ -214,8 +214,8 @@ class TestBasic(PyScriptTest):
""" """
<py-script> <py-script>
import js import js
js.console.log(PyScript.__version__) js.console.log(pyscript.__version__)
js.console.log(str(PyScript.version_info)) js.console.log(str(pyscript.version_info))
</py-script> </py-script>
""" """
) )
@@ -232,27 +232,47 @@ class TestBasic(PyScriptTest):
is not None is not None
) )
def test_python_modules_deprecated(self): def test_assert_no_banners(self):
# GIVEN a py-script tag """
Test that the DOM doesn't contain error/warning banners
"""
self.pyscript_run( self.pyscript_run(
""" """
<py-script> <py-script>
print('hello pyscript') import pyscript
raise Exception('this is an error') pyscript.showWarning("hello")
pyscript.showWarning("world")
</py-script> </py-script>
""" """
) )
# TODO: Adding a quick check that the deprecation warning is logged. Not spending with pytest.raises(AssertionError, match="Found 2 alert banners"):
# to much time to make it perfect since we'll remove this right after the self.assert_no_banners()
# release. (Anyone wanting to improve it, please feel free to)
warning_msg = ( def test_deprecated_globals(self):
"[pyscript/main] DEPRECATION WARNING: 'micropip', 'Element', 'console', 'document' " self.pyscript_run(
"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. " <py-script>
"To avoid errors in future releases use import from pyscript " # trigger various warnings
"instead. For instance: from pyscript import micropip, Element, " create("div", classes="a b c")
"console, document" 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 banner = self.page.locator(".py-warning")
# global namespace in the next releases messages = banner.all_inner_texts()
assert warning_msg in self.console.warning.lines 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 # Source code of a simple plugin that creates a Custom Element for testing purposes
CE_PLUGIN_CODE = """ CE_PLUGIN_CODE = """
from pyscript import Plugin, console from pyscript import Plugin
from js import console
plugin = Plugin('py-upper') plugin = Plugin('py-upper')
@@ -20,7 +21,8 @@ class Upper:
# Source of a plugin hooks into the PyScript App lifecycle events # Source of a plugin hooks into the PyScript App lifecycle events
HOOKS_PLUGIN_CODE = """ HOOKS_PLUGIN_CODE = """
from pyscript import Plugin, console from pyscript import Plugin
from js import console
class TestLogger(Plugin): class TestLogger(Plugin):
def configure(self, config): def configure(self, config):
@@ -44,7 +46,8 @@ plugin = TestLogger()
# Source of a script that doesn't call define a `plugin` attribute # Source of a script that doesn't call define a `plugin` attribute
NO_PLUGIN_CODE = """ NO_PLUGIN_CODE = """
from pyscript import Plugin, console from pyscript import Plugin
from js import console
class TestLogger(Plugin): class TestLogger(Plugin):
pass pass
@@ -52,7 +55,8 @@ class TestLogger(Plugin):
# Source code of a simple plugin that creates a Custom Element for testing purposes # Source code of a simple plugin that creates a Custom Element for testing purposes
CODE_CE_PLUGIN_BAD_RETURNS = """ CODE_CE_PLUGIN_BAD_RETURNS = """
from pyscript import Plugin, console from pyscript import Plugin
from js import console
plugin = Plugin('py-broken') plugin = Plugin('py-broken')

View File

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

View File

@@ -12,15 +12,16 @@ class TestElement:
def test_element(self, monkeypatch): def test_element(self, monkeypatch):
el = pyscript.Element("something") el = pyscript.Element("something")
document_mock = Mock() js_mock = Mock()
js_mock.document = Mock()
call_result = "some_result" call_result = "some_result"
document_mock.querySelector = Mock(return_value=call_result) js_mock.document.querySelector = Mock(return_value=call_result)
monkeypatch.setattr(pyscript, "document", document_mock) monkeypatch.setattr(pyscript, "js", js_mock)
assert not el._element assert not el._element
real_element = el.element real_element = el.element
assert real_element assert real_element
assert pyscript.document.querySelector.call_count == 1 assert js_mock.document.querySelector.call_count == 1
pyscript.document.querySelector.assert_called_with("#something") js_mock.document.querySelector.assert_called_with("#something")
assert real_element == call_result assert real_element == call_result
@@ -121,6 +122,86 @@ def test_uses_top_level_await():
def test_set_version_info(): def test_set_version_info():
version_string = "1234.56.78.ABCD" version_string = "1234.56.78.ABCD"
pyscript.PyScript.set_version_info(version_string) pyscript._set_version_info(version_string)
assert pyscript.PyScript.__version__ == version_string assert pyscript.__version__ == version_string
assert pyscript.PyScript.version_info == (1234, 56, 78, "ABCD") 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