From a6e228108e77a47488a2b83543d5999f6c2d3ee9 Mon Sep 17 00:00:00 2001 From: Nathan Clack Date: Wed, 24 Nov 2021 13:46:16 -0800 Subject: [PATCH 01/11] Some cleanup --- EXAMPLE/main.py | 2 - EXAMPLE/napari.json | 29 -- EXAMPLE/napari.yaml | 26 -- EXAMPLE/pyproject.toml | 20 -- npe2/manifest/schema.py | 73 +---- npe2/manifest/spdx.txt | 430 ----------------------------- tests/sample/my_plugin/napari.yaml | 1 - 7 files changed, 13 insertions(+), 568 deletions(-) delete mode 100644 EXAMPLE/main.py delete mode 100644 EXAMPLE/napari.json delete mode 100644 EXAMPLE/napari.yaml delete mode 100644 EXAMPLE/pyproject.toml delete mode 100644 npe2/manifest/spdx.txt diff --git a/EXAMPLE/main.py b/EXAMPLE/main.py deleted file mode 100644 index 880ea8a3..00000000 --- a/EXAMPLE/main.py +++ /dev/null @@ -1,2 +0,0 @@ -def activate(): - print("hi") diff --git a/EXAMPLE/napari.json b/EXAMPLE/napari.json deleted file mode 100644 index 6bdace35..00000000 --- a/EXAMPLE/napari.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "my_plugin", - "entry_point": "main.py", - "contributions": { - "commands": [ - { "command": "identifier.hello_world", "title": "Hello World" }, - { - "command": "identifier.other_command", - "title": "Other Command", - "icon": "some_icon.svg" - }, - { - "command": "identifier.third", - "title": "Third Command", - "icon": { "light": "light_icon.svg", "dark": "dark.svg" } - } - ], - "menus": { - "layers__context": [ - { "submenu": "mysubmenu" }, - { "command": "identifier.other_command" } - ] - }, - "keybindings": [{ "command": "identifier.hello_world", "key": "ctrl+f1" }], - "submenus": [{ "id": "mysubmenu", "label": "My SubMenu" }] - }, - "license": "BSD-3-Clause", - "manifest_file": "EXAMPLE/pyproject.toml" -} diff --git a/EXAMPLE/napari.yaml b/EXAMPLE/napari.yaml deleted file mode 100644 index a8d57c65..00000000 --- a/EXAMPLE/napari.yaml +++ /dev/null @@ -1,26 +0,0 @@ -entry_point: main.py -license: BSD-3-Clause -manifest_file: EXAMPLE/pyproject.toml -name: my_plugin -contributions: - commands: - - id: identifier.hello_world - title: Hello World - - id: identifier.other_command - icon: some_icon.svg - title: Other Command - - id: identifier.third - icon: - dark: dark.svg - light: light_icon.svg - title: Third Command - keybindings: - - command: identifier.hello_world - key: ctrl+f1 - menus: - layers__context: - - submenu: mysubmenu - - command: identifier.other_command - submenus: - - id: mysubmenu - label: My SubMenu diff --git a/EXAMPLE/pyproject.toml b/EXAMPLE/pyproject.toml deleted file mode 100644 index 97aeac9f..00000000 --- a/EXAMPLE/pyproject.toml +++ /dev/null @@ -1,20 +0,0 @@ -[tool.napari] -name = 'my_plugin' -entry_point = 'main.py' -license = 'BSD-3-Clause' - -[tool.napari.contributions] -commands = [ - { command = 'identifier.hello_world', title = "Hello World" }, - { command = 'identifier.other_command', title = "Other Command", icon = 'some_icon.svg' }, - { command = 'identifier.third', title = "Third Command", icon = { light = 'light_icon.svg', dark = 'dark.svg' } }, -] -submenus = [{ id = 'mysubmenu', label = "My SubMenu" }] -keybindings = [{ command = 'identifier.hello_world', "key" = "ctrl+f1" }] - -[tool.napari.contributions.menus] -mysubmenu = [{ command = 'identifier.hello_world' }] -layers__context = [ - { submenu = 'mysubmenu' }, - { command = 'identifier.other_command' }, -] diff --git a/npe2/manifest/schema.py b/npe2/manifest/schema.py index a0d028a2..50993cb5 100644 --- a/npe2/manifest/schema.py +++ b/npe2/manifest/schema.py @@ -4,7 +4,6 @@ import sys import types from contextlib import contextmanager -from enum import Enum from importlib import import_module, util from logging import getLogger from pathlib import Path @@ -14,7 +13,6 @@ Any, Callable, Iterator, - List, NamedTuple, Optional, Sequence, @@ -37,9 +35,6 @@ from importlib.metadata import EntryPoint -spdx_ids = (Path(__file__).parent / "spdx.txt").read_text().splitlines() -SPDX = Enum("SPDX", {i.replace("-", "_"): i for i in spdx_ids}) # type: ignore - logger = getLogger(__name__) ENTRY_POINT = "napari.manifest" @@ -53,17 +48,12 @@ class DiscoverResults(NamedTuple): class PluginManifest(BaseModel): - # VS Code uses . as a unique ID for the extension - # should this just be the package name ... not the module name? (probably yes) - # do we normalize this? (i.e. underscores / dashes ?) - # TODO: enforce that this matches the package name name: str = Field( ..., - description="The name of the plugin - should be all lowercase with no spaces.", + description="The name of the plugin. Should correspond to the python " + "package name for this plugin.", ) - # this is not something that has an equivalent on PyPI ... - # it might be a good field with which we can identify trusted source - # but... it's not entire clear how that "trust" gets validated at the moment + publisher: Optional[str] = Field( None, description="The publisher name - can be an individual or an organization", @@ -77,55 +67,27 @@ class PluginManifest(BaseModel): # non-word character. regex=r"^[^\W_][\w -~]{1,38}[^\W_]$", ) - # take this from setup.cfg + description: Optional[str] = Field( description="A short description of what your extension is and does." + "When unspecified, the description is taken from package metadata." ) - # TODO: - # Perhaps we should version the plugin interface (not so the manifest, but - # the actual mechanism/consumption of plugin information) independently - # of napari itself - - # mechanistic things: - # this is the module that has the activate() function + # The module that has the activate() function entry_point: Optional[str] = Field( default=None, - description="The extension entry point. This should be a fully qualified " - "module string. e.g. `foo.bar.baz`", + description="The extension entry point. This should be a fully " + "qualified module string. e.g. `foo.bar.baz` for a module containing " + "the plugin's activate() function.", ) - # this comes from setup.cfg - version: Optional[str] = Field(None, description="SemVer compatible version.") - # this should come from setup.cfg ... but they don't requireq SPDX - license: Optional[SPDX] = None - - contributions: Optional[ContributionPoints] - - categories: List[str] = Field( - default_factory=list, - description="specifically defined classifiers", - ) - - # in the absense of input. should be inferred from version (require using rc ...) - # or use `classifiers = Status` - preview: Optional[bool] = Field( + version: Optional[str] = Field( None, - description="Sets the extension to be flagged as a Preview in napari-hub.", + description="SemVer compatible version. When unspecified the version " + "is taken from package metadata.", ) - private: bool = Field(False, description="Whether this extension is private") - # activationEvents: Optional[List[ActivationEvent]] = Field( - # default_factory=list, - # description="Events upon which your extension becomes active.", - # ) - - # @validator("activationEvents", pre=True) - # def _validateActivationEvent(cls, val): - # return [ - # dict(zip(("kind", "id"), x.split(":"))) if ":" in x else x - # for x in val - # ] + contributions: Optional[ContributionPoints] _manifest_file: Optional[Path] = None @@ -275,15 +237,6 @@ def _populate_missing_meta(self, metadata: Message): self.version = metadata["Version"] if not self.description: self.description = metadata["Summary"] - if not self.license: - self.license = metadata["License"] - if self.preview is None: - for k, v in getattr(metadata, "_headers"): - if k.lower() == "classifier" and v.lower().startswith( - "development status" - ): - self.preview = int(v.split(":: ")[-1][0]) < 3 - break @classmethod def discover( diff --git a/npe2/manifest/spdx.txt b/npe2/manifest/spdx.txt deleted file mode 100644 index 82e072d4..00000000 --- a/npe2/manifest/spdx.txt +++ /dev/null @@ -1,430 +0,0 @@ -0BSD -AAL -Abstyles -Adobe-2006 -Adobe-Glyph -ADSL -AFL-1.1 -AFL-1.2 -AFL-2.0 -AFL-2.1 -AFL-3.0 -Afmparse -AGPL-1.0-only -AGPL-1.0-or-later -AGPL-3.0-only -AGPL-3.0-or-later -Aladdin -AMDPLPA -AML -AMPAS -ANTLR-PD -ANTLR-PD-fallback -Apache-1.0 -Apache-1.1 -Apache-2.0 -APAFML -APL-1.0 -APSL-1.0 -APSL-1.1 -APSL-1.2 -APSL-2.0 -Artistic-1.0 -Artistic-1.0-cl8 -Artistic-1.0-Perl -Artistic-2.0 -Bahyph -Barr -Beerware -BitTorrent-1.0 -BitTorrent-1.1 -blessing -BlueOak-1.0.0 -Borceux -BSD-1-Clause -BSD-2-Clause -BSD-2-Clause-Patent -BSD-2-Clause-Views -BSD-3-Clause -BSD-3-Clause-Attribution -BSD-3-Clause-Clear -BSD-3-Clause-LBNL -BSD-3-Clause-Modification -BSD-3-Clause-No-Military-License -BSD-3-Clause-No-Nuclear-License -BSD-3-Clause-No-Nuclear-License-2014 -BSD-3-Clause-No-Nuclear-Warranty -BSD-3-Clause-Open-MPI -BSD-4-Clause -BSD-4-Clause-Shortened -BSD-4-Clause-UC -BSD-Protection -BSD-Source-Code -BSL-1.0 -BUSL-1.1 -bzip2-1.0.5 -bzip2-1.0.6 -C-UDA-1.0 -CAL-1.0 -CAL-1.0-Combined-Work-Exception -Caldera -CATOSL-1.1 -CC-BY-1.0 -CC-BY-2.0 -CC-BY-2.5 -CC-BY-3.0 -CC-BY-3.0-AT -CC-BY-3.0-US -CC-BY-4.0 -CC-BY-NC-1.0 -CC-BY-NC-2.0 -CC-BY-NC-2.5 -CC-BY-NC-3.0 -CC-BY-NC-4.0 -CC-BY-NC-ND-1.0 -CC-BY-NC-ND-2.0 -CC-BY-NC-ND-2.5 -CC-BY-NC-ND-3.0 -CC-BY-NC-ND-3.0-IGO -CC-BY-NC-ND-4.0 -CC-BY-NC-SA-1.0 -CC-BY-NC-SA-2.0 -CC-BY-NC-SA-2.5 -CC-BY-NC-SA-3.0 -CC-BY-NC-SA-4.0 -CC-BY-ND-1.0 -CC-BY-ND-2.0 -CC-BY-ND-2.5 -CC-BY-ND-3.0 -CC-BY-ND-4.0 -CC-BY-SA-1.0 -CC-BY-SA-2.0 -CC-BY-SA-2.0-UK -CC-BY-SA-2.1-JP -CC-BY-SA-2.5 -CC-BY-SA-3.0 -CC-BY-SA-3.0-AT -CC-BY-SA-4.0 -CC-PDDC -CC0-1.0 -CDDL-1.0 -CDDL-1.1 -CDL-1.0 -CDLA-Permissive-1.0 -CDLA-Sharing-1.0 -CECILL-1.0 -CECILL-1.1 -CECILL-2.0 -CECILL-2.1 -CECILL-B -CECILL-C -CERN-OHL-1.1 -CERN-OHL-1.2 -CERN-OHL-P-2.0 -CERN-OHL-S-2.0 -CERN-OHL-W-2.0 -ClArtistic -CNRI-Jython -CNRI-Python -CNRI-Python-GPL-Compatible -Condor-1.1 -copyleft-next-0.3.0 -copyleft-next-0.3.1 -CPAL-1.0 -CPL-1.0 -CPOL-1.02 -Crossword -CrystalStacker -CUA-OPL-1.0 -Cube -curl -D-FSL-1.0 -diffmark -DOC -Dotseqn -DRL-1.0 -DSDP -dvipdfm -ECL-1.0 -ECL-2.0 -EFL-1.0 -EFL-2.0 -eGenix -Entessa -EPICS -EPL-1.0 -EPL-2.0 -ErlPL-1.1 -etalab-2.0 -EUDatagrid -EUPL-1.0 -EUPL-1.1 -EUPL-1.2 -Eurosym -Fair -Frameworx-1.0 -FreeBSD-DOC -FreeImage -FSFAP -FSFUL -FSFULLR -FTL -GD -GFDL-1.1-invariants-only -GFDL-1.1-invariants-or-later -GFDL-1.1-no-invariants-only -GFDL-1.1-no-invariants-or-later -GFDL-1.1-only -GFDL-1.1-or-later -GFDL-1.2-invariants-only -GFDL-1.2-invariants-or-later -GFDL-1.2-no-invariants-only -GFDL-1.2-no-invariants-or-later -GFDL-1.2-only -GFDL-1.2-or-later -GFDL-1.3-invariants-only -GFDL-1.3-invariants-or-later -GFDL-1.3-no-invariants-only -GFDL-1.3-no-invariants-or-later -GFDL-1.3-only -GFDL-1.3-or-later -Giftware -GL2PS -Glide -Glulxe -GLWTPL -gnuplot -GPL-1.0-only -GPL-1.0-or-later -GPL-2.0-only -GPL-2.0-or-later -GPL-3.0-only -GPL-3.0-or-later -gSOAP-1.3b -HaskellReport -Hippocratic-2.1 -HPND -HPND-sell-variant -HTMLTIDY -IBM-pibs -ICU -IJG -ImageMagick -iMatix -Imlib2 -Info-ZIP -Intel -Intel-ACPI -Interbase-1.0 -IPA -IPL-1.0 -ISC -JasPer-2.0 -JPNIC -JSON -LAL-1.2 -LAL-1.3 -Latex2e -Leptonica -LGPL-2.0-only -LGPL-2.0-or-later -LGPL-2.1-only -LGPL-2.1-or-later -LGPL-3.0-only -LGPL-3.0-or-later -LGPLLR -Libpng -libpng-2.0 -libselinux-1.0 -libtiff -LiLiQ-P-1.1 -LiLiQ-R-1.1 -LiLiQ-Rplus-1.1 -Linux-OpenIB -LPL-1.0 -LPL-1.02 -LPPL-1.0 -LPPL-1.1 -LPPL-1.2 -LPPL-1.3a -LPPL-1.3c -MakeIndex -MirOS -MIT -MIT-0 -MIT-advertising -MIT-CMU -MIT-enna -MIT-feh -MIT-Modern-Variant -MIT-open-group -MITNFA -Motosoto -mpich2 -MPL-1.0 -MPL-1.1 -MPL-2.0 -MPL-2.0-no-copyleft-exception -MS-PL -MS-RL -MTLL -MulanPSL-1.0 -MulanPSL-2.0 -Multics -Mup -NAIST-2003 -NASA-1.3 -Naumen -NBPL-1.0 -NCGL-UK-2.0 -NCSA -Net-SNMP -NetCDF -Newsletr -NGPL -NIST-PD -NIST-PD-fallback -NLOD-1.0 -NLPL -Nokia -NOSL -Noweb -NPL-1.0 -NPL-1.1 -NPOSL-3.0 -NRL -NTP -NTP-0 -O-UDA-1.0 -OCCT-PL -OCLC-2.0 -ODbL-1.0 -ODC-By-1.0 -OFL-1.0 -OFL-1.0-no-RFN -OFL-1.0-RFN -OFL-1.1 -OFL-1.1-no-RFN -OFL-1.1-RFN -OGC-1.0 -OGDL-Taiwan-1.0 -OGL-Canada-2.0 -OGL-UK-1.0 -OGL-UK-2.0 -OGL-UK-3.0 -OGTSL -OLDAP-1.1 -OLDAP-1.2 -OLDAP-1.3 -OLDAP-1.4 -OLDAP-2.0 -OLDAP-2.0.1 -OLDAP-2.1 -OLDAP-2.2 -OLDAP-2.2.1 -OLDAP-2.2.2 -OLDAP-2.3 -OLDAP-2.4 -OLDAP-2.5 -OLDAP-2.6 -OLDAP-2.7 -OLDAP-2.8 -OML -OpenSSL -OPL-1.0 -OSET-PL-2.1 -OSL-1.0 -OSL-1.1 -OSL-2.0 -OSL-2.1 -OSL-3.0 -Parity-6.0.0 -Parity-7.0.0 -PDDL-1.0 -PHP-3.0 -PHP-3.01 -Plexus -PolyForm-Noncommercial-1.0.0 -PolyForm-Small-Business-1.0.0 -PostgreSQL -PSF-2.0 -psfrag -psutils -Python-2.0 -Qhull -QPL-1.0 -Rdisc -RHeCos-1.1 -RPL-1.1 -RPL-1.5 -RPSL-1.0 -RSA-MD -RSCPL -Ruby -SAX-PD -Saxpath -SCEA -Sendmail -Sendmail-8.23 -SGI-B-1.0 -SGI-B-1.1 -SGI-B-2.0 -SHL-0.5 -SHL-0.51 -SimPL-2.0 -SISSL -SISSL-1.2 -Sleepycat -SMLNJ -SMPPL -SNIA -Spencer-86 -Spencer-94 -Spencer-99 -SPL-1.0 -SSH-OpenSSH -SSH-short -SSPL-1.0 -SugarCRM-1.1.3 -SWL -TAPR-OHL-1.0 -TCL -TCP-wrappers -TMate -TORQUE-1.1 -TOSL -TU-Berlin-1.0 -TU-Berlin-2.0 -UCL-1.0 -Unicode-DFS-2015 -Unicode-DFS-2016 -Unicode-TOU -Unlicense -UPL-1.0 -Vim -VOSTROM -VSL-1.0 -W3C -W3C-19980720 -W3C-20150513 -Watcom-1.0 -Wsuipa -WTFPL -X11 -Xerox -XFree86-1.1 -xinetd -Xnet -xpp -XSkat -YPL-1.0 -YPL-1.1 -Zed -Zend-2.0 -Zimbra-1.3 -Zimbra-1.4 -Zlib -zlib-acknowledgement -ZPL-1.1 -ZPL-2.0 -ZPL-2.1 diff --git a/tests/sample/my_plugin/napari.yaml b/tests/sample/my_plugin/napari.yaml index f181f760..a4de60b3 100644 --- a/tests/sample/my_plugin/napari.yaml +++ b/tests/sample/my_plugin/napari.yaml @@ -1,7 +1,6 @@ name: my_plugin publisher: publisher display_name: My Plugin -license: BSD-3-Clause entry_point: my_plugin contributions: commands: From fb71bc398275ad1386445928d1c92857afe82499 Mon Sep 17 00:00:00 2001 From: Nathan Clack Date: Wed, 24 Nov 2021 14:52:22 -0800 Subject: [PATCH 02/11] Some more cleanup --- npe2/_command_registry.py | 4 ---- npe2/_from_npe1.py | 14 ++++++++++---- npe2/_plugin_manager.py | 11 ++++++----- npe2/manifest/schema.py | 4 +++- tests/test_conversion.py | 5 +++++ 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/npe2/_command_registry.py b/npe2/_command_registry.py index f7c5cd23..944e8f0c 100644 --- a/npe2/_command_registry.py +++ b/npe2/_command_registry.py @@ -27,9 +27,6 @@ def register(self, id: str, command: Callable) -> PDisposable: if not callable(command): raise TypeError(f"Cannot register non-callable command: {command}") - # TODO: validate argumemnts and type constraints - # possibly wrap command in a type validator? - self._commands[id] = command self.commandRegistered.emit(id) @@ -56,7 +53,6 @@ def unregister(self, id: str): self._commands.pop(id, None) def get(self, id: str) -> Callable: - # FIXME: who should control activation? if id not in self._commands: from ._plugin_manager import PluginManager diff --git a/npe2/_from_npe1.py b/npe2/_from_npe1.py index f84f1b88..c6f28a24 100644 --- a/npe2/_from_npe1.py +++ b/npe2/_from_npe1.py @@ -80,13 +80,19 @@ def manifest_from_npe1( plugin_name = cast(str, plugin_name) if not plugin_manager.is_registered(plugin_name): - # TODO: it would be nice to add some logic to prevent confusion here. - # for example... if the plugin name doesn't equal the package name, we - # should still be able to find it if the user gives a package name + # "plugin name" is not necessarily the package name. If the user + # supplies the package name, try to look it up and see if it's a plugin. try: dist = distribution(plugin_name) # returns a list. multiple plugins? - plugin_name = dist.entry_points[0].name + plugin_name = next( + e.name for e in dist.entry_points if e.group == "napari.plugin" + ) + except StopIteration: + raise RuntimeError( + f"Could not find plugin {plugin_name!r}. Found a package by " + "that name but lacked the `napari.plugin` entry point group." + ) except PackageNotFoundError: raise PackageNotFoundError( f"Could not find plugin {plugin_name!r}\n" diff --git a/npe2/_plugin_manager.py b/npe2/_plugin_manager.py index a3a37cdc..bf8b113f 100644 --- a/npe2/_plugin_manager.py +++ b/npe2/_plugin_manager.py @@ -52,7 +52,7 @@ def __getitem__(self, index: Union[int, slice]) -> Set[Interval[T]]: ... -PluginName = str # this is defined on PluginManifest as `publisher.name` +PluginName = str # this is defined on PluginManifest as `manifest.name` class _ContributionsIndex: @@ -96,7 +96,7 @@ def __init__(self, reg: Optional[CommandRegistry] = None) -> None: self._command_registry = reg or CommandRegistry() self._contexts: Dict[PluginName, PluginContext] = {} self._manifests: Dict[PluginName, PluginManifest] = {} - self.discover() # TODO: should we be immediately discovering? + self.discover() @property def commands(self) -> CommandRegistry: @@ -154,7 +154,6 @@ def activate(self, key: PluginName) -> PluginContext: - otherwise calls the plugin's activate() function, passing the Context. - imports any commands that were declared as python_name: """ - # TODO: this is an important function... should be carefully considered try: mf = self._manifests[key] except KeyError: @@ -318,8 +317,10 @@ def get_writer( elif not ext and len(layer_types) == 1: # No extension, single layer. ext = next(iter(writer.filename_extensions), "") return writer, path + ext - else: - raise ValueError + # When the list of extensions for the writer doesn't match the + # extension in the filename, keep searching. + + # Nothing got found return None, path diff --git a/npe2/manifest/schema.py b/npe2/manifest/schema.py index 50993cb5..da572345 100644 --- a/npe2/manifest/schema.py +++ b/npe2/manifest/schema.py @@ -56,7 +56,9 @@ class PluginManifest(BaseModel): publisher: Optional[str] = Field( None, - description="The publisher name - can be an individual or an organization", + description="The publisher name - can be an individual or an " + "organization. When unspecified, the description is take from the " + "Author field of the package metadata.", ) display_name: str = Field( diff --git a/tests/test_conversion.py b/tests/test_conversion.py index 89121037..cffe079e 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -93,3 +93,8 @@ def test_conversion2(): def test_conversion_missing(): with pytest.raises(ModuleNotFoundError), pytest.warns(UserWarning): manifest_from_npe1("does-not-exist-asdf6as987") + + +def test_conversion_package_is_not_a_plugin(): + with pytest.raises(RuntimeError): + manifest_from_npe1("pytest") From 1da7bceab93be36bf83c7a14c9f683eac5211adb Mon Sep 17 00:00:00 2001 From: Nathan Clack Date: Wed, 24 Nov 2021 17:19:19 -0800 Subject: [PATCH 03/11] Change 'publisher' to 'authors' (#39) --- npe2/_from_npe1.py | 2 +- npe2/_plugin_manager.py | 2 +- npe2/manifest/schema.py | 9 ++++----- tests/sample/my_plugin/napari.yaml | 1 - 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/npe2/_from_npe1.py b/npe2/_from_npe1.py index f84f1b88..827d4b55 100644 --- a/npe2/_from_npe1.py +++ b/npe2/_from_npe1.py @@ -102,7 +102,7 @@ def manifest_from_npe1( return PluginManifest( name=package, - publisher=standard_meta.get("author"), + authors=standard_meta.get("author"), description=standard_meta.get("summary"), version=standard_meta.get("version"), contributions=dict(parser.contributions), diff --git a/npe2/_plugin_manager.py b/npe2/_plugin_manager.py index a3a37cdc..f8e68a0f 100644 --- a/npe2/_plugin_manager.py +++ b/npe2/_plugin_manager.py @@ -52,7 +52,7 @@ def __getitem__(self, index: Union[int, slice]) -> Set[Interval[T]]: ... -PluginName = str # this is defined on PluginManifest as `publisher.name` +PluginName = str # this is defined on PluginManifest as `manifest.name` class _ContributionsIndex: diff --git a/npe2/manifest/schema.py b/npe2/manifest/schema.py index a0d028a2..d6a98ff6 100644 --- a/npe2/manifest/schema.py +++ b/npe2/manifest/schema.py @@ -61,12 +61,11 @@ class PluginManifest(BaseModel): ..., description="The name of the plugin - should be all lowercase with no spaces.", ) - # this is not something that has an equivalent on PyPI ... - # it might be a good field with which we can identify trusted source - # but... it's not entire clear how that "trust" gets validated at the moment - publisher: Optional[str] = Field( + + authors: Optional[str] = Field( None, - description="The publisher name - can be an individual or an organization", + description="The author name(s). When unspecified, the description is " + "take from the 'Author' field of the package metadata.", ) display_name: str = Field( diff --git a/tests/sample/my_plugin/napari.yaml b/tests/sample/my_plugin/napari.yaml index f181f760..5a3d3d18 100644 --- a/tests/sample/my_plugin/napari.yaml +++ b/tests/sample/my_plugin/napari.yaml @@ -1,5 +1,4 @@ name: my_plugin -publisher: publisher display_name: My Plugin license: BSD-3-Clause entry_point: my_plugin From 3bf56c5dad689d1e4f943f0ba4ad16cb45fa1a0b Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sat, 27 Nov 2021 09:33:09 -0500 Subject: [PATCH 04/11] Update npe2/_plugin_manager.py --- npe2/_plugin_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npe2/_plugin_manager.py b/npe2/_plugin_manager.py index f8e68a0f..15f64d12 100644 --- a/npe2/_plugin_manager.py +++ b/npe2/_plugin_manager.py @@ -52,7 +52,7 @@ def __getitem__(self, index: Union[int, slice]) -> Set[Interval[T]]: ... -PluginName = str # this is defined on PluginManifest as `manifest.name` +# this is `PluginManifest.name` class _ContributionsIndex: From 7a98b5de5fa9f9decab8034b9625fad78c850f23 Mon Sep 17 00:00:00 2001 From: Nathan Clack Date: Sat, 27 Nov 2021 21:08:31 -0800 Subject: [PATCH 05/11] Update npe2/_from_npe1.py Co-authored-by: Talley Lambert --- npe2/_from_npe1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npe2/_from_npe1.py b/npe2/_from_npe1.py index 827d4b55..c862d07e 100644 --- a/npe2/_from_npe1.py +++ b/npe2/_from_npe1.py @@ -102,7 +102,7 @@ def manifest_from_npe1( return PluginManifest( name=package, - authors=standard_meta.get("author"), + author=standard_meta.get("author"), description=standard_meta.get("summary"), version=standard_meta.get("version"), contributions=dict(parser.contributions), From ff94b398d9587406ea83139837947df6c36608c5 Mon Sep 17 00:00:00 2001 From: Nathan Clack Date: Sat, 27 Nov 2021 21:08:38 -0800 Subject: [PATCH 06/11] Update npe2/manifest/schema.py Co-authored-by: Talley Lambert --- npe2/manifest/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npe2/manifest/schema.py b/npe2/manifest/schema.py index d6a98ff6..05b94c01 100644 --- a/npe2/manifest/schema.py +++ b/npe2/manifest/schema.py @@ -62,7 +62,7 @@ class PluginManifest(BaseModel): description="The name of the plugin - should be all lowercase with no spaces.", ) - authors: Optional[str] = Field( + author: Optional[str] = Field( None, description="The author name(s). When unspecified, the description is " "take from the 'Author' field of the package metadata.", From 514c60cf62271f619adb9941b66e632c6a2765c4 Mon Sep 17 00:00:00 2001 From: Nathan Clack Date: Sun, 28 Nov 2021 12:40:48 -0800 Subject: [PATCH 07/11] Add back some comments --- npe2/_command_registry.py | 3 +++ npe2/_plugin_manager.py | 2 +- npe2/manifest/schema.py | 10 ++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/npe2/_command_registry.py b/npe2/_command_registry.py index 944e8f0c..6dea3b5d 100644 --- a/npe2/_command_registry.py +++ b/npe2/_command_registry.py @@ -27,6 +27,9 @@ def register(self, id: str, command: Callable) -> PDisposable: if not callable(command): raise TypeError(f"Cannot register non-callable command: {command}") + # TODO: validate arguments and type constraints + # possibly wrap command in a type validator? + self._commands[id] = command self.commandRegistered.emit(id) diff --git a/npe2/_plugin_manager.py b/npe2/_plugin_manager.py index bf8b113f..96dcd04f 100644 --- a/npe2/_plugin_manager.py +++ b/npe2/_plugin_manager.py @@ -96,7 +96,7 @@ def __init__(self, reg: Optional[CommandRegistry] = None) -> None: self._command_registry = reg or CommandRegistry() self._contexts: Dict[PluginName, PluginContext] = {} self._manifests: Dict[PluginName, PluginManifest] = {} - self.discover() + self.discover() # TODO: should we be immediately discovering? @property def commands(self) -> CommandRegistry: diff --git a/npe2/manifest/schema.py b/npe2/manifest/schema.py index da572345..4e05decc 100644 --- a/npe2/manifest/schema.py +++ b/npe2/manifest/schema.py @@ -48,6 +48,11 @@ class DiscoverResults(NamedTuple): class PluginManifest(BaseModel): + # VS Code uses . as a unique ID for the extension + # should this just be the package name ... not the module name? (yes) + # do we normalize this? (i.e. underscores / dashes ?) (no) + # TODO: enforce that this matches the package name + name: str = Field( ..., description="The name of the plugin. Should correspond to the python " @@ -75,6 +80,11 @@ class PluginManifest(BaseModel): "When unspecified, the description is taken from package metadata." ) + # TODO: + # Perhaps we should version the plugin interface (not so the manifest, but + # the actual mechanism/consumption of plugin information) independently + # of napari itself + # The module that has the activate() function entry_point: Optional[str] = Field( default=None, From 0967151bf92765eb57bba691235e563b71fb3325 Mon Sep 17 00:00:00 2001 From: Nathan Clack Date: Sun, 28 Nov 2021 14:00:20 -0800 Subject: [PATCH 08/11] fix test --- tests/test_conversion.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_conversion.py b/tests/test_conversion.py index cffe079e..1f1c123d 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -1,3 +1,5 @@ +from importlib.metadata import PackageNotFoundError + import pytest from magicgui import magic_factory from napari_plugin_engine import napari_hook_implementation @@ -96,5 +98,5 @@ def test_conversion_missing(): def test_conversion_package_is_not_a_plugin(): - with pytest.raises(RuntimeError): + with pytest.raises(PackageNotFoundError): manifest_from_npe1("pytest") From faa690053fa1ef2d4b835d62be4ba4b95588a1ed Mon Sep 17 00:00:00 2001 From: Nathan Clack Date: Sun, 28 Nov 2021 14:08:01 -0800 Subject: [PATCH 09/11] fix test --- tests/test_conversion.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_conversion.py b/tests/test_conversion.py index 1f1c123d..64eaff2b 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -1,11 +1,14 @@ -from importlib.metadata import PackageNotFoundError - import pytest from magicgui import magic_factory from napari_plugin_engine import napari_hook_implementation from npe2._from_npe1 import manifest_from_npe1 +try: + from importlib.metadata import PackageNotFoundError +except ImportError: + from importlib_metadata import PackageNotFoundError # type: ignore + def gen_data(): ... From 298ee4176242e494ce285793d42d7c6f9b5f286d Mon Sep 17 00:00:00 2001 From: Nathan Clack Date: Tue, 30 Nov 2021 08:58:39 -0800 Subject: [PATCH 10/11] add back license field (#17) --- npe2/manifest/schema.py | 6 + npe2/manifest/spdx.txt | 430 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 436 insertions(+) create mode 100644 npe2/manifest/spdx.txt diff --git a/npe2/manifest/schema.py b/npe2/manifest/schema.py index a9f86327..50f597ff 100644 --- a/npe2/manifest/schema.py +++ b/npe2/manifest/schema.py @@ -4,6 +4,7 @@ import sys import types from contextlib import contextmanager +from enum import Enum from importlib import import_module, util from logging import getLogger from pathlib import Path @@ -34,6 +35,8 @@ from email.message import Message from importlib.metadata import EntryPoint +spdx_ids = (Path(__file__).parent / "spdx.txt").read_text().splitlines() +SPDX = Enum("SPDX", {i.replace("-", "_"): i for i in spdx_ids}) # type: ignore logger = getLogger(__name__) @@ -92,6 +95,9 @@ class PluginManifest(BaseModel): "the plugin's activate() function.", ) + # this should come from setup.cfg ... but they don't require SPDX + license: Optional[SPDX] = None + version: Optional[str] = Field( None, description="SemVer compatible version. When unspecified the version " diff --git a/npe2/manifest/spdx.txt b/npe2/manifest/spdx.txt new file mode 100644 index 00000000..82e072d4 --- /dev/null +++ b/npe2/manifest/spdx.txt @@ -0,0 +1,430 @@ +0BSD +AAL +Abstyles +Adobe-2006 +Adobe-Glyph +ADSL +AFL-1.1 +AFL-1.2 +AFL-2.0 +AFL-2.1 +AFL-3.0 +Afmparse +AGPL-1.0-only +AGPL-1.0-or-later +AGPL-3.0-only +AGPL-3.0-or-later +Aladdin +AMDPLPA +AML +AMPAS +ANTLR-PD +ANTLR-PD-fallback +Apache-1.0 +Apache-1.1 +Apache-2.0 +APAFML +APL-1.0 +APSL-1.0 +APSL-1.1 +APSL-1.2 +APSL-2.0 +Artistic-1.0 +Artistic-1.0-cl8 +Artistic-1.0-Perl +Artistic-2.0 +Bahyph +Barr +Beerware +BitTorrent-1.0 +BitTorrent-1.1 +blessing +BlueOak-1.0.0 +Borceux +BSD-1-Clause +BSD-2-Clause +BSD-2-Clause-Patent +BSD-2-Clause-Views +BSD-3-Clause +BSD-3-Clause-Attribution +BSD-3-Clause-Clear +BSD-3-Clause-LBNL +BSD-3-Clause-Modification +BSD-3-Clause-No-Military-License +BSD-3-Clause-No-Nuclear-License +BSD-3-Clause-No-Nuclear-License-2014 +BSD-3-Clause-No-Nuclear-Warranty +BSD-3-Clause-Open-MPI +BSD-4-Clause +BSD-4-Clause-Shortened +BSD-4-Clause-UC +BSD-Protection +BSD-Source-Code +BSL-1.0 +BUSL-1.1 +bzip2-1.0.5 +bzip2-1.0.6 +C-UDA-1.0 +CAL-1.0 +CAL-1.0-Combined-Work-Exception +Caldera +CATOSL-1.1 +CC-BY-1.0 +CC-BY-2.0 +CC-BY-2.5 +CC-BY-3.0 +CC-BY-3.0-AT +CC-BY-3.0-US +CC-BY-4.0 +CC-BY-NC-1.0 +CC-BY-NC-2.0 +CC-BY-NC-2.5 +CC-BY-NC-3.0 +CC-BY-NC-4.0 +CC-BY-NC-ND-1.0 +CC-BY-NC-ND-2.0 +CC-BY-NC-ND-2.5 +CC-BY-NC-ND-3.0 +CC-BY-NC-ND-3.0-IGO +CC-BY-NC-ND-4.0 +CC-BY-NC-SA-1.0 +CC-BY-NC-SA-2.0 +CC-BY-NC-SA-2.5 +CC-BY-NC-SA-3.0 +CC-BY-NC-SA-4.0 +CC-BY-ND-1.0 +CC-BY-ND-2.0 +CC-BY-ND-2.5 +CC-BY-ND-3.0 +CC-BY-ND-4.0 +CC-BY-SA-1.0 +CC-BY-SA-2.0 +CC-BY-SA-2.0-UK +CC-BY-SA-2.1-JP +CC-BY-SA-2.5 +CC-BY-SA-3.0 +CC-BY-SA-3.0-AT +CC-BY-SA-4.0 +CC-PDDC +CC0-1.0 +CDDL-1.0 +CDDL-1.1 +CDL-1.0 +CDLA-Permissive-1.0 +CDLA-Sharing-1.0 +CECILL-1.0 +CECILL-1.1 +CECILL-2.0 +CECILL-2.1 +CECILL-B +CECILL-C +CERN-OHL-1.1 +CERN-OHL-1.2 +CERN-OHL-P-2.0 +CERN-OHL-S-2.0 +CERN-OHL-W-2.0 +ClArtistic +CNRI-Jython +CNRI-Python +CNRI-Python-GPL-Compatible +Condor-1.1 +copyleft-next-0.3.0 +copyleft-next-0.3.1 +CPAL-1.0 +CPL-1.0 +CPOL-1.02 +Crossword +CrystalStacker +CUA-OPL-1.0 +Cube +curl +D-FSL-1.0 +diffmark +DOC +Dotseqn +DRL-1.0 +DSDP +dvipdfm +ECL-1.0 +ECL-2.0 +EFL-1.0 +EFL-2.0 +eGenix +Entessa +EPICS +EPL-1.0 +EPL-2.0 +ErlPL-1.1 +etalab-2.0 +EUDatagrid +EUPL-1.0 +EUPL-1.1 +EUPL-1.2 +Eurosym +Fair +Frameworx-1.0 +FreeBSD-DOC +FreeImage +FSFAP +FSFUL +FSFULLR +FTL +GD +GFDL-1.1-invariants-only +GFDL-1.1-invariants-or-later +GFDL-1.1-no-invariants-only +GFDL-1.1-no-invariants-or-later +GFDL-1.1-only +GFDL-1.1-or-later +GFDL-1.2-invariants-only +GFDL-1.2-invariants-or-later +GFDL-1.2-no-invariants-only +GFDL-1.2-no-invariants-or-later +GFDL-1.2-only +GFDL-1.2-or-later +GFDL-1.3-invariants-only +GFDL-1.3-invariants-or-later +GFDL-1.3-no-invariants-only +GFDL-1.3-no-invariants-or-later +GFDL-1.3-only +GFDL-1.3-or-later +Giftware +GL2PS +Glide +Glulxe +GLWTPL +gnuplot +GPL-1.0-only +GPL-1.0-or-later +GPL-2.0-only +GPL-2.0-or-later +GPL-3.0-only +GPL-3.0-or-later +gSOAP-1.3b +HaskellReport +Hippocratic-2.1 +HPND +HPND-sell-variant +HTMLTIDY +IBM-pibs +ICU +IJG +ImageMagick +iMatix +Imlib2 +Info-ZIP +Intel +Intel-ACPI +Interbase-1.0 +IPA +IPL-1.0 +ISC +JasPer-2.0 +JPNIC +JSON +LAL-1.2 +LAL-1.3 +Latex2e +Leptonica +LGPL-2.0-only +LGPL-2.0-or-later +LGPL-2.1-only +LGPL-2.1-or-later +LGPL-3.0-only +LGPL-3.0-or-later +LGPLLR +Libpng +libpng-2.0 +libselinux-1.0 +libtiff +LiLiQ-P-1.1 +LiLiQ-R-1.1 +LiLiQ-Rplus-1.1 +Linux-OpenIB +LPL-1.0 +LPL-1.02 +LPPL-1.0 +LPPL-1.1 +LPPL-1.2 +LPPL-1.3a +LPPL-1.3c +MakeIndex +MirOS +MIT +MIT-0 +MIT-advertising +MIT-CMU +MIT-enna +MIT-feh +MIT-Modern-Variant +MIT-open-group +MITNFA +Motosoto +mpich2 +MPL-1.0 +MPL-1.1 +MPL-2.0 +MPL-2.0-no-copyleft-exception +MS-PL +MS-RL +MTLL +MulanPSL-1.0 +MulanPSL-2.0 +Multics +Mup +NAIST-2003 +NASA-1.3 +Naumen +NBPL-1.0 +NCGL-UK-2.0 +NCSA +Net-SNMP +NetCDF +Newsletr +NGPL +NIST-PD +NIST-PD-fallback +NLOD-1.0 +NLPL +Nokia +NOSL +Noweb +NPL-1.0 +NPL-1.1 +NPOSL-3.0 +NRL +NTP +NTP-0 +O-UDA-1.0 +OCCT-PL +OCLC-2.0 +ODbL-1.0 +ODC-By-1.0 +OFL-1.0 +OFL-1.0-no-RFN +OFL-1.0-RFN +OFL-1.1 +OFL-1.1-no-RFN +OFL-1.1-RFN +OGC-1.0 +OGDL-Taiwan-1.0 +OGL-Canada-2.0 +OGL-UK-1.0 +OGL-UK-2.0 +OGL-UK-3.0 +OGTSL +OLDAP-1.1 +OLDAP-1.2 +OLDAP-1.3 +OLDAP-1.4 +OLDAP-2.0 +OLDAP-2.0.1 +OLDAP-2.1 +OLDAP-2.2 +OLDAP-2.2.1 +OLDAP-2.2.2 +OLDAP-2.3 +OLDAP-2.4 +OLDAP-2.5 +OLDAP-2.6 +OLDAP-2.7 +OLDAP-2.8 +OML +OpenSSL +OPL-1.0 +OSET-PL-2.1 +OSL-1.0 +OSL-1.1 +OSL-2.0 +OSL-2.1 +OSL-3.0 +Parity-6.0.0 +Parity-7.0.0 +PDDL-1.0 +PHP-3.0 +PHP-3.01 +Plexus +PolyForm-Noncommercial-1.0.0 +PolyForm-Small-Business-1.0.0 +PostgreSQL +PSF-2.0 +psfrag +psutils +Python-2.0 +Qhull +QPL-1.0 +Rdisc +RHeCos-1.1 +RPL-1.1 +RPL-1.5 +RPSL-1.0 +RSA-MD +RSCPL +Ruby +SAX-PD +Saxpath +SCEA +Sendmail +Sendmail-8.23 +SGI-B-1.0 +SGI-B-1.1 +SGI-B-2.0 +SHL-0.5 +SHL-0.51 +SimPL-2.0 +SISSL +SISSL-1.2 +Sleepycat +SMLNJ +SMPPL +SNIA +Spencer-86 +Spencer-94 +Spencer-99 +SPL-1.0 +SSH-OpenSSH +SSH-short +SSPL-1.0 +SugarCRM-1.1.3 +SWL +TAPR-OHL-1.0 +TCL +TCP-wrappers +TMate +TORQUE-1.1 +TOSL +TU-Berlin-1.0 +TU-Berlin-2.0 +UCL-1.0 +Unicode-DFS-2015 +Unicode-DFS-2016 +Unicode-TOU +Unlicense +UPL-1.0 +Vim +VOSTROM +VSL-1.0 +W3C +W3C-19980720 +W3C-20150513 +Watcom-1.0 +Wsuipa +WTFPL +X11 +Xerox +XFree86-1.1 +xinetd +Xnet +xpp +XSkat +YPL-1.0 +YPL-1.1 +Zed +Zend-2.0 +Zimbra-1.3 +Zimbra-1.4 +Zlib +zlib-acknowledgement +ZPL-1.1 +ZPL-2.0 +ZPL-2.1 From 01e03be8ae881d177913d8121f0ec95b2a516006 Mon Sep 17 00:00:00 2001 From: Nathan Clack Date: Tue, 30 Nov 2021 09:09:06 -0800 Subject: [PATCH 11/11] add back license field try2 (#17) --- npe2/manifest/schema.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/npe2/manifest/schema.py b/npe2/manifest/schema.py index 50f597ff..b8343002 100644 --- a/npe2/manifest/schema.py +++ b/npe2/manifest/schema.py @@ -254,6 +254,8 @@ def _populate_missing_meta(self, metadata: Message): self.version = metadata["Version"] if not self.description: self.description = metadata["Summary"] + if not self.license: + self.license = metadata["License"] @classmethod def discover(