Repository: pyscript/pyscript

Issue:
[RELEASE BLOCKER] Clean up the `PyScript` vs `pyscript` mess
There is a lot of confusion about using `pyscript.XXX` vs `PyScript.XXX`. Related PR #982 .

Let's start from the current state in github main. We have this:
https://github.com/pyscript/pyscript/blob/a6280268383d92fe180f32b500a7829aeac7f877/pyscriptjs/src/python/pyscript.py#L121-L172

and this:
https://github.com/pyscript/pyscript/blob/a6280268383d92fe180f32b500a7829aeac7f877/pyscriptjs/src/python/pyscript.py#L465-L466

Remember that currently the content of `pyscript.py` is merely executed in the global namespace, so all names defined there are automatically available to the `<py-script>` tags.

Let's ignore `set_version_info` for now. The class `PyScript` exposes a bunch of static methods and the `loop` variable; `pyscript` is an instance of `PyScript`: since they are static methods, the two following lines are essentially the same:
```python
pyscript.write(...)
PyScript.write(...)

pyscript.loop
PyScript.loop

#...
```

`set_version_info` is weird: it is a class method which destroy itself upon calling. It is called from here:
https://github.com/pyscript/pyscript/blob/a6280268383d92fe180f32b500a7829aeac7f877/pyscriptjs/src/pyodide.ts#L73-L75

What it does is to compute and attach `version_info` to the `PyScript` class, which means that it will be available both as `PyScript.version_info` and `pyscript.version_info.`

---

Things will become even weirder as soon as #961 is merged. In that PR, `pyscript` becomes a real module. And by default we do `from pyscript import *` in the global namespace:
https://github.com/pyscript/pyscript/blob/764f01ce30112e4794f598448ebf12d90a531f3b/pyscriptjs/src/main.ts#L217-L224

So, the end result is that in that branch, we have the following:
```python
 # both should work. Note that pyscript is NOT the pyscript module, but the PyScript() instance!
PyScript.write(...)
pyscript.write(...)

import pyscript
# now we imported the pyscript module, and we shadowed the previous pyscript variable
pyscript.write(...) # AttributeError
PyScript.write(...) # works
pyscript.pyscript.write(...) # works
pyscript.PyScript.write(...) # works
```

Honestly, it's a giant mess and it's extremely confusing. Especially the fact that `pyscript` is always available but contains different things if you do or don't do the `import pyscript.`.
I think this is enough to be a release blocker IMHO.

---
**Solution**

