Improve UX when we can't install a package (#1000)

This commit is contained in:
Fábio Rosado
2022-12-05 14:35:15 +00:00
committed by GitHub
parent 5aa9135a34
commit af60299324
5 changed files with 102 additions and 6 deletions

View File

@@ -8,6 +8,7 @@ This reference guide contains the error codes you might find and a description o
| Error code | Description | Recommendation |
|------------|--------------------------------|--------------------|
| 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. |
| PY9000 | Top level await is deprecated | Create a coroutine with your code and schedule it with `asyncio.ensure_future` or similar |
@@ -26,3 +27,13 @@ These error codes are related to any exception raised when trying to fetch a res
| PY0404 | The page you are trying to fetch does not exist. |
| PY0500 | The server encountered an internal error. |
| PY0503 | The server is currently unavailable. |
## PY1001
Pyscript cannot install the package(s) you specified in your `py-config` tag. This can happen for a few reasons:
- The package does not exist
- The package does not contain a pure Python 3 wheel
- 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.

View File

@@ -21,6 +21,7 @@ export enum ErrorCode {
FETCH_SERVER_ERROR = "PY0500",
FETCH_UNAVAILABLE_ERROR = "PY0503",
BAD_CONFIG = "PY1000",
MICROPIP_INSTALL_ERROR = "PY1001",
TOP_LEVEL_AWAIT = "PY9000"
}
@@ -48,6 +49,15 @@ export class FetchError extends Error {
}
}
export class InstallError extends UserError {
errorCode: ErrorCode;
constructor(errorCode: ErrorCode, message: string) {
super(errorCode, message)
this.name = "InstallError";
}
}
export function _createAlertBanner(
message: string,
level: 'error' | 'warning' = 'error',

View File

@@ -227,8 +227,10 @@ be removed from the Python global namespace in the following release. \
To avoid errors in future releases use import from pyscript instead. For instance: \
from pyscript import micropip, Element, console, document`);
logger.info('Packages to install: ', this.config.packages);
await runtime.installPackage(this.config.packages);
if (this.config.packages) {
logger.info('Packages to install: ', this.config.packages);
await runtime.installPackage(this.config.packages);
}
await this.fetchPaths(runtime);
//This may be unnecessary - only useful if plugins try to import files fetch'd in fetchPaths()

View File

@@ -1,5 +1,6 @@
import { Runtime } from './runtime';
import { getLogger } from './logger';
import { InstallError, ErrorCode } from './exceptions'
import type { loadPyodide as loadPyodideDeclaration, PyodideInterface, PyProxy } from 'pyodide';
import { robustFetch } from './fetch';
import type { AppConfig } from './pyconfig';
@@ -67,8 +68,10 @@ export class PyodideRuntime extends Runtime {
this.globals = this.interpreter.globals;
// XXX: ideally, we should load micropip only if we actually need it
await this.loadPackage('micropip');
if (this.config.packages) {
logger.info("Found packages in configuration to install. Loading micropip...")
await this.loadPackage('micropip');
}
logger.info('pyodide loaded and initialized');
}
@@ -89,8 +92,41 @@ export class PyodideRuntime extends Runtime {
if (package_name.length > 0) {
logger.info(`micropip install ${package_name.toString()}`);
const micropip = this.globals.get('micropip') as Micropip;
await micropip.install(package_name);
micropip.destroy();
try {
await micropip.install(package_name);
micropip.destroy();
} catch(e) {
let exceptionMessage = `Unable to install package(s) '` + package_name +`'.`
// If we can't fetch `package_name` micropip.install throws a huge
// Python traceback in `e.message` this logic is to handle the
// error and throw a more sensible error message instead of the
// huge traceback.
if (e.message.includes("Can't find a pure Python 3 wheel")) {
exceptionMessage += (
` Reason: Can't find a pure Python 3 Wheel for package(s) '` + package_name +
`'. See: https://pyodide.org/en/stable/usage/faq.html#micropip-can-t-find-a-pure-python-wheel ` +
`for more information.`
)
} else if (e.message.includes("Can't fetch metadata")) {
exceptionMessage += (
" Unable to find package in PyPI. " +
"Please make sure you have entered a correct package name."
)
} else {
exceptionMessage += (
` Reason: ${e.message as string}. Please open an issue at ` +
`https://github.com/pyscript/pyscript/issues/new if you require help or ` +
`you think it's a bug.`)
}
logger.error(e);
throw new InstallError(
ErrorCode.MICROPIP_INSTALL_ERROR,
exceptionMessage
)
}
}
}

View File

@@ -101,6 +101,43 @@ class TestBasic(PyScriptTest):
"hello asciitree", # printed by us
]
def test_non_existent_package(self):
self.pyscript_run(
"""
<py-config>
packages = ["nonexistendright"]
</py-config>
""",
wait_for_pyscript=False,
)
expected_alert_banner_msg = (
"(PY1001): Unable to install package(s) 'nonexistendright'. "
"Unable to find package in PyPI. Please make sure you have "
"entered a correct package name."
)
alert_banner = self.page.wait_for_selector(".alert-banner")
assert expected_alert_banner_msg in alert_banner.inner_text()
def test_no_python_wheel(self):
self.pyscript_run(
"""
<py-config>
packages = ["opsdroid"]
</py-config>
""",
wait_for_pyscript=False,
)
expected_alert_banner_msg = (
"(PY1001): Unable to install package(s) 'opsdroid'. "
"Reason: Can't find a pure Python 3 Wheel for package(s) 'opsdroid'"
)
alert_banner = self.page.wait_for_selector(".alert-banner")
assert expected_alert_banner_msg in alert_banner.inner_text()
def test_dynamically_add_py_script_tag(self):
self.pyscript_run(
"""