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/_command_registry.py b/npe2/_command_registry.py index f7c5cd23..b35c6355 100644 --- a/npe2/_command_registry.py +++ b/npe2/_command_registry.py @@ -27,7 +27,7 @@ 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 + # TODO: validate arguments and type constraints # possibly wrap command in a type validator? self._commands[id] = command diff --git a/npe2/_from_npe1.py b/npe2/_from_npe1.py index c862d07e..302c0625 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 + dist = distribution(plugin_name) + plugin_name = next( + e.name for e in dist.entry_points if e.group == "napari.plugin" + ) + except StopIteration: + raise PackageNotFoundError( + f"Could not find plugin {plugin_name!r}. Found a package by " + "that name but it 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 15f64d12..455eda41 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]]: ... -# this is `PluginManifest.name` +PluginName = str # this is `PluginManifest.name` class _ContributionsIndex: @@ -318,8 +318,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 05b94c01..b8343002 100644 --- a/npe2/manifest/schema.py +++ b/npe2/manifest/schema.py @@ -14,7 +14,6 @@ Any, Callable, Iterator, - List, NamedTuple, Optional, Sequence, @@ -36,7 +35,6 @@ 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 @@ -54,12 +52,14 @@ 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 ?) + # 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 be all lowercase with no spaces.", + description="The name of the plugin. Should correspond to the python " + "package name for this plugin.", ) author: Optional[str] = Field( @@ -76,9 +76,10 @@ 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: @@ -86,45 +87,24 @@ class PluginManifest(BaseModel): # 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 + # this should come from setup.cfg ... but they don't require 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 @@ -276,13 +256,6 @@ def _populate_missing_meta(self, metadata: Message): 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/tests/sample/my_plugin/napari.yaml b/tests/sample/my_plugin/napari.yaml index 5a3d3d18..ad4c7446 100644 --- a/tests/sample/my_plugin/napari.yaml +++ b/tests/sample/my_plugin/napari.yaml @@ -1,6 +1,5 @@ name: my_plugin display_name: My Plugin -license: BSD-3-Clause entry_point: my_plugin contributions: commands: diff --git a/tests/test_conversion.py b/tests/test_conversion.py index 89121037..64eaff2b 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -4,6 +4,11 @@ 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(): ... @@ -93,3 +98,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(PackageNotFoundError): + manifest_from_npe1("pytest")