I propose the following:
1. kill the `PyScript` class: its static methods will become top-level functions of the `pyscript` module. The same for `loop` (if we really want to expose it)
2. same for `set_version_info` (which should probably be renamed to `_set_version_info`. It should set a `version_info` attribute on pyscript module.
3. kill the `pyscript = PyScript()` instance
4. remove the `from pyscript import *`, and instead do a proper `import pyscript` automatically in the global namespace
5. do the equivalent of `from pyscript import display`, to make sure that `display()` is always available

This plan has the nice property of cleaning up a bit of mess (the useless `PyScript` class) and automatically preserve some backwards compatibility (because e.g. `pyscript.write()` continues to work: instead of being a staticmehod on the `pyscript` instance, it's a normal function on the `pyscript` module.

With this setup, we have the problem of breaking some other backwards compatibility though: in particular, the various `Element`, `document`, `micropip`, and `PyScript` are no longer available in the global scope. I propose to fix this using the solution which I described in this comment:
https://github.com/pyscript/pyscript/pull/961#discussion_r1032163076



Discussion:
@antocuni blimey.

Thoughts (that mostly resonate with your proposed changes).

* Why are we using static methods (do we do something special with the class)..? Otherwise, they should just be plain old functions, right..? Nice and simple and easy to understand.
* `set_version_info` feels bonkers. Why not just `const version = "1.2.3";` somewhere obvious as a single point of reference..?
* I'm not really a fan of new-style JS classes. Damn kids, get off my lawn. What's wrong with a function defining a closure and returning plain old JS object containing methods and const values..?
> * Why are we using static methods (do we do something special with the class)..? Otherwise, they should just be plain old functions, right..? Nice and simple and easy to understand.

I don't know, this code predates me. I guess that the original idea was to offer a `PyScript` namespace where to put various stuff, but I don't really know.
Anyway, AFAIK we don't do anything special with the class so we can just kill it.

> * `set_version_info` feels bonkers. Why not just `const version = "1.2.3";` somewhere obvious as a single point of reference..?

We have a single point of reference, but it's in JS:
https://github.com/pyscript/pyscript/blob/764f01ce30112e4794f598448ebf12d90a531f3b/pyscriptjs/src/runtime.ts#L7-L9

`set_version_info` is used to make it available also on the Python side. It seems a bit overengineered though.

> * I'm not really a fan of new-style JS classes. Damn kids, get off my lawn. What's wrong with a function defining a closure and returning plain old JS object containing methods and const values..?

in which way this is related to this issue? :thinking: 
> in which way this is related to this issue? thinking

My misunderstanding, and I must be getting grumpy in my old age. :-)
Just to clear something up - what @FabioRosado and I are discussing in #982 is unrelated to object naming in Python. It's about the JS module.

In #868 I had elected to name the exported _javascript_ module "pyscript", as in `pyscript.runtime.interpreter.runPython("1+2")`.  In a couple of the examples, I had erroneously left it as "PyScript" - I could not find a consistent naming convention (CamelCase vs. lowercase vs. Capitalized) for naming js modules, so I elected to use lowercase. But like I say, that's in JavaScript, is set in `rollup.config.js` and is unrelated to this issue.

----

I completely agree that the `pyscript` instance vs `PyScript` class thing is a mess, and we should fix it. Steps 1 through 5 seem sound (with one exception, see next) +5 on renaming to `_set_version_info()` creating a version_info and __version_info___ attribute on the module.

One challenge in moving from running pyscript.py in the global namespace to importing a module (+5 yes please) is that we've been importing certain modules into the global namespace "for the user" by importing them at the top of pyscript.py, including: ast, asyncio, base64, html, io, and time. So users can have been writing:

```python 
<py-script>
    now = time.localtime()  
    print(now)
</py-script>
```

This is bad and should not have been possible, but the point is it _has_ been possible, and if we're going to take that ability away, it would be nice to have a deprecation warning for it too.

I guess my desire would be to have deprecation warnings/error for all the objects that have been available in PyScript.py, since we don't know which ones users may have been using. In 2022.09.1, that list was:

> 'Element', 'HTML', 'MIME_METHODS', 'MIME_RENDERERS', 'PyItemTemplate', 'PyListTemplate', 'PyScript', 'PyWidgetTheme', 'TopLevelAsyncFinder', 'add_classes', 'ast', 'asyncio', 'base64', 'create', 'dedent', 'display', 'eval_formatter', 'format_mime', 'get_current_display_target', 'html', 'identity', 'io', 'loop', 'namedtuple', 'pyscript', 'render_image', 'set_current_display_target', 'time', 'uses_top_level_await'

Like it or not, that's been the accessible surface of the PyScript API to this point. And if we're going to change that, we should try to deprecate the unavailable objects. Or at least show a meaningful error - I don't know what's best to handle long-dead things like OutputCtxManager. It would be nice if users trying to access it got a NameError (or similar) that actually explained that those objects are gone.
On reflection, maybe I'm being too precious about the state of the API - especially with modules, users can figure out they need to import their own modules, presumably. 
With the `DeprecatedGlobal` approach which I described above, we can easily add as many names as we want.
Not sure if it's a good idea to use it for *all* the names which you mentioned, but if we want we wan
> Not sure if it's a good idea to use it for all the names which you mentioned, but if we want we want.

Agreed. deprecating every single name is overkill - it's a fault of mine, I get nervous around big user-facing API changes without properly deprecating every last little thing 😅. But with warnings along the lines you propose, this hopefully will be the last time we have such a massive change all in one fell swoop.
I'm +1 on all the proposal, except of 4. I'm quite committed to trying to give a deprecation cycle between changes/releases and `4` would be basically a way to see, "meh, we don't care this time". As @JeffersGlass mentioned, there are lots of walking dead code in there that we should remove (and I'd be ok to just remove without warning) but they are mixed with things that are part of the current API. We are talking about 1 month (actually we can release early in December if we want to accelerate the rest) and I don't see a good reason for not sticking with deprecating and removing later.


> Why are we using static methods (do we do something special with the class)..? Otherwise, they should just be plain old functions, right..? Nice and simple and easy to understand.

This was a bad ugly hack for the wrong reason and ended up remaining there. Simple as that :)

Also, there's another aspect of this that I want to make explicit (and that @antocuni and I definitely disagree). The vision of PyScript is **not** that PyScript == Python. PyScript is a platform where Python is at the center of gravity but its main goal is not [only] to enable Python on the Browser but to bring programming to the masses, on the browser. If there are things that add value and make programming easier and more fun, we should most definitely add them to the global namespace, even if "it's not Python" (because the goal here is not to be Python!). Supporting MicroPython "will not be Python"! Adding other languages and trying to make them interop will not be Python. 

When choosing what is "correct" and what is best for our users (== what our users need), we want to prioritize the latter. (Obviously not all things are absolutes and there are lots of nuances but that is the vision with which we created the project ).

The reason we are removing everything from the global namespace (and leaving only `display`), is not because "it needs to be == Python" but because we want to be intentional and careful when adding things to the default global namespace. 
> I'm +1 on all the proposal, except of 4. I'm quite committed to trying to give a deprecation cycle between changes/releases and `4` would be basically a way to see, "meh, we don't care this time".

I'm confused. I never proposed to break backwards compatibility. I proposed to use `DeprecatedGlobal` which raises a warning only if you actually use a name.


> The reason we are removing everything from the global namespace (and leaving only `display`), is not because "it needs to be == Python" but because we want to be intentional and careful when adding things to the default global namespace.

I completely agree with this, I don't know why you think we are in disagreement here 😅.
When talking about pyscript design choices, I never said `if it's different that normal python it's bad`; usually when we disagree is because I think that that specific feature is bad per se.

Resolving patch:
diff --git a/examples/pylist.py b/examples/pylist.py
--- a/examples/pylist.py
+++ b/examples/pylist.py
@@ -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 @@ def on_click(self, evt=None):
         self.select("input").element.checked = self.data["done"]
 
 
-class PyList(PyListTemplate):
+class PyList(pyscript.PyListTemplate):
     item_class = PyItem
 
     def add(self, item):
diff --git a/pyscriptjs/src/python/pyscript.py b/pyscriptjs/src/python/pyscript.py
--- a/pyscriptjs/src/python/pyscript.py
+++ b/pyscriptjs/src/python/pyscript.py
@@ -7,8 +7,7 @@
 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
@@ -53,6 +52,41 @@ def identity(value, meta):
 }
 
 
