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

@@ -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)