Allow fetching plugins from URL (#1065)

This commit is contained in:
Fábio Rosado
2023-01-11 17:03:53 +00:00
committed by GitHub
parent 470c3489dd
commit cc4b460183
14 changed files with 568 additions and 114 deletions

View File

@@ -50,9 +50,6 @@ jobs:
- name: Copy redirect file
run: aws s3 cp --quiet ./docs/_build/html/_static/redirect.html s3://docs.pyscript.net/index.html
# - name: Delete release directory
# run: aws s3 rm --recursive s3://docs.pyscript.net/${{ github.ref_name }}/
- name: Sync to S3
run: aws s3 sync --quiet ./docs/_build/html/ s3://docs.pyscript.net/${{ github.ref_name }}/

View File

@@ -0,0 +1,229 @@
# Creating custom pyscript plugins
Pyscript has a few built-in plugins, but you can also create your own ones. This guide will show you how to develop both Javascript and Python plugins.
```{warning}
Pyscript plugins are currently under active development. The API is likely to go through breaking changes between releases.
```
You can add your custom plugins to the `<py-config>` tag on your page. For example:
```html
<py-config>
plugins = ["http://example.com/hello-world.py"]
</py-config>
```
Currently, only single files with the extension `.py` and `.js` files can be used as plugins.
## Python plugins
Python plugins allow you to write plugins in pure Python. We first need to import `Plugin` from `pyscript` and create a new instance of it.
```python
from pyscript import Plugin
plugin = Plugin("PyHelloWorld")
```
We can now create a new class containing our plugin code to add the text "Hello World" to the page.
```python
from pyscript import Plugin, js
plugin = Plugin("PyHelloWorld")
class PyHelloWorld:
def __init__(self, element):
self.element = element
def connect(self):
self.element.innerHTML = "<h1>Hello World!</h1>"
```
Let's now create our `index.html` page and add the plugin.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Python Plugin</title>
<link rel="stylesheet" href="https://pyscript.net/unstable/pyscript.css" />
<script defer src="https://pyscript.net/unstable/pyscript.js"></script>
</head>
<body>
<py-config>
plugins = ["./hello-world.py"]
</py-config>
</body>
</html>
```
Now we need to start a live server to serve our page. You can use Python's `http.server` module for this.
```bash
python -m http.server
```
Now you can open your browser and go to `http://localhost:8000` to see the page. You might be surprised that the text "Hello World" is not on the page. This is because we need to do a few more things to make our plugin work.
First, we must create a custom element that our plugin will use. We can use a decorator in our `PyHelloWorld` class.
```python
from pyscript import Plugin, js
plugin = Plugin("PyHelloWorld")
@plugin.register_custom_element("py-hello-world")
class PyHelloWorld:
def __init__(self, element):
self.element = element
def connect(self):
self.element.innerHTML = "<div id='hello'>Hello World!</div>"
```
Now that we have registered our custom element, we can use the custom tag `<py-hello-world>` to add our plugin to the page.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Python Plugin</title>
<link rel="stylesheet" href="https://pyscript.net/unstable/pyscript.css" />
<script defer src="https://pyscript.net/unstable/pyscript.js"></script>
</head>
<body>
<py-config>
plugins = ["./hello-world.py"]
</py-config>
<py-hello-world></py-hello-world>
</body>
</html>
```
Now, if you go to `http://localhost:8000` you should see the text "Hello World" on the page.
Writing plugins in Python is an excellent way if you want to use PyScript's API's. However, if you want to write plugins in Javascript, you can do that too.
## Javascript plugins
Javascript plugins need to have a specific structure to be loaded by PyScript. The plugin must also export a default class with the following method:
- `afterStartup(runtime)`: This method is called after the plugin has been loaded and the runtime has been initialized.
Note that the `afterStartup` method must accept a single argument, but you can name it whatever you want. For Javascript plugins, this argument is not in use.
```{note}
You need to specify the file extension `.js` when adding your custom plugin to the `<py-config>` tag.
```
### Creating a Hello World plugin
Let's create a simple plugin that will add the text "Hello World" to the page. We will create a `hello-world.js` file and write the plugin class.
```js
export default class HelloWorldPlugin {
afterStartup(runtime) {
// Code goes here
}
}
```
Now we need to add the code that will add the text to the page.
```js
export default class HelloWorldPlugin {
afterStartup(runtime) {
const elem = document.createElement("h1");
elem.innerText = "Hello World";
document.body.appendChild(elem);
}
}
```
Finally, we need to add the plugin to our page's `<py-config>` tag.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Javascript Plugin</title>
<link rel="stylesheet" href="https://pyscript.net/unstable/pyscript.css" />
<script defer src="https://pyscript.net/unstable/pyscript.js"></script>
</head>
<body>
<py-config>
plugins = ["./hello-world.js"]
</py-config>
</body>
</html>
```
Now we need to start a live server to serve our page. You can use Python's `http.server` module for this.
```bash
python -m http.server
```
Now you can open your browser and go to `http://localhost:8000` to see the page. You should see the text "Hello World" on the page.
```{note}
Because we are using a local file, you must start a live server. Otherwise, Pyscript will not be able to fetch the file.
```
### Expanding the Hello World plugin
As you can see, we could build all our plugin logic inside the `afterStartup` method. You may also want to create a custom html element for your plugin. Let's see how we can do that.
First, we need to create a custom html element. Let's start by creating our `PyHelloWorld` class that extends the `HTMLElement` class.
```js
class PyHelloWorld extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = `<h1>Hello, world!</h1>`;
this.mount_name = this.id;
}
}
```
We can now register our custom element in the `afterStartup` method of our `HelloWorldPlugin` class. We will also add the custom tag `py-hello-world` to the page.
```js
export default class HelloWorldPlugin {
afterStartup(runtime) {
// Create a custom element called <py-hello-world>
customElements.define("py-hello-world", PyHelloWorld);
// Add the custom element to the page so we can see it
const elem = document.createElement('py-hello-world');
document.body.append(elem);
}
}
```
Now we can open our page and see the custom element on the page.
By now, you should have a good idea for creating a custom plugin. Also, how powerful it can be to create custom elements that other users could use in their PyScript pages.

View File

@@ -17,4 +17,5 @@ caption: 'Contents:'
passing-objects
http-requests
asyncio
custom-plugins
```

View File

@@ -9,6 +9,8 @@ This reference guide contains the error codes you might find and a description o
|------------|--------------------------------|--------------------|
| PY1000 | Invalid configuration supplied | Confirm that your `py-config` tag is using a valid `TOML` or `JSON` syntax and is using the correct configuration type. |
| PY1001 | Unable to install package(s) | Confirm that the package contains a pure Python 3 wheel or the name of the package is correct. |
| PY2000 | Invalid plugin file extension | Only `.js` and `.py` files can be used when loading user plugins. Please confirm your path contains the file extension. |
| PY2001 | Plugin doesn't contain a default export | Please add `export default` to the main plugin class. |
| PY9000 | Top level await is deprecated | Create a coroutine with your code and schedule it with `asyncio.ensure_future` or similar |
@@ -37,3 +39,14 @@ Pyscript cannot install the package(s) you specified in your `py-config` tag. Th
- An error occurred while trying to install the package
An error banner should appear on your page with the error code and a description of the error or a traceback. You can also check the developer console for more information.
## PY2001
Javascript plugins must export a default class. This is required for PyScript to be able to load the plugin. Please add `export default` to the main plugin class. For example:
```js
export default class HelloWorldPlugin {
afterStartup(runtime) {
console.log("Hello World from the plugin!");
}
```

View File

@@ -7,6 +7,7 @@ These error codes are used to identify the type of error that occurred.
The convention is:
* PY0 - errors that occur when fetching
* PY1 - errors that occur in config
* Py2 - errors that occur in plugins
* PY9 - Deprecation errors
*/
@@ -22,6 +23,8 @@ export enum ErrorCode {
FETCH_UNAVAILABLE_ERROR = "PY0503",
BAD_CONFIG = "PY1000",
MICROPIP_INSTALL_ERROR = "PY1001",
BAD_PLUGIN_FILE_EXTENSION = "PY2000",
NO_DEFAULT_EXPORT = "PY2001",
TOP_LEVEL_AWAIT = "PY9000"
}
@@ -39,13 +42,11 @@ export class UserError extends Error {
}
export class FetchError extends Error {
export class FetchError extends UserError {
errorCode: ErrorCode;
constructor(errorCode: ErrorCode, message: string) {
super(message)
super(errorCode, message)
this.name = "FetchError";
this.errorCode = errorCode;
this.message = `(${errorCode}): ${message}`;
}
}

View File

@@ -1,10 +1,43 @@
import { FetchError, ErrorCode } from './exceptions';
/**
* This is a fetch wrapper that handles any non 200 responses and throws a
* FetchError with the right ErrorCode. This is useful because our FetchError
* will automatically create an alert banner.
*
* @param url - URL to fetch
* @param options - options to pass to fetch
* @returns Response
*/
export async function robustFetch(url: string, options?: RequestInit): Promise<Response> {
const response = await fetch(url, options);
let response: Response;
// Note: We need to wrap fetch into a try/catch block because fetch
// throws a TypeError if the URL is invalid such as http://blah.blah
try {
response = await fetch(url, options);
} catch (err) {
const error = err as Error;
let errMsg: string;
if (url.startsWith('http')) {
errMsg =
`Fetching from URL ${url} failed with error ` +
`'${error.message}'. Are your filename and path correct?`;
} else {
errMsg = `PyScript: Access to local files
(using "Paths:" in &lt;py-config&gt;)
is not available when directly opening a HTML file;
you must use a webserver to serve the additional files.
See <a style="text-decoration: underline;" href="https://github.com/pyscript/pyscript/issues/257#issuecomment-1119595062">this reference</a>
on starting a simple webserver with Python.
`;
}
throw new FetchError(ErrorCode.FETCH_ERROR, errMsg);
}
// Note that response.ok is true for 200-299 responses
if (!response.ok) {
const errorMsg = `Fetching from URL ${url} failed with error ${response.status} (${response.statusText}).`;
const errorMsg = `Fetching from URL ${url} failed with error ${response.status} (${response.statusText}). Are your filename and path correct?`;
switch (response.status) {
case 404:
throw new FetchError(ErrorCode.FETCH_NOT_FOUND_ERROR, errorMsg);

View File

@@ -8,7 +8,7 @@ import { PluginManager, define_custom_element } from './plugin';
import { make_PyScript, initHandlers, mountElements } from './components/pyscript';
import { PyodideRuntime } from './pyodide';
import { getLogger } from './logger';
import { handleFetchError, showWarning, globalExport } from './utils';
import { showWarning, globalExport } from './utils';
import { calculatePaths } from './plugins/fetch';
import { createCustomElements } from './components/elements';
import { UserError, ErrorCode, _createAlertBanner } from './exceptions';
@@ -20,6 +20,8 @@ import { StdioDirector as StdioDirector } from './plugins/stdiodirector';
// eslint-disable-next-line
// @ts-ignore
import pyscript from './python/pyscript.py';
import { robustFetch } from './fetch';
import type { Plugin } from './plugin';
const logger = getLogger('pyscript/main');
@@ -238,7 +240,7 @@ export class PyScriptApp {
//This may be unnecessary - only useful if plugins try to import files fetch'd in fetchPaths()
runtime.invalidate_module_path_cache();
// Finally load plugins
await this.fetchPythonPlugins(runtime);
await this.fetchUserPlugins(runtime);
}
async fetchPaths(runtime: Runtime) {
@@ -251,43 +253,94 @@ export class PyScriptApp {
logger.info('Paths to fetch: ', fetchPaths);
for (let i = 0; i < paths.length; i++) {
logger.info(` fetching path: ${fetchPaths[i]}`);
try {
// Exceptions raised from here will create an alert banner
await runtime.loadFromFile(paths[i], fetchPaths[i]);
} catch (e) {
// The 'TypeError' here happens when running pytest
// I'm not particularly happy with this solution.
if (e.name === 'FetchError' || e.name === 'TypeError') {
handleFetchError(<Error>e, fetchPaths[i]);
} else {
throw e;
}
}
}
logger.info('All paths fetched');
}
/**
* Fetches all the python plugins specified in this.config, saves them on the FS and import
* them as modules, executing any plugin define the module scope
* Fetch user plugins and adds them to `this.plugins` so they can
* be loaded by the PluginManager. Currently, we are just looking
* for .py and .js files and calling the appropriate methods.
*
* @param runtime - runtime that will execute the plugins
* @param runtime - runtime that will be used to execute the plugins that need it.
*/
async fetchPythonPlugins(runtime: Runtime) {
async fetchUserPlugins(runtime: Runtime) {
const plugins = this.config.plugins;
logger.info('Python plugins to fetch: ', plugins);
logger.info('Plugins to fetch: ', plugins);
for (const singleFile of plugins) {
logger.info(` fetching plugins: ${singleFile}`);
try {
const pathArr = singleFile.split('/');
if (singleFile.endsWith('.py')) {
await this.fetchPythonPlugin(runtime, singleFile);
} else if (singleFile.endsWith('.js')) {
await this.fetchJSPlugin(singleFile);
} else {
throw new UserError(
ErrorCode.BAD_PLUGIN_FILE_EXTENSION,
`Unable to load plugin from '${singleFile}'. ` +
`Plugins need to contain a file extension and be ` +
`either a python or javascript file.`,
);
}
logger.info('All plugins fetched');
}
}
/**
* Fetch a javascript plugin from a filePath, it gets a blob from the
* fetch and creates a file from it, then we create a URL from the file
* so we can import it as a module.
*
* This allow us to instantiate the imported plugin with the default
* export in the module (the plugin class) and add it to the plugins
* list with `new importedPlugin()`.
*
* @param filePath - URL of the javascript file to fetch.
*/
async fetchJSPlugin(filePath: string) {
const pluginBlob = await (await robustFetch(filePath)).blob();
const blobFile = new File([pluginBlob], 'plugin.js', { type: 'text/javascript' });
const fileUrl = URL.createObjectURL(blobFile);
const module = await import(fileUrl);
// Note: We have to put module.default in a variable
// because we have seen weird behaviour when doing
// new module.default() directly.
const importedPlugin = module.default;
// If the imported plugin doesn't have a default export
// it will be undefined, so we throw a user error, so
// an alter banner will be created.
if (importedPlugin === undefined) {
throw new UserError(
ErrorCode.NO_DEFAULT_EXPORT,
`Unable to load plugin from '${filePath}'. ` + `Plugins need to contain a default export.`,
);
} else {
this.plugins.add(new importedPlugin());
}
}
/**
* Fetch python plugins from a filePath, saves it on the FS and import
* it as a module, executing any plugin define the module scope.
*
* @param runtime - runtime that will execute the plugins
* @param filePath - path to the python file to fetch
*/
async fetchPythonPlugin(runtime: Runtime, filePath: string) {
const pathArr = filePath.split('/');
const filename = pathArr.pop();
// TODO: Would be probably be better to store plugins somewhere like /plugins/python/ or similar
const destPath = `./${filename}`;
await runtime.loadFromFile(destPath, singleFile);
await runtime.loadFromFile(destPath, filePath);
//refresh module cache before trying to import module files into runtime
runtime.invalidate_module_path_cache();
const modulename = singleFile.replace(/^.*[\\/]/, '').replace('.py', '');
const modulename = filePath.replace(/^.*[\\/]/, '').replace('.py', '');
console.log(`importing ${modulename}`);
// TODO: This is very specific to Pyodide API and will not work for other interpreters,
@@ -302,12 +355,6 @@ export class PyScriptApp {
logger.error(`Cannot find plugin on Python module ${modulename}! Python plugins \
modules must contain a "plugin" attribute. For more information check the plugins documentation.`);
}
} catch (e) {
//Should we still export full error contents to console?
handleFetchError(<Error>e, singleFile);
}
}
logger.info('All plugins fetched');
}
// lifecycle (7)

View File

@@ -93,11 +93,23 @@ export class PluginManager {
}
beforeLaunch(config: AppConfig) {
for (const p of this._plugins) p.beforeLaunch(config);
for (const p of this._plugins) {
try {
p.beforeLaunch(config);
} catch (e) {
logger.error(`Error while calling beforeLaunch hook of plugin ${p.constructor.name}`, e);
}
}
}
afterSetup(runtime: Runtime) {
for (const p of this._plugins) p.afterSetup(runtime);
for (const p of this._plugins) {
try {
p.afterSetup(runtime);
} catch (e) {
logger.error(`Error while calling afterSetup hook of plugin ${p.constructor.name}`, e);
}
}
for (const p of this._pythonPlugins) p.afterSetup?.(runtime);
}

View File

@@ -1,4 +1,4 @@
import { _createAlertBanner, UserError, FetchError, ErrorCode } from "./exceptions"
import { _createAlertBanner } from "./exceptions"
export function addClasses(element: HTMLElement, classes: string[]) {
for (const entry of classes) {
@@ -45,28 +45,6 @@ export function showWarning(msg: string, messageType: 'text' | 'html' = 'text'):
_createAlertBanner(msg, 'warning', messageType);
}
export function handleFetchError(e: Error, singleFile: string) {
// XXX: inspecting the error message to understand what happened is very
// fragile. We need a better solution.
let errorContent: string;
if (e.message.includes('Failed to fetch')) {
errorContent = `PyScript: Access to local files
(using "Paths:" in &lt;py-config&gt;)
is not available when directly opening a HTML file;
you must use a webserver to serve the additional files.
See <a style="text-decoration: underline;" href="https://github.com/pyscript/pyscript/issues/257#issuecomment-1119595062">this reference</a>
on starting a simple webserver with Python.`;
} else if (e.message.includes('404')) {
errorContent =
`PyScript: Loading from file <u>` +
singleFile +
`</u> failed with error 404 (File not Found). Are your filename and path are correct?`;
} else {
errorContent = `PyScript encountered an error while loading from file: ${e.message}`;
}
throw new UserError(ErrorCode.FETCH_ERROR, errorContent, 'html');
}
export function readTextFromPath(path: string) {
const request = new XMLHttpRequest();
request.open('GET', path, false);

View File

@@ -304,6 +304,21 @@ class PyScriptTest:
text = "\n".join(loc.all_inner_texts())
raise AssertionError(f"Found {n} alert banners:\n" + text)
def assert_banner_message(self, expected_message):
"""
Ensure that there is an alert banner on the page with the given message.
Currently it only handles a single.
"""
banner = self.page.wait_for_selector(".alert-banner")
banner_text = banner.inner_text()
if expected_message not in banner_text:
raise AssertionError(
f"Expected message '{expected_message}' does not "
f"match banner text '{banner_text}'"
)
return True
def check_tutor_generated_code(self, modules_to_check=None):
"""
Ensure that the source code viewer injected by the PyTutor plugin

View File

@@ -179,18 +179,12 @@ class TestBasic(PyScriptTest):
with pytest.raises(JsErrors) as exc:
self.check_js_errors()
error_msg = str(exc.value)
print(error_msg)
if self.is_fake_server:
assert (
"Fetching from URL foo.py failed with error 404 (Not Found)."
in error_msg
)
else:
assert (
"Fetching from URL foo.py failed with error 404 (File not found)"
in error_msg
)
error_msgs = str(exc.value)
expected_msg = "(PY0404): Fetching from URL foo.py failed with error 404"
assert expected_msg in error_msgs
assert self.assert_banner_message(expected_msg)
pyscript_tag = self.page.locator("py-script")
assert pyscript_tag.inner_html() == ""

View File

@@ -244,3 +244,115 @@ class TestPlugin(PyScriptTest):
)
# EXPECT an error for the missing attribute
assert error_msg in self.console.error.lines
def test_fetch_python_plugin(self):
"""
Test that we can fetch a plugin from a remote URL. Note we need to use
the 'raw' URL for the plugin, otherwise the request will be rejected
by cors policy.
"""
self.pyscript_run(
"""
<py-config>
plugins = [
"https://raw.githubusercontent.com/FabioRosado/pyscript-plugins/main/python/hello-world.py"
]
</py-config>
<py-hello-world></py-hello-world>
"""
)
hello_element = self.page.locator("py-hello-world")
assert hello_element.inner_html() == '<div id="hello">Hello World!</div>'
def test_fetch_js_plugin(self):
self.pyscript_run(
"""
<py-config>
plugins = [
"https://raw.githubusercontent.com/FabioRosado/pyscript-plugins/main/js/hello-world.js"
]
</py-config>
"""
)
hello_element = self.page.locator("py-hello-world")
assert hello_element.inner_html() == "<h1>Hello, world!</h1>"
def test_fetch_js_plugin_bare(self):
self.pyscript_run(
"""
<py-config>
plugins = [
"https://raw.githubusercontent.com/FabioRosado/pyscript-plugins/main/js/hello-world-base.js"
]
</py-config>
"""
)
hello_element = self.page.locator("py-hello-world")
assert hello_element.inner_html() == "<h1>Hello, world!</h1>"
def test_fetch_plugin_no_file_extension(self):
self.pyscript_run(
"""
<py-config>
plugins = [
"http://non-existent.blah/hello-world"
]
</py-config>
""",
wait_for_pyscript=False,
)
expected_msg = (
"(PY2000): Unable to load plugin from "
"'http://non-existent.blah/hello-world'. Plugins "
"need to contain a file extension and be either a "
"python or javascript file."
)
assert self.assert_banner_message(expected_msg)
def test_fetch_js_plugin_non_existent(self):
self.pyscript_run(
"""
<py-config>
plugins = [
"http://non-existent.example.com/hello-world.js"
]
</py-config>
""",
wait_for_pyscript=False,
)
expected_msg = (
"(PY0001): Fetching from URL "
"http://non-existent.example.com/hello-world.js failed "
"with error 'Failed to fetch'. Are your filename and "
"path correct?"
)
assert self.assert_banner_message(expected_msg)
def test_fetch_js_no_export(self):
self.pyscript_run(
"""
<py-config>
plugins = [
"https://raw.githubusercontent.com/FabioRosado/pyscript-plugins/main/js/hello-world-no-export.js"
]
</py-config>
""",
wait_for_pyscript=False,
)
expected_message = (
"(PY2001): Unable to load plugin from "
"'https://raw.githubusercontent.com/FabioRosado/pyscript-plugins"
"/main/js/hello-world-no-export.js'. "
"Plugins need to contain a default export."
)
assert self.assert_banner_message(expected_message)

View File

@@ -245,12 +245,10 @@ class TestConfig(PyScriptTest):
wait_for_pyscript=False,
)
expected = (
"Loading from file <u>./f.py</u> failed with error 404 (File not Found). "
"Are your filename and path are correct?"
)
expected = "(PY0404): Fetching from URL ./f.py failed with " "error 404"
inner_html = self.page.locator(".py-error").inner_html()
assert expected in inner_html
assert expected in self.console.error.lines[-1]

View File

@@ -19,19 +19,8 @@ describe("robustFetch", () => {
const url = "https://pyscript.net/non-existent-page"
const expectedError = new FetchError(
ErrorCode.FETCH_NOT_FOUND_ERROR,
`Fetching from URL ${url} failed with error 404 (Not Found).`
)
expect(() => robustFetch(url)).rejects.toThrow(expectedError);
})
it('receiving a 404 when fetching should throw FetchError with the right errorCode', async () => {
global.fetch = jest.fn(() => (Promise.resolve(new Response("Not Found", {status: 404}))));
const url = "https://pyscript.net/non-existent-page"
const expectedError = new FetchError(
ErrorCode.FETCH_NOT_FOUND_ERROR,
`Fetching from URL ${url} failed with error 404 (Not Found).`
`Fetching from URL ${url} failed with error 404 (Not Found). ` +
`Are your filename and path correct?`
)
expect(() => robustFetch(url)).rejects.toThrow(expectedError);
@@ -43,7 +32,8 @@ describe("robustFetch", () => {
const url = "https://pyscript.net/protected-page"
const expectedError = new FetchError(
ErrorCode.FETCH_UNAUTHORIZED_ERROR,
`Fetching from URL ${url} failed with error 401 (Unauthorized).`
`Fetching from URL ${url} failed with error 401 (Unauthorized). ` +
`Are your filename and path correct?`
)
expect(() => robustFetch(url)).rejects.toThrow(expectedError);
@@ -55,7 +45,8 @@ describe("robustFetch", () => {
const url = "https://pyscript.net/secret-page"
const expectedError = new FetchError(
ErrorCode.FETCH_FORBIDDEN_ERROR,
`Fetching from URL ${url} failed with error 403 (Forbidden).`
`Fetching from URL ${url} failed with error 403 (Forbidden). ` +
`Are your filename and path correct?`
)
expect(() => robustFetch(url)).rejects.toThrow(expectedError);
@@ -67,7 +58,8 @@ describe("robustFetch", () => {
const url = "https://pyscript.net/protected-page"
const expectedError = new FetchError(
ErrorCode.FETCH_SERVER_ERROR,
`Fetching from URL ${url} failed with error 500 (Internal Server Error).`
`Fetching from URL ${url} failed with error 500 (Internal Server Error). ` +
`Are your filename and path correct?`
)
expect(() => robustFetch(url)).rejects.toThrow(expectedError);
@@ -79,7 +71,39 @@ describe("robustFetch", () => {
const url = "https://pyscript.net/protected-page"
const expectedError = new FetchError(
ErrorCode.FETCH_UNAVAILABLE_ERROR,
`Fetching from URL ${url} failed with error 503 (Service Unavailable).`
`Fetching from URL ${url} failed with error 503 (Service Unavailable). ` +
`Are your filename and path correct?`
)
expect(() => robustFetch(url)).rejects.toThrow(expectedError);
})
it('handle TypeError when using a bad url', async () => {
global.fetch = jest.fn(() => (Promise.reject(new TypeError("Failed to fetch"))));
const url = "https://pyscript.net/protected-page"
const expectedError = new FetchError(
ErrorCode.FETCH_ERROR,
`Fetching from URL ${url} failed with error 'Failed to fetch'. Are your filename and path correct?`
)
expect(() => robustFetch(url)).rejects.toThrow(expectedError);
})
it('handle failed to fetch when using local file', async () => {
global.fetch = jest.fn(() => (Promise.reject(new TypeError("Failed to fetch"))));
const url = "./my-awesome-pyscript.py"
const expectedError = new FetchError(
ErrorCode.FETCH_ERROR,
`PyScript: Access to local files
(using "Paths:" in &lt;py-config&gt;)
is not available when directly opening a HTML file;
you must use a webserver to serve the additional files.
See <a style="text-decoration: underline;" href="https://github.com/pyscript/pyscript/issues/257#issuecomment-1119595062">this reference</a>
on starting a simple webserver with Python.
`
)
expect(() => robustFetch(url)).rejects.toThrow(expectedError);