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:
Fabio Pliger
2023-01-24 15:32:16 -06:00
committed by GitHub
parent 0de8cd9ab7
commit d55340a817
7 changed files with 305 additions and 6 deletions

View File

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

View File

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

View File

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

View 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")

View File

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