Skip to content

Commit d124871

Browse files
Merge pull request #420 from DataLab-Platform/feature/single-file-plugin-catalog
Support single-file plugins in online and offline catalogs
2 parents 3f47817 + 067e0e9 commit d124871

15 files changed

Lines changed: 1532 additions & 122 deletions

File tree

‎datalab/gui/plugins/available.py‎

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from collections.abc import Callable, Sequence
1717
from html import escape
1818
from importlib import metadata as importlib_metadata
19+
from importlib.machinery import PathFinder
1920

2021
from guidata.configtools import get_icon
2122
from packaging.utils import canonicalize_name
@@ -42,7 +43,7 @@
4243
load_catalogs,
4344
)
4445
from datalab.plugins.dependencies import DEPENDENCIES_DIRECTORY
45-
from datalab.plugins.store import WHEEL_KIND
46+
from datalab.plugins.store import MODULE_KIND, WHEEL_KIND
4647
from datalab.widgets.expandabletext import apply_palette_color, apply_subdued_color
4748

4849
CATALOG_PAGE_URL = "https://datalab-platform.com/plugins/"
@@ -88,7 +89,7 @@ class AvailablePluginItemWidget(QW.QWidget):
8889
def __init__(
8990
self,
9091
plugin: CatalogPlugin,
91-
installed: tuple[str, str],
92+
installed: tuple[str, str, str],
9293
parent: QW.QWidget | None = None,
9394
) -> None:
9495
super().__init__(parent)
@@ -135,7 +136,7 @@ def __init__(
135136
layout.addWidget(self.action_button)
136137

137138
def _describe(
138-
self, release: CatalogRelease | None, installed: tuple[str, str]
139+
self, release: CatalogRelease | None, installed: tuple[str, str, str]
139140
) -> tuple[str, str | None]:
140141
"""Return the status text and the action button text, if any."""
141142
plugin = self.plugin
@@ -146,9 +147,34 @@ def _describe(
146147
python = f"{sys.version_info.major}.{sys.version_info.minor}"
147148
return _("Not available for Python %s") % python, None
148149
return _("Not available for DataLab desktop"), None
149-
how, version = installed
150+
how, version, digest = installed
151+
if how == "unmanaged":
152+
return _("Provided outside the plugin store"), None
153+
if how == "conflict":
154+
return _("Conflicting installed plugin: %s") % version, None
155+
if how == "error":
156+
return version, None
150157
if how == "environment":
151158
return _("Installed in the Python environment (%s)") % version, None
159+
if how == "file" and plugin.kind == MODULE_KIND:
160+
notice = (
161+
_("Deprecated: %s") % plugin.status_reason
162+
if plugin.status == "deprecated"
163+
else ""
164+
)
165+
if digest == release.sha256:
166+
return notice or _("Installed"), None
167+
if version and release.version:
168+
try:
169+
if Version(release.version) < Version(version):
170+
return _("Installed: %s") % version, None
171+
if Version(release.version) > Version(version):
172+
return notice or _("Installed: %s") % version, _(
173+
"Update to %s"
174+
) % release.version
175+
except InvalidVersion:
176+
pass
177+
return notice or _("Different installed revision"), _("Replace...")
152178
if how == "file" and not _is_newer(release.version, version):
153179
return _("Installed"), None
154180
status = ""
@@ -363,7 +389,11 @@ def finished() -> None:
363389
self._populate()
364390

365391
self._installer.install_path(
366-
path, origin=origin, wheelhouse=wheelhouse, on_finished=finished
392+
path,
393+
origin=origin,
394+
wheelhouse=wheelhouse,
395+
on_finished=finished,
396+
catalog_plugin=plugin,
367397
)
368398

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

377-
def _get_installed(self, plugin: CatalogPlugin) -> tuple[str, str]:
407+
def _get_installed(self, plugin: CatalogPlugin) -> tuple[str, str, str]:
378408
"""Return how a catalog plugin is installed, and its version."""
379-
if not plugin.distribution:
380-
return "", ""
381-
key = canonicalize_name(plugin.distribution)
382409
try:
383410
installed_plugins = self._installer.store.get_plugins()
384-
except ValueError:
385-
installed_plugins = []
411+
except (OSError, ValueError) as exc:
412+
return "error", str(exc), ""
413+
if plugin.kind == MODULE_KIND:
414+
for installed in installed_plugins:
415+
same_module = installed.kind == MODULE_KIND and osp.normcase(
416+
installed.name
417+
) == osp.normcase(plugin.module)
418+
if same_module:
419+
if installed.name != plugin.module:
420+
return "conflict", installed.name, ""
421+
if installed.catalog_id and installed.catalog_id != plugin.id:
422+
return "conflict", installed.catalog_id, ""
423+
return "file", installed.version, installed.sha256
424+
if installed.catalog_id == plugin.id:
425+
return "conflict", installed.name, ""
426+
managed = osp.normcase(osp.realpath(self._installer.store.modules_dir))
427+
paths = [
428+
entry
429+
for entry in sys.path
430+
if osp.normcase(osp.realpath(entry)) != managed
431+
]
432+
if PathFinder.find_spec(plugin.module, paths) is not None:
433+
return "unmanaged", "", ""
434+
return "", "", ""
435+
if not plugin.distribution:
436+
return "", "", ""
437+
key = canonicalize_name(plugin.distribution)
386438
for installed in installed_plugins:
387439
if (
388440
installed.kind == WHEEL_KIND
389441
and canonicalize_name(installed.name) == key
390442
):
391-
return "file", installed.version
443+
return "file", installed.version, installed.sha256
444+
if installed.catalog_id == plugin.id and installed.kind != WHEEL_KIND:
445+
return "conflict", installed.name, ""
392446
try:
393-
return "environment", importlib_metadata.version(plugin.distribution)
447+
return "environment", importlib_metadata.version(plugin.distribution), ""
394448
except importlib_metadata.PackageNotFoundError:
395-
return "", ""
449+
return "", "", ""
396450

397451
def _populate(self) -> None:
398452
"""Rebuild the plugin rows matching the search text."""

‎datalab/gui/plugins/install.py‎

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
enable_installed_plugin_classes,
2828
get_installed_plugin_store,
2929
)
30+
from datalab.plugins.catalog import CatalogPlugin
3031
from datalab.plugins.dependencies import DependencyPlan
32+
from datalab.plugins.modules import apply_catalog_metadata
3133
from datalab.plugins.store import (
3234
WHEEL_KIND,
3335
InstalledPlugin,
@@ -93,7 +95,9 @@ def _format_date(timestamp: str) -> str:
9395
def _plugin_title(plugin: InstalledPlugin) -> str:
9496
if plugin.kind == WHEEL_KIND:
9597
return f"{plugin.name} {plugin.version}"
96-
return plugin.filename
98+
if plugin.version:
99+
return f"{plugin.filename} {plugin.version}"
100+
return f"{plugin.filename} ({_('revision %s') % plugin.sha256[:12]})"
97101

98102

99103
class PluginConsentDialog(QW.QDialog):
@@ -164,6 +168,26 @@ def __init__(
164168
form.addRow(_("Dependency source:"), source)
165169
else:
166170
form.addRow(_("Plugin module:"), QW.QLabel(manifest["name"]))
171+
if manifest["display_name"]:
172+
form.addRow(_("Plugin:"), QW.QLabel(escape(manifest["display_name"])))
173+
if manifest["version"]:
174+
form.addRow(_("Version:"), QW.QLabel(escape(manifest["version"])))
175+
else:
176+
form.addRow(_("Revision:"), QW.QLabel(manifest["sha256"][:12]))
177+
if manifest["summary"]:
178+
description = QW.QLabel(escape(manifest["summary"]))
179+
description.setWordWrap(True)
180+
form.addRow(_("Description:"), description)
181+
if manifest["warnings"]:
182+
warning = QW.QLabel(
183+
_(
184+
"Some metadata cannot be read without executing the file. "
185+
"Catalog metadata is used when provided; dependencies are "
186+
"not installed for single-file plugins."
187+
)
188+
)
189+
warning.setWordWrap(True)
190+
form.addRow(warning)
167191
form.addRow(_("File:"), QW.QLabel(manifest["filename"]))
168192
form.addRow(_("Size:"), QW.QLabel(_format_size(manifest["size_bytes"])))
169193
digest = QW.QLabel(manifest["sha256"])
@@ -245,7 +269,7 @@ def __init__(
245269
) -> None:
246270
super().__init__(parent)
247271
self._reload_plugins = reload_plugins
248-
self._pending_install: tuple[str, str | None] | None = None
272+
self._pending_install: tuple[str, str | None, str] | None = None
249273
self._pending_finish: Callable[[], None] | None = None
250274
self._dependency_plan: DependencyPlan | None = None
251275
self._manifest: dict | None = None
@@ -354,6 +378,7 @@ def install_path(
354378
*,
355379
wheelhouse: str | None = None,
356380
on_finished: Callable[[], None] | None = None,
381+
catalog_plugin: CatalogPlugin | None = None,
357382
) -> InstallResult | None:
358383
"""Install a plugin file after the user's consent.
359384
@@ -377,7 +402,11 @@ def install_path(
377402
item["applies"] and not item["compatible"]
378403
for item in manifest["dependencies"]
379404
):
380-
self._pending_install = path, origin
405+
self._pending_install = (
406+
path,
407+
origin,
408+
catalog_plugin.id if catalog_plugin else "",
409+
)
381410
self._pending_finish = on_finished
382411
self._manifest = manifest
383412
self._set_busy(True, _("Checking the local dependency wheels..."))
@@ -390,12 +419,35 @@ def install_path(
390419
return None
391420
else:
392421
manifest = self.store.inspect_module_file(path)
422+
if catalog_plugin is not None:
423+
release = catalog_plugin.get_installable_release()
424+
if release is None or catalog_plugin.module != manifest["name"]:
425+
raise PluginInstallError(
426+
"The module does not match the catalog"
427+
)
428+
manifest = apply_catalog_metadata(
429+
manifest,
430+
plugin_id=catalog_plugin.id,
431+
name=catalog_plugin.name,
432+
version=release.version,
433+
summary=catalog_plugin.summary,
434+
)
393435
if not self.confirm_installation(manifest, origin=origin):
394436
return None
395-
install = (
396-
self.store.install_wheel if is_wheel else self.store.install_module
397-
)
398-
result = install(path, expected_sha256=manifest["sha256"])
437+
if is_wheel:
438+
result = self.store.install_wheel(
439+
path,
440+
expected_sha256=manifest["sha256"],
441+
catalog_id=catalog_plugin.id if catalog_plugin else "",
442+
)
443+
else:
444+
result = self.store.install_module(
445+
path,
446+
expected_sha256=manifest["sha256"],
447+
catalog_id=catalog_plugin.id if catalog_plugin else "",
448+
catalog_name=catalog_plugin.name if catalog_plugin else "",
449+
catalog_version=manifest["version"],
450+
)
399451
except (OSError, ValueError) as exc:
400452
QW.QMessageBox.warning(
401453
self,
@@ -433,7 +485,7 @@ def _set_busy(self, busy: bool, text: str = "") -> None:
433485
def _on_prepared(self, prepared: tuple[dict, DependencyPlan | None]) -> None:
434486
manifest, self._dependency_plan = prepared
435487
assert self._pending_install is not None
436-
path, origin = self._pending_install
488+
path, origin, catalog_id = self._pending_install
437489
if self._manifest is None or manifest["sha256"] != self._manifest["sha256"]:
438490
self._on_install_failed(
439491
_("The plugin changed while checking dependencies.")
@@ -448,6 +500,7 @@ def _on_prepared(self, prepared: tuple[dict, DependencyPlan | None]) -> None:
448500
path,
449501
expected_sha256=manifest["sha256"],
450502
dependency_plan=self._dependency_plan,
503+
catalog_id=catalog_id,
451504
),
452505
self._on_installed,
453506
self._on_install_failed,

‎datalab/locale/fr/LC_MESSAGES/datalab.po‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,39 @@ msgstr ""
1010
"Content-Type: text/plain; charset=utf-8\n"
1111
"Content-Transfer-Encoding: 8bit\n"
1212

13+
msgid "Source code"
14+
msgstr "Code source"
15+
16+
msgid "not applicable"
17+
msgstr "sans objet"
18+
19+
msgid "Provided outside the plugin store"
20+
msgstr "Fourni hors du stockage géré des plugins"
21+
22+
msgid "Conflicting installed plugin: %s"
23+
msgstr "Plugin installé en conflit : %s"
24+
25+
msgid "Different installed revision"
26+
msgstr "Révision installée différente"
27+
28+
msgid "Replace..."
29+
msgstr "Remplacer..."
30+
31+
msgid "Revision:"
32+
msgstr "Révision :"
33+
34+
msgid "revision %s"
35+
msgstr "révision %s"
36+
37+
msgid "Some metadata cannot be read without executing the file. Catalog metadata is used when provided; dependencies are not installed for single-file plugins."
38+
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."
39+
40+
msgid "Write the catalog.json file listing the plugin files."
41+
msgstr "Écrire le fichier catalog.json qui liste les fichiers des plugins."
42+
43+
msgid "Check the catalog.json file against the plugin files."
44+
msgstr "Vérifier le fichier catalog.json par rapport aux fichiers des plugins."
45+
1346
msgid "Additional packages from the local wheelhouse:"
1447
msgstr "Paquets supplémentaires du dépôt local :"
1548

0 commit comments

Comments
 (0)