+# 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
@@ -111,7 +145,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)
@@ -123,57 +157,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):
@@ -216,7 +215,7 @@ def id(self):
     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
@@ -233,7 +232,7 @@ def write(self, value, append=False):
             return
 
         if append:
-            child = document.createElement("div")
+            child = js.document.createElement("div")
             self.element.appendChild(child)
 
         if self.element.children:
@@ -242,7 +241,7 @@ def write(self, value, append=False):
             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
@@ -263,7 +262,7 @@ def select(self, query, from_content=False):
         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:
@@ -303,7 +302,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)
@@ -417,7 +416,7 @@ def foo(evt):
             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:
@@ -487,7 +486,7 @@ def init(self, app):
 
     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
@@ -497,4 +496,158 @@ def wrapper(class_):
         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)


Test patch:
diff --git a/pyscriptjs/tests/integration/support.py b/pyscriptjs/tests/integration/support.py
--- a/pyscriptjs/tests/integration/support.py
+++ b/pyscriptjs/tests/integration/support.py
@@ -291,6 +291,17 @@ def iter_locator(self, loc):
         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 ==============
 
diff --git a/pyscriptjs/tests/integration/test_01_basic.py b/pyscriptjs/tests/integration/test_01_basic.py
--- a/pyscriptjs/tests/integration/test_01_basic.py
+++ b/pyscriptjs/tests/integration/test_01_basic.py
@@ -210,8 +210,8 @@ def test_python_version(self):
             """
         <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>
         """
         )
