Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 68 additions & 14 deletions datalab/gui/plugins/available.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from collections.abc import Callable, Sequence
from html import escape
from importlib import metadata as importlib_metadata
from importlib.machinery import PathFinder

from guidata.configtools import get_icon
from packaging.utils import canonicalize_name
Expand All @@ -42,7 +43,7 @@
load_catalogs,
)
from datalab.plugins.dependencies import DEPENDENCIES_DIRECTORY
from datalab.plugins.store import WHEEL_KIND
from datalab.plugins.store import MODULE_KIND, WHEEL_KIND
from datalab.widgets.expandabletext import apply_palette_color, apply_subdued_color

CATALOG_PAGE_URL = "https://datalab-platform.com/plugins/"
Expand Down Expand Up @@ -88,7 +89,7 @@ class AvailablePluginItemWidget(QW.QWidget):
def __init__(
self,
plugin: CatalogPlugin,
installed: tuple[str, str],
installed: tuple[str, str, str],
parent: QW.QWidget | None = None,
) -> None:
super().__init__(parent)
Expand Down Expand Up @@ -135,7 +136,7 @@ def __init__(
layout.addWidget(self.action_button)

def _describe(
self, release: CatalogRelease | None, installed: tuple[str, str]
self, release: CatalogRelease | None, installed: tuple[str, str, str]
) -> tuple[str, str | None]:
"""Return the status text and the action button text, if any."""
plugin = self.plugin
Expand All @@ -146,9 +147,34 @@ def _describe(
python = f"{sys.version_info.major}.{sys.version_info.minor}"
return _("Not available for Python %s") % python, None
return _("Not available for DataLab desktop"), None
how, version = installed
how, version, digest = installed
if how == "unmanaged":
return _("Provided outside the plugin store"), None
if how == "conflict":
return _("Conflicting installed plugin: %s") % version, None
if how == "error":
return version, None
if how == "environment":
return _("Installed in the Python environment (%s)") % version, None
if how == "file" and plugin.kind == MODULE_KIND:
notice = (
_("Deprecated: %s") % plugin.status_reason
if plugin.status == "deprecated"
else ""
)
if digest == release.sha256:
return notice or _("Installed"), None
if version and release.version:
try:
if Version(release.version) < Version(version):
return _("Installed: %s") % version, None
if Version(release.version) > Version(version):
return notice or _("Installed: %s") % version, _(
"Update to %s"
) % release.version
except InvalidVersion:
pass
return notice or _("Different installed revision"), _("Replace...")
if how == "file" and not _is_newer(release.version, version):
return _("Installed"), None
status = ""
Expand Down Expand Up @@ -363,7 +389,11 @@ def finished() -> None:
self._populate()

self._installer.install_path(
path, origin=origin, wheelhouse=wheelhouse, on_finished=finished
path,
origin=origin,
wheelhouse=wheelhouse,
on_finished=finished,
catalog_plugin=plugin,
)

def _on_download_failed(self, message: str) -> None:
Expand All @@ -374,25 +404,49 @@ def _on_download_failed(self, message: str) -> None:
self.status_label.setText("")
QW.QMessageBox.warning(self, _("Install plugin"), message)

def _get_installed(self, plugin: CatalogPlugin) -> tuple[str, str]:
def _get_installed(self, plugin: CatalogPlugin) -> tuple[str, str, str]:
"""Return how a catalog plugin is installed, and its version."""
if not plugin.distribution:
return "", ""
key = canonicalize_name(plugin.distribution)
try:
installed_plugins = self._installer.store.get_plugins()
except ValueError:
installed_plugins = []
except (OSError, ValueError) as exc:
return "error", str(exc), ""
if plugin.kind == MODULE_KIND:
for installed in installed_plugins:
same_module = installed.kind == MODULE_KIND and osp.normcase(
installed.name
) == osp.normcase(plugin.module)
if same_module:
if installed.name != plugin.module:
return "conflict", installed.name, ""
if installed.catalog_id and installed.catalog_id != plugin.id:
return "conflict", installed.catalog_id, ""
return "file", installed.version, installed.sha256
if installed.catalog_id == plugin.id:
return "conflict", installed.name, ""
managed = osp.normcase(osp.realpath(self._installer.store.modules_dir))
paths = [
entry
for entry in sys.path
if osp.normcase(osp.realpath(entry)) != managed
]
if PathFinder.find_spec(plugin.module, paths) is not None:
return "unmanaged", "", ""
return "", "", ""
if not plugin.distribution:
return "", "", ""
key = canonicalize_name(plugin.distribution)
for installed in installed_plugins:
if (
installed.kind == WHEEL_KIND
and canonicalize_name(installed.name) == key
):
return "file", installed.version
return "file", installed.version, installed.sha256
if installed.catalog_id == plugin.id and installed.kind != WHEEL_KIND:
return "conflict", installed.name, ""
try:
return "environment", importlib_metadata.version(plugin.distribution)
return "environment", importlib_metadata.version(plugin.distribution), ""
except importlib_metadata.PackageNotFoundError:
return "", ""
return "", "", ""

def _populate(self) -> None:
"""Rebuild the plugin rows matching the search text."""
Expand Down
69 changes: 61 additions & 8 deletions datalab/gui/plugins/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
enable_installed_plugin_classes,
get_installed_plugin_store,
)
from datalab.plugins.catalog import CatalogPlugin
from datalab.plugins.dependencies import DependencyPlan
from datalab.plugins.modules import apply_catalog_metadata
from datalab.plugins.store import (
WHEEL_KIND,
InstalledPlugin,
Expand Down Expand Up @@ -93,7 +95,9 @@ def _format_date(timestamp: str) -> str:
def _plugin_title(plugin: InstalledPlugin) -> str:
if plugin.kind == WHEEL_KIND:
return f"{plugin.name} {plugin.version}"
return plugin.filename
if plugin.version:
return f"{plugin.filename} {plugin.version}"
return f"{plugin.filename} ({_('revision %s') % plugin.sha256[:12]})"


class PluginConsentDialog(QW.QDialog):
Expand Down Expand Up @@ -164,6 +168,26 @@ def __init__(
form.addRow(_("Dependency source:"), source)
else:
form.addRow(_("Plugin module:"), QW.QLabel(manifest["name"]))
if manifest["display_name"]:
form.addRow(_("Plugin:"), QW.QLabel(escape(manifest["display_name"])))
if manifest["version"]:
form.addRow(_("Version:"), QW.QLabel(escape(manifest["version"])))
else:
form.addRow(_("Revision:"), QW.QLabel(manifest["sha256"][:12]))
if manifest["summary"]:
description = QW.QLabel(escape(manifest["summary"]))
description.setWordWrap(True)
form.addRow(_("Description:"), description)
if manifest["warnings"]:
warning = QW.QLabel(
_(
"Some metadata cannot be read without executing the file. "
"Catalog metadata is used when provided; dependencies are "
"not installed for single-file plugins."
)
)
warning.setWordWrap(True)
form.addRow(warning)
form.addRow(_("File:"), QW.QLabel(manifest["filename"]))
form.addRow(_("Size:"), QW.QLabel(_format_size(manifest["size_bytes"])))
digest = QW.QLabel(manifest["sha256"])
Expand Down Expand Up @@ -245,7 +269,7 @@ def __init__(
) -> None:
super().__init__(parent)
self._reload_plugins = reload_plugins
self._pending_install: tuple[str, str | None] | None = None
self._pending_install: tuple[str, str | None, str] | None = None
self._pending_finish: Callable[[], None] | None = None
self._dependency_plan: DependencyPlan | None = None
self._manifest: dict | None = None
Expand Down Expand Up @@ -354,6 +378,7 @@ def install_path(
*,
wheelhouse: str | None = None,
on_finished: Callable[[], None] | None = None,
catalog_plugin: CatalogPlugin | None = None,
) -> InstallResult | None:
"""Install a plugin file after the user's consent.

Expand All @@ -377,7 +402,11 @@ def install_path(
item["applies"] and not item["compatible"]
for item in manifest["dependencies"]
):
self._pending_install = path, origin
self._pending_install = (
path,
origin,
catalog_plugin.id if catalog_plugin else "",
)
self._pending_finish = on_finished
self._manifest = manifest
self._set_busy(True, _("Checking the local dependency wheels..."))
Expand All @@ -390,12 +419,35 @@ def install_path(
return None
else:
manifest = self.store.inspect_module_file(path)
if catalog_plugin is not None:
release = catalog_plugin.get_installable_release()
if release is None or catalog_plugin.module != manifest["name"]:
raise PluginInstallError(
"The module does not match the catalog"
)
manifest = apply_catalog_metadata(
manifest,
plugin_id=catalog_plugin.id,
name=catalog_plugin.name,
version=release.version,
summary=catalog_plugin.summary,
)
if not self.confirm_installation(manifest, origin=origin):
return None
install = (
self.store.install_wheel if is_wheel else self.store.install_module
)
result = install(path, expected_sha256=manifest["sha256"])
if is_wheel:
result = self.store.install_wheel(
path,
expected_sha256=manifest["sha256"],
catalog_id=catalog_plugin.id if catalog_plugin else "",
)
else:
result = self.store.install_module(
path,
expected_sha256=manifest["sha256"],
catalog_id=catalog_plugin.id if catalog_plugin else "",
catalog_name=catalog_plugin.name if catalog_plugin else "",
catalog_version=manifest["version"],
)
except (OSError, ValueError) as exc:
QW.QMessageBox.warning(
self,
Expand Down Expand Up @@ -433,7 +485,7 @@ def _set_busy(self, busy: bool, text: str = "") -> None:
def _on_prepared(self, prepared: tuple[dict, DependencyPlan | None]) -> None:
manifest, self._dependency_plan = prepared
assert self._pending_install is not None
path, origin = self._pending_install
path, origin, catalog_id = self._pending_install
if self._manifest is None or manifest["sha256"] != self._manifest["sha256"]:
self._on_install_failed(
_("The plugin changed while checking dependencies.")
Expand All @@ -448,6 +500,7 @@ def _on_prepared(self, prepared: tuple[dict, DependencyPlan | None]) -> None:
path,
expected_sha256=manifest["sha256"],
dependency_plan=self._dependency_plan,
catalog_id=catalog_id,
),
self._on_installed,
self._on_install_failed,
Expand Down
33 changes: 33 additions & 0 deletions datalab/locale/fr/LC_MESSAGES/datalab.po
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,39 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"

msgid "Source code"
msgstr "Code source"

msgid "not applicable"
msgstr "sans objet"

msgid "Provided outside the plugin store"
msgstr "Fourni hors du stockage géré des plugins"

msgid "Conflicting installed plugin: %s"
msgstr "Plugin installé en conflit : %s"

msgid "Different installed revision"
msgstr "Révision installée différente"

msgid "Replace..."
msgstr "Remplacer..."

msgid "Revision:"
msgstr "Révision :"

msgid "revision %s"
msgstr "révision %s"

msgid "Some metadata cannot be read without executing the file. Catalog metadata is used when provided; dependencies are not installed for single-file plugins."
msgstr "Certaines métadonnées ne peuvent pas être lues sans exécuter le fichier. Les métadonnées du catalogue sont utilisées si elles sont fournies ; les dépendances ne sont pas installées pour les plugins en fichier unique."

msgid "Write the catalog.json file listing the plugin files."
msgstr "Écrire le fichier catalog.json qui liste les fichiers des plugins."

msgid "Check the catalog.json file against the plugin files."
msgstr "Vérifier le fichier catalog.json par rapport aux fichiers des plugins."

msgid "Additional packages from the local wheelhouse:"
msgstr "Paquets supplémentaires du dépôt local :"

Expand Down
Loading
Loading