mirror of
https://github.com/pyscript/pyscript.git
synced 2026-02-19 16:00:42 -05:00
Better test support for Python Plugins (#1108)
* add plugins testing utils module * add plugins manager fixture and init plugins tests helper in conftest * add _custom_elements attribute to pyscript.Plugin to allow plugins to track the CE they register * add test for py_tutor * remove unrelated code from prims js script * ensure a Plugin always has the app attribute and improve tests * add tests for py_tutor create_code_section * implement PluginsManager reset and add teardown on plugins_manager fixture to clean it up after a test * add test to check if plugin has been registered * add docstrings to new tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add docstrings to plugins tester * add changes from main * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * lint * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add todo to add remaining PluginsManager lifecycle events Co-authored-by: Fabio Pliger <fpliger@anaconda.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -90,10 +90,6 @@ class PyTutor:
|
||||
# Add the JS file
|
||||
script = js.document.createElement("script")
|
||||
script.type = "text/javascript"
|
||||
try:
|
||||
script.appendChild(js.document.createTextNode(PAGE_SCRIPT))
|
||||
except BaseException:
|
||||
script.text = PAGE_SCRIPT
|
||||
script.src = "./assets/prism/prism.js"
|
||||
js.document.head.appendChild(script)
|
||||
|
||||
|
||||
@@ -494,11 +494,27 @@ class Plugin:
|
||||
name = self.__class__.__name__
|
||||
|
||||
self.name = name
|
||||
self._custom_elements = []
|
||||
self.app = None
|
||||
|
||||
def init(self, app):
|
||||
self.app = app
|
||||
|
||||
def register_custom_element(self, tag):
|
||||
"""
|
||||
Decorator to register a new custom element as part of a Plugin and associate
|
||||
tag to it. Internally, it delegates the registration to the PyScript internal
|
||||
[JS] plugin manager, who actually creates the JS custom element that can be
|
||||
attached to the page and instantiate an instance of the class passing the custom
|
||||
element to the plugin constructor.
|
||||
|
||||
Exammple:
|
||||
>> plugin = Plugin("PyTutorial")
|
||||
>> @plugin.register_custom_element("py-tutor")
|
||||
>> class PyTutor:
|
||||
>> def __init__(self, element):
|
||||
>> self.element = element
|
||||
"""
|
||||
# TODO: Ideally would be better to use the logger.
|
||||
js.console.info(f"Defining new custom element {tag}")
|
||||
|
||||
@@ -507,6 +523,7 @@ class Plugin:
|
||||
# until we have JS interface that works across interpreters
|
||||
define_custom_element(tag, create_proxy(class_)) # noqa: F821
|
||||
|
||||
self._custom_elements.append(tag)
|
||||
return create_proxy(wrapper)
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
# current working directory
|
||||
base_path = pathlib.Path().absolute()
|
||||
# add pyscript folder to path
|
||||
@@ -11,3 +13,16 @@ sys.path.append(str(python_source))
|
||||
# add Python plugins folder to path
|
||||
python_plugins_source = base_path / "src" / "plugins" / "python"
|
||||
sys.path.append(str(python_plugins_source))
|
||||
|
||||
# patch pyscript module where needed
|
||||
import pyscript # noqa: E402
|
||||
import pyscript_plugins_tester as ppt # noqa: E402
|
||||
|
||||
pyscript.define_custom_element = ppt.define_custom_element
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def plugins_manager():
|
||||
"""return a new instance of a Test version the PyScript application plugins manager"""
|
||||
yield ppt.plugins_manager # PluginsManager()
|
||||
ppt.plugins_manager.reset()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Mock module that emulates some of the pyodide js module features for the sake of tests"""
|
||||
from unittest.mock import Mock
|
||||
|
||||
create_proxy = Mock()
|
||||
create_proxy = Mock(side_effect=lambda x: x)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Mock module that emulates some of the pyodide js module features for the sake of tests"""
|
||||
from unittest.mock import Mock
|
||||
|
||||
create_proxy = Mock()
|
||||
create_proxy = Mock(side_effect=lambda x: x)
|
||||
|
||||
119
pyscriptjs/tests/py-unit/pyscript_plugins_tester.py
Normal file
119
pyscriptjs/tests/py-unit/pyscript_plugins_tester.py
Normal file
@@ -0,0 +1,119 @@
|
||||
import xml.dom
|
||||
from xml.dom.minidom import Node # nosec
|
||||
|
||||
import pyscript
|
||||
|
||||
|
||||
class classList:
|
||||
"""Class that (lightly) emulates the behaviour of HTML Nodes ClassList"""
|
||||
|
||||
def __init__(self):
|
||||
self._classes = []
|
||||
|
||||
def add(self, classname: str):
|
||||
"""Add classname to the classList"""
|
||||
self._classes.append(classname)
|
||||
|
||||
def remove(self, classname: str):
|
||||
"""Remove classname from the classList"""
|
||||
self._classes.remove(classname)
|
||||
|
||||
|
||||
class PluginsManager:
|
||||
"""
|
||||
Emulator of PyScript PluginsManager that can be used to simulate plugins lifecycle events
|
||||
|
||||
TODO: Currently missing most of the lifecycle events in PluginsManager implementation. Need
|
||||
to add more than just addPythonPlugin
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.plugins = []
|
||||
|
||||
# mapping containing all the custom elements createed by plugins
|
||||
self._custom_elements = {}
|
||||
|
||||
def addPythonPlugin(self, pluginInstance: pyscript.Plugin):
|
||||
"""
|
||||
Add a pluginInstance to the plugins managed by the PluginManager and calls
|
||||
pluginInstance.init(self) to initialized the plugin with the manager
|
||||
"""
|
||||
pluginInstance.init(self)
|
||||
self.plugins.append(pluginInstance)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Unregister all plugins and related custom elements.
|
||||
"""
|
||||
for plugin in self.plugins:
|
||||
plugin.app = None
|
||||
|
||||
self.plugins = []
|
||||
self._custom_elements = {}
|
||||
|
||||
|
||||
class CustomElement:
|
||||
def __init__(self, plugin_class: pyscript.Plugin):
|
||||
self.pyPluginInstance = plugin_class(self)
|
||||
self.attributes = {}
|
||||
self.innerHTML = ""
|
||||
|
||||
def connectedCallback(self):
|
||||
return self.pyPluginInstance.connect()
|
||||
|
||||
def getAttribute(self, attr: str):
|
||||
return self.attributes.get(attr)
|
||||
|
||||
|
||||
def define_custom_element(tag, plugin_class: pyscript.Plugin):
|
||||
"""
|
||||
Mock method to emulate the behaviour of the PyScript `define_custom_element`
|
||||
that basically creates a new CustomElement passing plugin_class as Python
|
||||
proxy object. For more info check out the logic of the original implementation at:
|
||||
|
||||
src/plugin.ts:define_custom_element
|
||||
"""
|
||||
ce = CustomElement(plugin_class)
|
||||
plugins_manager._custom_elements[tag] = ce
|
||||
|
||||
|
||||
plugins_manager = PluginsManager()
|
||||
|
||||
# Init pyscript testing mocks
|
||||
impl = xml.dom.getDOMImplementation()
|
||||
|
||||
|
||||
class Node:
|
||||
"""
|
||||
Represent an HTML Node.
|
||||
|
||||
This classes us an abstraction on top of xml.dom.minidom.Node
|
||||
"""
|
||||
|
||||
def __init__(self, el: Node):
|
||||
self._el = el
|
||||
self.classList = classList()
|
||||
|
||||
# Automatic delegation is a simple and short boilerplate:
|
||||
def __getattr__(self, attr: str):
|
||||
return getattr(self._el, attr)
|
||||
|
||||
def createElement(self, *args, **kws):
|
||||
newEl = self._el.createElement(*args, **kws)
|
||||
return Node(newEl)
|
||||
|
||||
|
||||
class Document(Node):
|
||||
"""
|
||||
Represent an HTML Document.
|
||||
|
||||
This classes us an abstraction on top of xml.dom.minidom.Document
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._el = impl.createDocument(None, "document", None)
|
||||
|
||||
|
||||
pyscript.js.document = doc = Document()
|
||||
pyscript.js.document.head = doc.createElement("head")
|
||||
pyscript.js.document.body = doc.createElement("body")
|
||||
@@ -1,6 +1,32 @@
|
||||
import html
|
||||
from unittest.mock import Mock
|
||||
|
||||
import py_markdown
|
||||
import py_tutor
|
||||
import pyscript
|
||||
import pyscript_plugins_tester as ppt
|
||||
|
||||
TUTOR_SOURCE = """
|
||||
<py-config>
|
||||
packages = [
|
||||
"folium",
|
||||
"pandas"
|
||||
]
|
||||
plugins = [
|
||||
"../build/plugins/python/py_tutor.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
import folium
|
||||
import json
|
||||
import pandas as pd
|
||||
|
||||
from pyodide.http import open_url
|
||||
|
||||
# the rest of the code goes one
|
||||
</py-script>
|
||||
"""
|
||||
|
||||
|
||||
class TestPyMarkdown:
|
||||
@@ -15,3 +41,129 @@ class TestPyMarkdown:
|
||||
|
||||
py_markdown.plugin.afterStartup(interpreter)
|
||||
console_mock.log.assert_called_with("interpreter received: just an interpreter")
|
||||
|
||||
|
||||
class TestPyTutor:
|
||||
def check_prism_added(self):
|
||||
"""
|
||||
Assert that the add_prism method has been correctly executed and the
|
||||
related prism assets have been added to the page head
|
||||
"""
|
||||
# GIVEN a previous call to py_tutor.plugin.append_script_to_page
|
||||
head = pyscript.js.document.head
|
||||
|
||||
# EXPECT the head to contain a link element pointing to the prism.css
|
||||
links = head.getElementsByTagName("link")
|
||||
assert len(links) == 1
|
||||
link = links[0]
|
||||
assert link.type == "text/css"
|
||||
assert link.rel == "stylesheet"
|
||||
assert link.href == "./assets/prism/prism.css"
|
||||
|
||||
# EXPECT the head to contain a script src == prism.js
|
||||
scripts = head.getElementsByTagName("script")
|
||||
assert len(scripts) == 1
|
||||
script = scripts[0]
|
||||
assert script.type == "text/javascript"
|
||||
assert script.src == "./assets/prism/prism.js"
|
||||
|
||||
def check_append_script_to_page(self):
|
||||
"""
|
||||
Assert that the append_script_to_page has been correctly executed and the
|
||||
py_tutor.PAGE_SCRIPT code needed for the plugin JS animation has been added
|
||||
to the page body
|
||||
"""
|
||||
# GIVEN a previous call to py_tutor.plugin.append_script_to_page
|
||||
body = pyscript.js.document.body
|
||||
|
||||
# EXPECT the body of the page to contain a script of type text/javascript
|
||||
# and that contains the py_tutor.PAGE_SCRIPT script
|
||||
scripts = body.getElementsByTagName("script")
|
||||
assert len(scripts) == 1
|
||||
script = scripts[0]
|
||||
assert script.type == "text/javascript"
|
||||
|
||||
# Check the actual JS script code
|
||||
# To do so we have 2 methods (it depends on browser support so we check either...)
|
||||
if script.childNodes:
|
||||
# in this case it means the content has been added as a child element
|
||||
node = script.childNodes[0]
|
||||
assert node.data == py_tutor.PAGE_SCRIPT
|
||||
else:
|
||||
assert script.text == py_tutor.PAGE_SCRIPT
|
||||
|
||||
def check_create_code_section(self):
|
||||
"""
|
||||
Assert that the create_code_section has been correctly executed and the
|
||||
related code section has been created and added to the page.
|
||||
"""
|
||||
# GIVEN a previous call to py_tutor.plugin.check_create_code_section
|
||||
console = py_tutor.js.console
|
||||
|
||||
# EXPECT the console to have the messages printed by the plugin while
|
||||
# executing the plugin operations
|
||||
console.info.assert_any_call("Creating code introspection section.")
|
||||
console.info.assert_any_call("Creating new code section element.")
|
||||
|
||||
# EXPECT the page body to contain a section with the input source code
|
||||
body = pyscript.js.document.body
|
||||
sections = body.getElementsByTagName("section")
|
||||
section = sections[0]
|
||||
assert "code" in section.classList._classes
|
||||
section_innerHTML = py_tutor.TEMPLATE_CODE_SECTION.format(
|
||||
source=html.escape(TUTOR_SOURCE), modules_section=""
|
||||
)
|
||||
assert html.escape(TUTOR_SOURCE) in section.innerHTML
|
||||
assert section.innerHTML == section_innerHTML
|
||||
|
||||
def test_connected_calls(self, plugins_manager: ppt.PluginsManager):
|
||||
"""
|
||||
Test that all parts of the plugin have been added to the page body and head
|
||||
properly. This test effectively calls `self.check_prism_added`,
|
||||
`self.check_append_script_to_page` and `check_create_code_section` assert
|
||||
the new nodes have been added properly.
|
||||
"""
|
||||
# GIVEN THAT we add the plugin to the app plugin manager
|
||||
# this will:
|
||||
# - init the plugin instance passing the plugins_manager as parent app
|
||||
# - add the plugin instance to plugins_manager.plugins
|
||||
assert not py_tutor.plugin.app
|
||||
plugins_manager.addPythonPlugin(py_tutor.plugin)
|
||||
|
||||
# EXPECT: the plugin app to now be the plugin manager
|
||||
assert py_tutor.plugin.app == plugins_manager
|
||||
tutor_ce = plugins_manager._custom_elements["py-tutor"]
|
||||
# tutor_ce_python_instance = tutor_ce.pyPluginInstance
|
||||
# GIVEN: The following innerHTML on the ce elements
|
||||
tutor_ce.innerHTML = TUTOR_SOURCE
|
||||
|
||||
# GIVEN: the CustomElement connectedCallback gets called
|
||||
tutor_ce.connectedCallback()
|
||||
|
||||
# EXPECT: the
|
||||
self.check_prism_added()
|
||||
|
||||
self.check_append_script_to_page()
|
||||
|
||||
self.check_create_code_section()
|
||||
|
||||
def test_plugin_registered(self, plugins_manager: ppt.PluginsManager):
|
||||
"""
|
||||
Test that, when registered, plugin actually has an app attribute set
|
||||
and that it's present in plugins manager plugins list.
|
||||
"""
|
||||
# EXPECT py_tutor.plugin to not have any app associate
|
||||
assert not py_tutor.plugin.app
|
||||
|
||||
# EXPECT: the plugin manager to not have any plugin registered
|
||||
assert not plugins_manager.plugins
|
||||
|
||||
# GIVEN THAT we add the plugin to the app plugin manager
|
||||
plugins_manager.addPythonPlugin(py_tutor.plugin)
|
||||
|
||||
# EXPECT: the plugin app to now be the plugin manager
|
||||
assert py_tutor.plugin.app == plugins_manager
|
||||
assert "py-tutor" in py_tutor.plugin._custom_elements
|
||||
|
||||
# EXPECT: the pytutor.plugin manager to be part of
|
||||
assert py_tutor.plugin in plugins_manager.plugins
|
||||
|
||||
Reference in New Issue
Block a user