@@ -228,27 +228,47 @@ def test_python_version(self):
             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"
-        )
-        # 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
+        with pytest.raises(AssertionError, match="Found 2 alert banners"):
+            self.assert_no_banners()
+
+    def test_deprecated_globals(self):
+        self.pyscript_run(
+            """
+            <py-script>
+                # trigger various warnings
+                create("div", classes="a b c")
+                assert sys.__name__ == 'sys'
+                dedent("")
+                format_mime("")
+                assert MIME_RENDERERS['text/html'] is not None
+                console.log("hello")
+                PyScript.loop
+            </py-script>
+
+            <div id="mydiv"></div>
+            """
+        )
+        banner = self.page.locator(".py-warning")
+        messages = banner.all_inner_texts()
+        assert messages == [
+            "The PyScript object is deprecated. Please use pyscript instead.",
+            "Direct usage of console is deprecated. Please use js.console instead.",
+            "MIME_RENDERERS is deprecated. This is a private implementation detail of pyscript. You should not use it.",  # noqa: E501
+            "format_mime is deprecated. This is a private implementation detail of pyscript. You should not use it.",  # noqa: E501
+            "Direct usage of dedent is deprecated. Please use from textwrap import dedent instead.",
+            "Direct usage of sys is deprecated. Please use import sys instead.",
+            "Direct usage of create is deprecated. Please use pyscript.create instead.",
+        ]
diff --git a/pyscriptjs/tests/integration/test_plugins.py b/pyscriptjs/tests/integration/test_plugins.py
--- a/pyscriptjs/tests/integration/test_plugins.py
+++ b/pyscriptjs/tests/integration/test_plugins.py
@@ -2,7 +2,8 @@
 
 # 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 @@ def connect(self):
 
 # 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 @@ def onUserError(self, config):
 
 # 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')
 
diff --git a/pyscriptjs/tests/integration/test_zz_examples.py b/pyscriptjs/tests/integration/test_zz_examples.py
--- a/pyscriptjs/tests/integration/test_zz_examples.py
+++ b/pyscriptjs/tests/integration/test_zz_examples.py
@@ -60,6 +60,7 @@ def test_hello_world(self):
         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 @@ def test_simple_clock(self):
                 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 @@ def test_altair(self):
         # 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 @@ def test_bokeh(self):
         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 @@ def test_bokeh_interactive(self):
         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 @@ def test_d3(self):
         # 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 @@ def test_folium(self):
         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 @@ def test_matplotlib(self):
         # 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 @@ def test_numpy_canvas_fractals(self):
         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 @@ def test_panel(self):
 
         # 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 @@ def test_panel_deckgl(self):
         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 @@ def test_panel_kmeans(self):
         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 @@ def test_panel_stream(self):
         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 @@ def test_repl(self):
         # 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 @@ def test_repl2(self):
         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 @@ def test_todo(self):
             '<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 @@ def test_todo_pylist(self):
         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 @@ def test_toga_freedom(self):
         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 @@ def test_webgl_raycaster_index(self):
         self.wait_for_pyscript()
         assert self.page.title() == "Raycaster"
         wait_for_render(self.page, "*", "<canvas.*?>")
+        self.assert_no_banners()
diff --git a/pyscriptjs/tests/py-unit/test_pyscript.py b/pyscriptjs/tests/py-unit/test_pyscript.py
--- a/pyscriptjs/tests/py-unit/test_pyscript.py
+++ b/pyscriptjs/tests/py-unit/test_pyscript.py
@@ -12,15 +12,16 @@ def test_id_is_correct(self):
 
     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 @@ async def foo():
 
 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