mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
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:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ==============
|
||||
|
||||
|
||||
@@ -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.",
|
||||
]
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user