Move pyodide to a web worker (#1333)

This PR adds support for optionally running pyodide in a web worker:

- add a new option config.execution_thread, which can be `main` or `worker`. The default is `main`

- improve the test machinery so that we run all tests twice, once for `main` and once for `worker`

- add a new esbuild target which builds the code for the worker

The support for workers is not complete and many features are still missing: there are 71 tests which are marked as `@skip_worker`, but we can fix them in subsequent PRs.

The vast majority of tests fail because js.document is unavailable: for it to run transparently, we need the "auto-syncify" feature of synclink.


Co-authored-by: Hood Chatham <roberthoodchatham@gmail.com>
Co-authored-by: Madhur Tandon <20173739+madhur-tandon@users.noreply.github.com>
This commit is contained in:
Antonio Cuni
2023-04-14 10:55:31 +02:00
committed by GitHub
parent dfa116eb70
commit 8c5475f78f
28 changed files with 497 additions and 99 deletions

View File

@@ -26,6 +26,10 @@ Features
### Plugins
- Plugins may now implement the `beforePyReplExec()` and `afterPyReplExec()` hooks, which are called immediately before and after code in a `py-repl` tag is executed. ([#1106](https://github.com/pyscript/pyscript/pull/1106))
### Web worker support
- introduced the new experimental `execution_thread` config option: if you set `execution_thread = "worker"`, the python interpreter runs inside a web worker
- worker support is still **very** experimental: not everything works, use it at your own risk
Bug fixes
---------

View File

@@ -190,6 +190,16 @@ $ pytest test_01_basic.py -k test_pyscript_hello -s --dev
`--dev` implies `--headed --no-fake-server`. In addition, it also
automatically open chrome dev tools.
#### To run only main thread or worker tests
By default, we run each test twice: one with `execution_thread = "main"` and
one with `execution_thread = "worker"`. If you want to run only half of them,
you can use `-m`:
```
$ pytest -m main # run only the tests in the main thread
$ pytest -m worker # ron only the tests in the web worker
```
## Fake server, HTTP cache

View File

@@ -16,20 +16,18 @@ module.exports = {
browser: true,
},
plugins: ['@typescript-eslint'],
ignorePatterns: ['node_modules'],
ignorePatterns: ['node_modules', 'src/interpreter_worker/*'],
rules: {
// ts-ignore is already an explicit override, no need to have a second lint
'@typescript-eslint/ban-ts-comment': 'off',
// any-related lints
// These two come up a lot, so they probably aren't worth it
// any related lints
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
// encourage people to cast "any" to a more specific type before using it
'@typescript-eslint/no-unsafe-call': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-argument': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
// other rules
'no-prototype-builtins': 'error',

View File

@@ -59,7 +59,7 @@ build:
npm run build
build-fast:
node esbuild.js
node esbuild.mjs
# use the following rule to do all the checks done by precommit: in
# particular, use this if you want to run eslint.

View File

@@ -14,6 +14,7 @@ dependencies:
- pillow
- numpy
- markdown
- toml
- pip:
- playwright
- pytest-playwright

View File

@@ -61,6 +61,14 @@ const pyScriptConfig = {
plugins: [bundlePyscriptPythonPlugin()],
};
const interpreterWorkerConfig = {
entryPoints: ['src/interpreter_worker/worker.ts'],
loader: { '.py': 'text' },
bundle: true,
format: 'iife',
plugins: [bundlePyscriptPythonPlugin()],
};
const copyPath = (source, dest, ...rest) => cp(join(__dirname, source), join(__dirname, dest), ...rest);
const esbuild = async () => {
@@ -80,6 +88,14 @@ const esbuild = async () => {
minify: true,
outfile: 'build/pyscript.min.js',
}),
// XXX I suppose we should also build a minified version
// TODO (HC): Simplify config a bit
build({
...interpreterWorkerConfig,
sourcemap: false,
minify: false,
outfile: 'build/interpreter_worker.js',
}),
]);
const copy = [];

View File

@@ -40,7 +40,6 @@ export class InterpreterClient extends Object {
*/
async initializeRemote(): Promise<void> {
await this._remote.loadInterpreter(this.config, Synclink.proxy(this.stdio));
// await this._remote.loadInterpreter(this.config, Synclink.proxy(this.stdio));
this.globals = this._remote.globals;
}

View File

@@ -0,0 +1,26 @@
// XXX: what about code duplications?
// With the current build configuration, the code for logger,
// remote_interpreter and everything which is included from there is
// bundled/fetched/executed twice, once in pyscript.js and once in
// worker_interpreter.js.
import { getLogger } from '../logger';
import { RemoteInterpreter } from '../remote_interpreter';
import * as Synclink from 'synclink';
const logger = getLogger('worker');
logger.info('Interpreter worker starting...');
async function worker_initialize(cfg) {
const remote_interpreter = new RemoteInterpreter(cfg.src);
// this is the equivalent of await import(interpreterURL)
logger.info(`Downloading ${cfg.name}...`); // XXX we should use logStatus
importScripts(cfg.src);
logger.info('worker_initialize() complete');
return Synclink.proxy(remote_interpreter);
}
Synclink.expose(worker_initialize);
export type { worker_initialize };

View File

@@ -1,7 +1,7 @@
import './styles/pyscript_base.css';
import { loadConfigFromElement } from './pyconfig';
import type { AppConfig } from './pyconfig';
import type { AppConfig, InterpreterConfig } from './pyconfig';
import { InterpreterClient } from './interpreter_client';
import { PluginManager, Plugin, PythonPlugin } from './plugin';
import { make_PyScript, initHandlers, mountElements } from './components/pyscript';
@@ -59,16 +59,6 @@ throwHandler.serialize = new_error_transfer_handler;
user scripts
8. initialize the rest of web components such as py-button, py-repl, etc.
More concretely:
- Points 1-4 are implemented sequentially in PyScriptApp.main().
- PyScriptApp.loadInterpreter adds a <script> tag to the document to initiate
the download, and then adds an event listener for the 'load' event, which
in turns calls PyScriptApp.afterInterpreterLoad().
- PyScriptApp.afterInterpreterLoad() implements all the points >= 5.
*/
export let interpreter;
@@ -173,6 +163,52 @@ export class PyScriptApp {
logger.info('config loaded:\n' + JSON.stringify(this.config, null, 2));
}
_get_base_url(): string {
// Note that this requires that pyscript is loaded via a <script>
// tag. If we want to allow loading via an ES6 module in the future,
// we need to think about some other strategy
const elem = document.currentScript as HTMLScriptElement;
const slash = elem.src.lastIndexOf('/');
return elem.src.slice(0, slash);
}
async _startInterpreter_main(interpreter_cfg: InterpreterConfig) {
logger.info('Starting the interpreter in the main thread');
// this is basically equivalent to worker_initialize()
const remote_interpreter = new RemoteInterpreter(interpreter_cfg.src);
const { port1, port2 } = new Synclink.FakeMessageChannel() as unknown as MessageChannel;
port1.start();
port2.start();
Synclink.expose(remote_interpreter, port2);
const wrapped_remote_interpreter = Synclink.wrap(port1);
this.logStatus(`Downloading ${interpreter_cfg.name}...`);
/* Dynamically download and import pyodide: the import() puts a
loadPyodide() function into globalThis, which is later called by
RemoteInterpreter.
This is suboptimal: ideally, we would like to import() a module
which exports loadPyodide(), but this plays badly with workers
because at the moment of writing (2023-03-24) Firefox does not
support ES modules in workers:
https://caniuse.com/mdn-api_worker_worker_ecmascript_modules
*/
const interpreterURL = interpreter_cfg.src;
await import(interpreterURL);
return { remote_interpreter, wrapped_remote_interpreter };
}
async _startInterpreter_worker(interpreter_cfg: InterpreterConfig) {
logger.warn('execution_thread = "worker" is still VERY experimental, use it at your own risk');
logger.info('Starting the interpreter in a web worker');
const base_url = this._get_base_url();
const worker = new Worker(base_url + '/interpreter_worker.js');
const worker_initialize: any = Synclink.wrap(worker);
const wrapped_remote_interpreter = await worker_initialize(interpreter_cfg);
const remote_interpreter = undefined; // this is _unwrapped_remote
return { remote_interpreter, wrapped_remote_interpreter };
}
// lifecycle (4)
async loadInterpreter() {
logger.info('Initializing interpreter');
@@ -184,35 +220,21 @@ export class PyScriptApp {
showWarning('Multiple interpreters are not supported yet.<br />Only the first will be used', 'html');
}
const interpreter_cfg = this.config.interpreters[0];
const cfg = this.config.interpreters[0];
let x;
if (this.config.execution_thread == 'worker') {
x = await this._startInterpreter_worker(cfg);
} else {
x = await this._startInterpreter_main(cfg);
}
const { remote_interpreter, wrapped_remote_interpreter } = x;
const remote_interpreter = new RemoteInterpreter(interpreter_cfg.src);
const { port1, port2 } = new Synclink.FakeMessageChannel() as unknown as MessageChannel;
port1.start();
port2.start();
Synclink.expose(remote_interpreter, port2);
const wrapped_remote_interpreter = Synclink.wrap(port1);
this.interpreter = new InterpreterClient(
this.config,
this._stdioMultiplexer,
wrapped_remote_interpreter as Synclink.Remote<RemoteInterpreter>,
remote_interpreter,
);
this.logStatus(`Downloading ${interpreter_cfg.name}...`);
/* Dynamically download and import pyodide: the import() puts a
loadPyodide() function into globalThis, which is later called by
RemoteInterpreter.
This is suboptimal: ideally, we would like to import() a module
which exports loadPyodide(), but this plays badly with workers
because at the moment of writing (2023-03-24) Firefox does not
support ES modules in workers:
https://caniuse.com/mdn-api_worker_worker_ecmascript_modules
*/
const interpreterURL = await this.interpreter._remote.src;
await import(interpreterURL);
await this.afterInterpreterLoad(this.interpreter);
}

View File

@@ -22,6 +22,7 @@ export interface AppConfig extends Record<string, any> {
fetch?: FetchConfig[];
plugins?: string[];
pyscript?: PyScriptMetadata;
execution_thread?: string; // "main" or "worker"
}
export type FetchConfig = {
@@ -43,7 +44,7 @@ export type PyScriptMetadata = {
};
const allKeys = Object.entries({
string: ['name', 'description', 'version', 'type', 'author_name', 'author_email', 'license'],
string: ['name', 'description', 'version', 'type', 'author_name', 'author_email', 'license', 'execution_thread'],
number: ['schema_version'],
array: ['runtimes', 'interpreters', 'packages', 'fetch', 'plugins'],
});
@@ -63,6 +64,7 @@ export const defaultConfig: AppConfig = {
packages: [],
fetch: [],
plugins: [],
execution_thread: 'main',
};
export function loadConfigFromElement(el: Element): AppConfig {
@@ -237,6 +239,15 @@ function validateConfig(configText: string, configType = 'toml') {
}
finalConfig[item].push(eachFetchConfig);
});
} else if (item == 'execution_thread') {
const value = config[item];
if (value !== 'main' && value !== 'worker') {
throw new UserError(
ErrorCode.BAD_CONFIG,
`"${value}" is not a valid value for the property "execution_thread". The only valid values are "main" and "worker"`,
);
}
finalConfig[item] = value;
} else {
finalConfig[item] = config[item];
}

View File

@@ -1,7 +1,7 @@
import time
from textwrap import dedent
from js import console, document
import js
from . import _internal
from ._mime import format_mime as _format_mime
@@ -22,7 +22,7 @@ class HTML:
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(
js.console.warn(
dedent(
"""PyScript Deprecation Warning: PyScript.write is
marked as deprecated and will be removed sometime soon. Please, use
@@ -55,7 +55,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
@@ -72,7 +72,7 @@ class Element:
return
if append:
child = document.createElement("div")
child = js.document.createElement("div")
self.element.appendChild(child)
if append and self.element.children:
@@ -81,7 +81,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
@@ -102,7 +102,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:
@@ -142,7 +142,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)
@@ -256,7 +256,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:

View File

@@ -105,7 +105,6 @@ export class RemoteInterpreter extends Object {
this.interface = Synclink.proxy(
await loadPyodide({
stdout: (msg: string) => {
// TODO: add syncify when moved to worker
stdio.stdout_writeline(msg).syncify();
},
stderr: (msg: string) => {
@@ -114,6 +113,8 @@ export class RemoteInterpreter extends Object {
fullStdLib: false,
}),
);
this.interface.registerComlink(Synclink);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.FS = this.interface.FS;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
this.PATH = (this.interface as any)._module.PATH;

View File

@@ -1,4 +1,5 @@
import dataclasses
import functools
import math
import os
import pdb
@@ -11,13 +12,100 @@ from dataclasses import dataclass
import py
import pytest
import toml
from playwright.sync_api import Error as PlaywrightError
ROOT = py.path.local(__file__).dirpath("..", "..", "..")
BUILD = ROOT.join("pyscriptjs", "build")
def params_with_marks(params):
"""
Small helper to automatically apply to each param a pytest.mark with the
same name of the param itself. E.g.:
params_with_marks(['aaa', 'bbb'])
is equivalent to:
[pytest.param('aaa', marks=pytest.mark.aaa),
pytest.param('bbb', marks=pytest.mark.bbb)]
This makes it possible to use 'pytest -m aaa' to run ONLY the tests which
uses the param 'aaa'.
"""
return [pytest.param(name, marks=getattr(pytest.mark, name)) for name in params]
def with_execution_thread(*values):
"""
Class decorator to override config.execution_thread.
By default, we run each test twice:
- execution_thread = 'main'
- execution_thread = 'worker'
If you want to execute certain tests with only one specific values of
execution_thread, you can use this class decorator. For example:
@with_execution_thread('main')
class TestOnlyMainThread:
...
@with_execution_thread('worker')
class TestOnlyWorker:
...
If you use @with_execution_thread(None), the logic to inject the
execution_thread config is disabled.
"""
if values == (None,):
@pytest.fixture
def execution_thread(self, request):
return None
else:
for value in values:
assert value in ("main", "worker")
@pytest.fixture(params=params_with_marks(values))
def execution_thread(self, request):
return request.param
def with_execution_thread_decorator(cls):
cls.execution_thread = execution_thread
return cls
return with_execution_thread_decorator
def skip_worker(reason):
"""
Decorator to skip a test if self.execution_thread == 'worker'
"""
if callable(reason):
# this happens if you use @skip_worker instead of @skip_worker("bla bla bla")
raise Exception(
"You need to specify a reason for skipping, "
"please use: @skip_worker('...')"
)
def decorator(fn):
@functools.wraps(fn)
def decorated(self, *args):
if self.execution_thread == "worker":
pytest.skip(reason)
return fn(self, *args)
return decorated
return decorator
@pytest.mark.usefixtures("init")
@with_execution_thread("main", "worker")
class PyScriptTest:
"""
Base class to write PyScript integration tests, based on playwright.
@@ -47,7 +135,7 @@ class PyScriptTest:
"""
@pytest.fixture()
def init(self, request, tmpdir, logger, page):
def init(self, request, tmpdir, logger, page, execution_thread):
"""
Fixture to automatically initialize all the tests in this class and its
subclasses.
@@ -69,6 +157,7 @@ class PyScriptTest:
tmpdir.join("build").mksymlinkto(BUILD)
self.tmpdir.chdir()
self.logger = logger
self.execution_thread = execution_thread
if request.config.option.no_fake_server:
# use a real HTTP server. Note that as soon as we request the
@@ -78,7 +167,7 @@ class PyScriptTest:
self.is_fake_server = False
else:
# use the internal playwright routing
self.http_server = "http://fake_server"
self.http_server = "https://fake_server"
self.router = SmartRouter(
"fake_server",
cache=request.config.cache,
@@ -215,12 +304,16 @@ class PyScriptTest:
url = f"{self.http_server}/{path}"
self.page.goto(url, timeout=0)
def wait_for_console(self, text, *, timeout=None, check_js_errors=True):
def wait_for_console(
self, text, *, match_substring=False, timeout=None, check_js_errors=True
):
"""
Wait until the given message appear in the console. If the message was
already printed in the console, return immediately.
Note: it must be the *exact* string as printed by e.g. console.log.
By default "text" must be the *exact* string as printed by a single
call to e.g. console.log. If match_substring is True, it is enough
that the console contains the given text anywhere.
timeout is expressed in milliseconds. If it's None, it will use
the same default as playwright, which is 30 seconds.
@@ -230,6 +323,16 @@ class PyScriptTest:
Return the elapsed time in ms.
"""
if match_substring:
def find_text():
return text in self.console.all.text
else:
def find_text():
return text in self.console.all.lines
if timeout is None:
timeout = 30 * 1000
# NOTE: we cannot use playwright's own page.expect_console_message(),
@@ -242,7 +345,7 @@ class PyScriptTest:
if elapsed_ms > timeout:
raise TimeoutError(f"{elapsed_ms:.2f} ms")
#
if text in self.console.all.lines:
if find_text():
# found it!
return elapsed_ms
#
@@ -279,6 +382,69 @@ class PyScriptTest:
# events aren't being triggered in the tests.
self.page.wait_for_timeout(100)
def _parse_py_config(self, doc):
configs = re.findall("<py-config>(.*?)</py-config>", doc, flags=re.DOTALL)
configs = [cfg.strip() for cfg in configs]
if len(configs) == 0:
return None
elif len(configs) == 1:
return toml.loads(configs[0])
else:
raise AssertionError("Too many <py-config>")
def _inject_execution_thread_config(self, snippet, execution_thread):
"""
If snippet already contains a py-config, let's try to inject
execution_thread automatically. Note that this works only for plain
<py-config> with inline config: type="json" and src="..." are not
supported by this logic, which should remain simple.
"""
cfg = self._parse_py_config(snippet)
if cfg is None:
# we don't have any <py-config>, let's add one
py_config_maybe = f"""
<py-config>
execution_thread = "{execution_thread}"
</py-config>
"""
else:
cfg["execution_thread"] = execution_thread
dumped_cfg = toml.dumps(cfg)
new_py_config = f"""
<py-config>
{dumped_cfg}
</py-config>
"""
snippet = re.sub(
"<py-config>.*</py-config>", new_py_config, snippet, flags=re.DOTALL
)
# no need for extra config, it's already in the snippet
py_config_maybe = ""
#
return snippet, py_config_maybe
def _pyscript_format(self, snippet, *, execution_thread, extra_head=""):
if execution_thread is None:
py_config_maybe = ""
else:
snippet, py_config_maybe = self._inject_execution_thread_config(
snippet, execution_thread
)
doc = f"""
<html>
<head>
<link rel="stylesheet" href="{self.http_server}/build/pyscript.css" />
<script defer src="{self.http_server}/build/pyscript.js"></script>
{extra_head}
</head>
<body>
{py_config_maybe}
{snippet}
</body>
</html>
"""
return doc
def pyscript_run(
self, snippet, *, extra_head="", wait_for_pyscript=True, timeout=None
):
@@ -295,18 +461,9 @@ class PyScriptTest:
- open a playwright page for it
- wait until pyscript has been fully loaded
"""
doc = f"""
<html>
<head>
<link rel="stylesheet" href="{self.http_server}/build/pyscript.css" />
<script defer src="{self.http_server}/build/pyscript.js"></script>
{extra_head}
</head>
<body>
{snippet}
</body>
</html>
"""
doc = self._pyscript_format(
snippet, execution_thread=self.execution_thread, extra_head=extra_head
)
if not wait_for_pyscript and timeout is not None:
raise ValueError("Cannot set a timeout if wait_for_pyscript=False")
filename = f"{self.testname}.html"
@@ -746,7 +903,11 @@ class SmartRouter:
assert url.path[0] == "/"
relative_path = url.path[1:]
if os.path.exists(relative_path):
route.fulfill(status=200, path=relative_path)
headers = {
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Opener-Policy": "same-origin",
}
route.fulfill(status=200, headers=headers, path=relative_path)
else:
route.fulfill(status=404)
return

View File

@@ -3,9 +3,10 @@ import textwrap
import pytest
from .support import JsErrors, JsErrorsDidNotRaise, PyScriptTest
from .support import JsErrors, JsErrorsDidNotRaise, PyScriptTest, with_execution_thread
@with_execution_thread(None)
class TestSupport(PyScriptTest):
"""
These are NOT tests about PyScript.
@@ -183,9 +184,9 @@ class TestSupport(PyScriptTest):
"""
JS errors found: 2
Error: error 1
at http://fake_server/mytest.html:.*
at https://fake_server/mytest.html:.*
Error: error 2
at http://fake_server/mytest.html:.*
at https://fake_server/mytest.html:.*
"""
).strip()
assert re.search(expected, msg)
@@ -214,9 +215,9 @@ class TestSupport(PyScriptTest):
"""
JS errors found: 2
Error: NOT expected 2
at http://fake_server/mytest.html:.*
at https://fake_server/mytest.html:.*
Error: NOT expected 4
at http://fake_server/mytest.html:.*
at https://fake_server/mytest.html:.*
"""
).strip()
assert re.search(expected, msg)
@@ -243,9 +244,9 @@ class TestSupport(PyScriptTest):
---
The following JS errors were raised but not expected:
Error: error 1
at http://fake_server/mytest.html:.*
at https://fake_server/mytest.html:.*
Error: error 2
at http://fake_server/mytest.html:.*
at https://fake_server/mytest.html:.*
"""
).strip()
assert re.search(expected, msg)
@@ -391,6 +392,24 @@ class TestSupport(PyScriptTest):
# clear the errors, else the test fails at teardown
self.clear_js_errors()
def test_wait_for_console_match_substring(self):
doc = """
<html>
<body>
<script>
console.log('Foo Bar Baz');
</script>
</body>
</html>
"""
self.writefile("mytest.html", doc)
self.goto("mytest.html")
with pytest.raises(TimeoutError):
self.wait_for_console("Bar", timeout=200)
#
self.wait_for_console("Bar", timeout=200, match_substring=True)
assert self.console.log.lines[-1] == "Foo Bar Baz"
def test_iter_locator(self):
doc = """
<html>
@@ -427,7 +446,7 @@ class TestSupport(PyScriptTest):
self.router.clear_cache(URL)
self.goto("mytest.html")
assert self.router.requests == [
(200, "fake_server", "http://fake_server/mytest.html"),
(200, "fake_server", "https://fake_server/mytest.html"),
(200, "NETWORK", URL),
]
#
@@ -435,10 +454,10 @@ class TestSupport(PyScriptTest):
self.goto("mytest.html")
assert self.router.requests == [
# 1st visit
(200, "fake_server", "http://fake_server/mytest.html"),
(200, "fake_server", "https://fake_server/mytest.html"),
(200, "NETWORK", URL),
# 2nd visit
(200, "fake_server", "http://fake_server/mytest.html"),
(200, "fake_server", "https://fake_server/mytest.html"),
(200, "CACHED", URL),
]
@@ -450,3 +469,22 @@ class TestSupport(PyScriptTest):
assert [
"Failed to load resource: the server responded with a status of 404 (Not Found)"
] == self.console.all.lines
def test__pyscript_format_inject_execution_thread(self):
"""
This is slightly different than other tests: it doesn't use playwright, it
just tests that our own internal helper works
"""
doc = self._pyscript_format("<b>Hello</b>", execution_thread="main")
cfg = self._parse_py_config(doc)
assert cfg == {"execution_thread": "main"}
def test__pyscript_format_modify_existing_py_config(self):
src = """
<py-config>
hello = 42
</py-config>
"""
doc = self._pyscript_format(src, execution_thread="main")
cfg = self._parse_py_config(doc)
assert cfg == {"execution_thread": "main", "hello": 42}

View File

@@ -2,11 +2,37 @@ import re
import pytest
from .support import JsErrors, PyScriptTest
from .support import JsErrors, PyScriptTest, skip_worker
class TestBasic(PyScriptTest):
def test_pyscript_hello(self):
self.pyscript_run(
"""
<py-script>
import js
js.console.log('hello pyscript')
</py-script>
"""
)
assert self.console.log.lines == ["hello pyscript"]
def test_execution_thread(self):
self.pyscript_run(
"""
<!-- we don't really need anything here, we just want to check that
pyscript starts -->
"""
)
assert self.execution_thread in ("main", "worker")
if self.execution_thread == "main":
where = "the main thread"
elif self.execution_thread == "worker":
where = "a web worker"
expected = f"[pyscript/main] Starting the interpreter in {where}"
assert expected in self.console.info.lines
def test_print(self):
self.pyscript_run(
"""
<py-script>
@@ -53,6 +79,9 @@ class TestBasic(PyScriptTest):
)
self.page.locator("button").click()
self.wait_for_console(
"Exception: this is an error inside handler", match_substring=True
)
## error in console
tb_lines = self.console.error.lines[-1].splitlines()
@@ -120,6 +149,7 @@ class TestBasic(PyScriptTest):
"hello asciitree", # printed by us
]
@skip_worker("FIXME: the banner doesn't appear")
def test_non_existent_package(self):
self.pyscript_run(
"""
@@ -139,6 +169,7 @@ class TestBasic(PyScriptTest):
alert_banner = self.page.wait_for_selector(".alert-banner")
assert expected_alert_banner_msg in alert_banner.inner_text()
@skip_worker("FIXME: the banner doesn't appear")
def test_no_python_wheel(self):
self.pyscript_run(
"""
@@ -240,6 +271,7 @@ class TestBasic(PyScriptTest):
is not None
)
@skip_worker("FIXME: showWarning()")
def test_assert_no_banners(self):
"""
Test that the DOM doesn't contain error/warning banners

View File

@@ -7,10 +7,11 @@ import re
import numpy as np
from PIL import Image
from .support import PyScriptTest, wait_for_render
from .support import PyScriptTest, skip_worker, wait_for_render
class TestOutput(PyScriptTest):
class TestDisplay(PyScriptTest):
@skip_worker("FIXME: display()")
def test_simple_display(self):
self.pyscript_run(
"""
@@ -24,6 +25,7 @@ class TestOutput(PyScriptTest):
assert re.search(pattern, node_list[0].inner_html())
assert len(node_list) == 1
@skip_worker("FIXME: display()")
def test_consecutive_display(self):
self.pyscript_run(
"""
@@ -41,6 +43,7 @@ class TestOutput(PyScriptTest):
lines = [line for line in lines if line != ""] # remove empty lines
assert lines == ["hello 1", "hello 2", "hello 3"]
@skip_worker("FIXME: display()")
def test_target_attribute(self):
self.pyscript_run(
"""
@@ -53,6 +56,7 @@ class TestOutput(PyScriptTest):
mydiv = self.page.locator("#mydiv")
assert mydiv.inner_text() == "hello world"
@skip_worker("FIXME: display()")
def test_consecutive_display_target(self):
self.pyscript_run(
"""
@@ -73,6 +77,7 @@ class TestOutput(PyScriptTest):
lines = [line for line in lines if line != ""] # remove empty lines
assert lines == ["hello 1", "hello in between 1 and 2", "hello 2", "hello 3"]
@skip_worker("FIXME: display()")
def test_multiple_display_calls_same_tag(self):
self.pyscript_run(
"""
@@ -86,6 +91,7 @@ class TestOutput(PyScriptTest):
lines = tag.inner_text().splitlines()
assert lines == ["hello", "world"]
@skip_worker("FIXME: display()")
def test_implicit_target_from_a_different_tag(self):
self.pyscript_run(
"""
@@ -104,6 +110,7 @@ class TestOutput(PyScriptTest):
assert py1.inner_text() == ""
assert py2.inner_text() == "hello"
@skip_worker("FIXME: display()")
def test_no_implicit_target(self):
self.pyscript_run(
"""
@@ -129,6 +136,7 @@ class TestOutput(PyScriptTest):
text = self.page.text_content("body")
assert "hello world" not in text
@skip_worker("FIXME: display()")
def test_explicit_target_pyscript_tag(self):
self.pyscript_run(
"""
@@ -144,6 +152,7 @@ class TestOutput(PyScriptTest):
text = self.page.locator("id=second-pyscript-tag").inner_text()
assert text == "hello"
@skip_worker("FIXME: display()")
def test_explicit_target_on_button_tag(self):
self.pyscript_run(
"""
@@ -158,6 +167,7 @@ class TestOutput(PyScriptTest):
text = self.page.locator("id=my-button").inner_text()
assert "hello" in text
@skip_worker("FIXME: display()")
def test_explicit_different_target_from_call(self):
self.pyscript_run(
"""
@@ -176,6 +186,7 @@ class TestOutput(PyScriptTest):
text = self.page.locator("id=second-pyscript-tag").all_inner_texts()
assert "hello" in text
@skip_worker("FIXME: display()")
def test_append_true(self):
self.pyscript_run(
"""
@@ -189,6 +200,7 @@ class TestOutput(PyScriptTest):
assert re.search(pattern, node_list[0].inner_html())
assert len(node_list) == 1
@skip_worker("FIXME: display()")
def test_append_false(self):
self.pyscript_run(
"""
@@ -201,6 +213,7 @@ class TestOutput(PyScriptTest):
pattern = r'<py-script id="py-.*">hello world</py-script>'
assert re.search(pattern, inner_html)
@skip_worker("FIXME: display()")
def test_display_multiple_values(self):
self.pyscript_run(
"""
@@ -214,6 +227,7 @@ class TestOutput(PyScriptTest):
inner_text = self.page.inner_text("html")
assert inner_text == "hello\nworld"
@skip_worker("FIXME: display()")
def test_display_multiple_append_false(self):
self.pyscript_run(
"""
@@ -227,6 +241,7 @@ class TestOutput(PyScriptTest):
pattern = r'<py-script id="py-.*">world</py-script>'
assert re.search(pattern, inner_html)
@skip_worker("FIXME: display()")
def test_display_multiple_append_false_with_target(self):
self.pyscript_run(
"""
@@ -255,6 +270,7 @@ class TestOutput(PyScriptTest):
== '<svg height="20" width="20"><circle cx="10" cy="10" r="10" fill="red"></circle></svg>' # noqa: E501
)
@skip_worker("FIXME: display()")
def test_display_list_dict_tuple(self):
self.pyscript_run(
"""
@@ -273,6 +289,7 @@ class TestOutput(PyScriptTest):
== "['A', 1, '!']\n{'B': 2, 'List': ['A', 1, '!']}\n('C', 3, '!')"
)
@skip_worker("FIXME: display()")
def test_display_should_escape(self):
self.pyscript_run(
"""
@@ -285,6 +302,7 @@ class TestOutput(PyScriptTest):
assert out.inner_html() == html.escape("<p>hello world</p>")
assert out.inner_text() == "<p>hello world</p>"
@skip_worker("FIXME: display()")
def test_display_HTML(self):
self.pyscript_run(
"""
@@ -297,6 +315,7 @@ class TestOutput(PyScriptTest):
assert out.inner_html() == "<p>hello world</p>"
assert out.inner_text() == "hello world"
@skip_worker("FIXME: display()")
def test_image_display(self):
self.pyscript_run(
"""
@@ -325,6 +344,7 @@ class TestOutput(PyScriptTest):
assert deviation == 0.0
self.assert_no_banners()
@skip_worker("FIXME: display()")
def test_empty_HTML_and_console_output(self):
self.pyscript_run(
"""
@@ -342,6 +362,7 @@ class TestOutput(PyScriptTest):
assert "print from js" in console_text
assert "error from js" in console_text
@skip_worker("FIXME: display()")
def test_text_HTML_and_console_output(self):
self.pyscript_run(
"""
@@ -362,6 +383,7 @@ class TestOutput(PyScriptTest):
print(self.console.error.lines)
assert self.console.error.lines[-1] == "error from js"
@skip_worker("FIXME: display()")
def test_console_line_break(self):
self.pyscript_run(
"""
@@ -375,6 +397,7 @@ class TestOutput(PyScriptTest):
assert console_text.index("1print") == (console_text.index("2print") - 1)
assert console_text.index("1console") == (console_text.index("2console") - 1)
@skip_worker("FIXME: display()")
def test_image_renders_correctly(self):
"""This is just a sanity check to make sure that images are rendered correctly."""
buffer = io.BytesIO()

View File

@@ -1,4 +1,4 @@
from .support import PyScriptTest
from .support import PyScriptTest, skip_worker
class TestElement(PyScriptTest):
@@ -21,6 +21,7 @@ class TestElement(PyScriptTest):
py_terminal = self.page.wait_for_selector("py-terminal")
assert "foo" in py_terminal.inner_text()
@skip_worker("FIXME: js.document")
def test_element_value(self):
"""Test the element value"""
self.pyscript_run(
@@ -38,6 +39,7 @@ class TestElement(PyScriptTest):
py_terminal = self.page.wait_for_selector("py-terminal")
assert "bar" in py_terminal.inner_text()
@skip_worker("FIXME: js.document")
def test_element_innerHtml(self):
"""Test the element innerHtml"""
self.pyscript_run(
@@ -55,6 +57,7 @@ class TestElement(PyScriptTest):
py_terminal = self.page.wait_for_selector("py-terminal")
assert "bar" in py_terminal.inner_text()
@skip_worker("FIXME: js.document")
def test_element_write_no_append(self):
"""Test the element write"""
self.pyscript_run(
@@ -71,6 +74,7 @@ class TestElement(PyScriptTest):
div = self.page.wait_for_selector("#foo")
assert "World!" in div.inner_text()
@skip_worker("FIXME: js.document")
def test_element_write_append(self):
"""Test the element write"""
self.pyscript_run(
@@ -90,6 +94,7 @@ class TestElement(PyScriptTest):
# confirm that the second write was appended
assert "Hello!<div>World!</div>" in parent_div.inner_html()
@skip_worker("FIXME: js.document")
def test_element_clear_div(self):
"""Test the element clear"""
self.pyscript_run(
@@ -105,6 +110,7 @@ class TestElement(PyScriptTest):
div = self.page.locator("#foo")
assert div.inner_text() == ""
@skip_worker("FIXME: js.document")
def test_element_clear_input(self):
"""Test the element clear"""
self.pyscript_run(
@@ -120,6 +126,7 @@ class TestElement(PyScriptTest):
input = self.page.wait_for_selector("#foo")
assert input.input_value() == ""
@skip_worker("FIXME: js.document")
def test_element_select(self):
"""Test the element select"""
self.pyscript_run(
@@ -137,6 +144,7 @@ class TestElement(PyScriptTest):
select = self.page.wait_for_selector("#foo")
assert select.inner_text() == "Bar"
@skip_worker("FIXME: js.document")
def test_element_clone_no_id(self):
"""Test the element clone"""
self.pyscript_run(
@@ -154,6 +162,7 @@ class TestElement(PyScriptTest):
assert divs.first.inner_text() == "Hello!"
assert divs.last.inner_text() == "Hello!"
@skip_worker("FIXME: js.document")
def test_element_clone_with_id(self):
"""Test the element clone"""
self.pyscript_run(
@@ -173,6 +182,7 @@ class TestElement(PyScriptTest):
clone = self.page.locator("#bar")
assert clone.inner_text() == "Hello!"
@skip_worker("FIXME: js.document")
def test_element_clone_to_other_element(self):
"""Test the element clone"""
self.pyscript_run(
@@ -207,6 +217,7 @@ class TestElement(PyScriptTest):
# Make sure that the clones are rendered in the right order
assert container_div.inner_text() == "Bond\nJames\nBond"
@skip_worker("FIXME: js.document")
def test_element_remove_single_class(self):
"""Test the element remove_class"""
self.pyscript_run(
@@ -222,6 +233,7 @@ class TestElement(PyScriptTest):
div = self.page.locator("#foo")
assert div.get_attribute("class") == "baz"
@skip_worker("FIXME: js.document")
def test_element_remove_multiple_classes(self):
"""Test the element remove_class"""
self.pyscript_run(
@@ -237,6 +249,7 @@ class TestElement(PyScriptTest):
div = self.page.locator("#foo")
assert div.get_attribute("class") == ""
@skip_worker("FIXME: js.document")
def test_element_add_single_class(self):
"""Test the element add_class"""
self.pyscript_run(
@@ -253,6 +266,7 @@ class TestElement(PyScriptTest):
div = self.page.locator("#foo")
assert div.get_attribute("class") == "red"
@skip_worker("FIXME: js.document")
def test_element_add_multiple_class(self):
"""Test the element add_class"""
self.pyscript_run(

View File

@@ -1,4 +1,4 @@
from .support import PyScriptTest
from .support import PyScriptTest, skip_worker
class TestAsync(PyScriptTest):
@@ -87,6 +87,7 @@ class TestAsync(PyScriptTest):
"b func done",
]
@skip_worker("FIXME: display()")
def test_multiple_async_multiple_display_targeted(self):
self.pyscript_run(
"""
@@ -119,6 +120,7 @@ class TestAsync(PyScriptTest):
inner_text = self.page.inner_text("html")
assert "A0\nA1\nB0\nB1" in inner_text
@skip_worker("FIXME: display()")
def test_async_display_untargeted(self):
self.pyscript_run(
"""

View File

@@ -1,4 +1,4 @@
from .support import PyScriptTest
from .support import PyScriptTest, skip_worker
# Source code of a simple plugin that creates a Custom Element for testing purposes
CE_PLUGIN_CODE = """
@@ -194,6 +194,7 @@ def prepare_test(
class TestPlugin(PyScriptTest):
@skip_worker("FIXME: relative paths")
@prepare_test("py-upper", CE_PLUGIN_CODE, tagname="py-up", html="Hello World")
def test_py_plugin_inline(self):
"""Test that a regular plugin that returns new HTML content from connected works"""
@@ -209,6 +210,7 @@ class TestPlugin(PyScriptTest):
rendered_text = self.page.locator("py-up").inner_text()
assert rendered_text == "HELLO WORLD"
@skip_worker("FIXME: relative paths")
@prepare_test("hooks_logger", HOOKS_PLUGIN_CODE, template=HTML_TEMPLATE_NO_TAG)
def test_execution_hooks(self):
"""Test that a Plugin that hooks into the PyScript App events, gets called
@@ -241,6 +243,7 @@ class TestPlugin(PyScriptTest):
# TODO: It'd be actually better to check that the events get called in order
@skip_worker("FIXME: relative paths")
@prepare_test(
"exec_test_logger",
PYSCRIPT_HOOKS_PLUGIN_CODE,
@@ -263,6 +266,7 @@ class TestPlugin(PyScriptTest):
assert "after_id:pyid" in log_lines
assert "result:2" in log_lines
@skip_worker("FIXME: relative paths")
@prepare_test(
"pyrepl_test_logger",
PYREPL_HOOKS_PLUGIN_CODE,
@@ -287,6 +291,7 @@ class TestPlugin(PyScriptTest):
assert "after_id:pyid" in log_lines
assert "result:2" in log_lines
@skip_worker("FIXME: relative paths")
@prepare_test("no_plugin", NO_PLUGIN_CODE)
def test_no_plugin_attribute_error(self):
"""
@@ -301,6 +306,7 @@ class TestPlugin(PyScriptTest):
# EXPECT an error for the missing attribute
assert error_msg in self.console.error.lines
@skip_worker("FIXME: relative paths")
def test_fetch_python_plugin(self):
"""
Test that we can fetch a plugin from a remote URL. Note we need to use
@@ -355,7 +361,7 @@ class TestPlugin(PyScriptTest):
"""
<py-config>
plugins = [
"http://non-existent.blah/hello-world"
"https://non-existent.blah/hello-world"
]
</py-config>
""",
@@ -364,7 +370,7 @@ class TestPlugin(PyScriptTest):
expected_msg = (
"(PY2000): Unable to load plugin from "
"'http://non-existent.blah/hello-world'. Plugins "
"'https://non-existent.blah/hello-world'. Plugins "
"need to contain a file extension and be either a "
"python or javascript file."
)

View File

@@ -6,7 +6,7 @@ from pathlib import Path
import pytest
import requests
from .support import PyScriptTest
from .support import PyScriptTest, with_execution_thread
@pytest.fixture
@@ -35,6 +35,15 @@ def unzip(location, extract_to="."):
file.extractall(path=extract_to)
# Disable the main/worker dual testing, for two reasons:
#
# 1. the <py-config> logic happens before we start the worker, so there is
# no point in running these tests twice
#
# 2. the logic to inject execution_thread into <py-config> works only with
# plain <py-config> tags, but here we want to test all weird combinations
# of config
@with_execution_thread(None)
class TestConfig(PyScriptTest):
def test_py_config_inline(self):
self.pyscript_run(

View File

@@ -1,6 +1,6 @@
import platform
from .support import PyScriptTest
from .support import PyScriptTest, skip_worker
class TestPyRepl(PyScriptTest):
@@ -79,6 +79,7 @@ class TestPyRepl(PyScriptTest):
# Shift-enter should not add a newline to the editor
assert self.page.locator(".cm-line").count() == 1
@skip_worker("FIXME: display()")
def test_display(self):
self.pyscript_run(
"""
@@ -92,6 +93,7 @@ class TestPyRepl(PyScriptTest):
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
assert out_div.inner_text() == "hello world"
@skip_worker("TIMEOUT")
def test_show_last_expression(self):
"""
Test that we display() the value of the last expression, as you would
@@ -109,6 +111,7 @@ class TestPyRepl(PyScriptTest):
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
assert out_div.inner_text() == "42"
@skip_worker("TIMEOUT")
def test_show_last_expression_with_output(self):
"""
Test that we display() the value of the last expression, as you would
@@ -130,6 +133,7 @@ class TestPyRepl(PyScriptTest):
out_div = self.page.wait_for_selector("#repl-target")
assert out_div.inner_text() == "42"
@skip_worker("FIXME: display()")
def test_run_clears_previous_output(self):
"""
Check that we clear the previous output of the cell before executing it
@@ -180,6 +184,7 @@ class TestPyRepl(PyScriptTest):
assert tb_lines[0] == "Traceback (most recent call last):"
assert tb_lines[-1] == "Exception: this is an error"
@skip_worker("FIXME: display()")
def test_multiple_repls(self):
"""
Multiple repls showing in the correct order in the page
@@ -202,6 +207,7 @@ class TestPyRepl(PyScriptTest):
self.page.wait_for_selector("#py-internal-1-repl-output")
assert self.page.inner_text("#py-internal-1-repl-output") == "second"
@skip_worker("FIXME: display()")
def test_python_exception_after_previous_output(self):
self.pyscript_run(
"""
@@ -223,6 +229,7 @@ class TestPyRepl(PyScriptTest):
assert "hello world" not in out_div.inner_text()
assert "ZeroDivisionError" in out_div.inner_text()
@skip_worker("FIXME: js.document")
def test_hide_previous_error_after_successful_run(self):
"""
this tests the fact that a new error div should be created once there's an
@@ -270,6 +277,7 @@ class TestPyRepl(PyScriptTest):
)
assert banner_content == expected
@skip_worker("TIMEOUT")
def test_auto_generate(self):
self.pyscript_run(
"""
@@ -300,6 +308,7 @@ class TestPyRepl(PyScriptTest):
out_texts = [el.inner_text() for el in self.iter_locator(outputs)]
assert out_texts == ["hello", "world", ""]
@skip_worker("FIXME: display()")
def test_multiple_repls_mixed_display_order(self):
"""
Displaying several outputs that don't obey the order in which the original
@@ -331,6 +340,7 @@ class TestPyRepl(PyScriptTest):
assert self.page.inner_text("#py-internal-1-1-repl-output") == "second children"
assert self.page.inner_text("#py-internal-0-1-repl-output") == "first children"
@skip_worker("FIXME: display()")
def test_repl_output_attribute(self):
# Test that output attribute sends stdout to the element
# with the given ID, but not display()
@@ -356,6 +366,7 @@ class TestPyRepl(PyScriptTest):
self.assert_no_banners()
@skip_worker("FIXME: js.document")
def test_repl_output_display_async(self):
# py-repls running async code are not expected to
# send display to element element
@@ -396,6 +407,7 @@ class TestPyRepl(PyScriptTest):
assert self.page.locator("#repl-target").text_content() == ""
self.assert_no_banners()
@skip_worker("FIXME: js.document")
def test_repl_stdio_dynamic_tags(self):
self.pyscript_run(
"""
@@ -498,6 +510,7 @@ class TestPyRepl(PyScriptTest):
assert self.page.wait_for_selector("#stderr-div").inner_text() == "one.\n"
self.assert_no_banners()
@skip_worker("TIMEOUT")
def test_repl_output_attribute_change(self):
# If the user changes the 'output' attribute of a <py-repl> tag mid-execution,
# Output should no longer go to the selected div and a warning should appear
@@ -535,6 +548,7 @@ class TestPyRepl(PyScriptTest):
alert_banner = self.page.wait_for_selector(".alert-banner")
assert expected_alert_banner_msg in alert_banner.inner_text()
@skip_worker("TIMEOUT")
def test_repl_output_element_id_change(self):
# If the user changes the ID of the targeted DOM element mid-execution,
# Output should no longer go to the selected element and a warning should appear
@@ -590,6 +604,7 @@ class TestPyRepl(PyScriptTest):
code = py_repl.inner_text()
assert "print('1')" in code
@skip_worker("TIMEOUT")
def test_repl_src_change(self):
self.writefile("loadReplSrc2.py", "2")
self.writefile("loadReplSrc3.py", "print('3')")

View File

@@ -1,6 +1,6 @@
from playwright.sync_api import expect
from .support import PyScriptTest
from .support import PyScriptTest, skip_worker
class TestPyTerminal(PyScriptTest):
@@ -35,6 +35,7 @@ class TestPyTerminal(PyScriptTest):
"this goes to stdout",
]
@skip_worker("FIXME: js.document")
def test_two_terminals(self):
"""
Multiple <py-terminal>s can cohexist.

View File

@@ -1,6 +1,6 @@
from playwright.sync_api import expect
from .support import PyScriptTest
from .support import PyScriptTest, skip_worker
class TestSplashscreen(PyScriptTest):
@@ -96,6 +96,7 @@ class TestSplashscreen(PyScriptTest):
py_terminal = self.page.wait_for_selector("py-terminal")
assert py_terminal.inner_text() == "Hello pyscript!\n"
@skip_worker("FIXME: js.document")
def test_splashscreen_custom_message(self):
self.pyscript_run(
"""

View File

@@ -1,4 +1,4 @@
from .support import PyScriptTest
from .support import PyScriptTest, skip_worker
class TestOutputHandling(PyScriptTest):
@@ -183,6 +183,7 @@ class TestOutputHandling(PyScriptTest):
self.assert_no_banners()
@skip_worker("FIXME: js.document")
def test_targeted_stdio_dynamic_tags(self):
# Test that creating py-script tags via Python still leaves
# stdio targets working
@@ -292,6 +293,7 @@ class TestOutputHandling(PyScriptTest):
assert self.page.locator("#stderr-div").text_content() == "one."
self.assert_no_banners()
@skip_worker("FIXME: js.document")
def test_stdio_output_attribute_change(self):
# If the user changes the 'output' attribute of a <py-script> tag mid-execution,
# Output should no longer go to the selected div and a warning should appear
@@ -325,6 +327,7 @@ class TestOutputHandling(PyScriptTest):
alert_banner = self.page.locator(".alert-banner")
assert expected_alert_banner_msg in alert_banner.inner_text()
@skip_worker("FIXME: js.document")
def test_stdio_target_element_id_change(self):
# If the user changes the ID of the targeted DOM element mid-execution,
# Output should no longer go to the selected element and a warning should appear

View File

@@ -8,9 +8,10 @@ import numpy as np
import pytest
from PIL import Image
from .support import ROOT, PyScriptTest, wait_for_render
from .support import ROOT, PyScriptTest, wait_for_render, with_execution_thread
@with_execution_thread(None)
@pytest.mark.usefixtures("chdir")
class TestExamples(PyScriptTest):
"""

View File

@@ -1,9 +1,10 @@
import re
from .support import PyScriptTest
from .support import PyScriptTest, skip_worker
class TestDocsSnippets(PyScriptTest):
@skip_worker("FIXME: js.document")
def test_tutorials_py_click(self):
self.pyscript_run(
"""
@@ -65,6 +66,7 @@ class TestDocsSnippets(PyScriptTest):
assert "userId" in py_terminal.inner_text()
self.assert_no_banners()
@skip_worker("FIXME: js.document")
def test_tutorials_py_config_fetch(self):
# flake8: noqa
self.pyscript_run(
@@ -147,6 +149,7 @@ class TestDocsSnippets(PyScriptTest):
assert "0.22.0a3" in py_terminal.inner_text()
self.assert_no_banners()
@skip_worker("FIXME: display()")
def test_tutorials_writing_to_page(self):
self.pyscript_run(
"""

View File

@@ -2,8 +2,9 @@ import sys
import textwrap
from unittest.mock import Mock
import js
import pyscript
from pyscript import HTML, Element, _html
from pyscript import HTML, Element
from pyscript._deprecated_globals import DeprecatedGlobal
from pyscript._internal import set_version_info, uses_top_level_await
from pyscript._mime import format_mime
@@ -19,7 +20,7 @@ class TestElement:
document = Mock()
call_result = "some_result"
document.querySelector = Mock(return_value=call_result)
monkeypatch.setattr(_html, "document", document)
monkeypatch.setattr(js, "document", document)
assert not el._element
real_element = el.element
assert real_element

View File

@@ -3,7 +3,7 @@
"_version": "3.0.0",
"include": ["src/**/*"],
"exclude": ["node_modules/*", "__sapper__/*", "public/*"],
"exclude": ["node_modules/*", "__sapper__/*", "public/*", "src/interpreter_worker/*"],
"compilerOptions": {
"moduleResolution": "node",
"target": "ES2020",