diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2c112ff..4552d71a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,13 +51,13 @@ jobs: - name: Test run: | coverage run --source=npe2 -m pytest --color yes - coverage xml - coverage report --show-missing - - name: Coverage - uses: codecov/codecov-action@v3 + - name: Upload coverage as artifact + uses: actions/upload-artifact@v3 with: - fail_ci_if_error: true + name: coverage reports + path: | + ./.coverage.* test_napari: name: napari tests @@ -104,6 +104,45 @@ jobs: env: NPE2_SCHEMA: "_schema.json" + upload_coverage: + needs: test + name: Upload coverage + if: always() + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + cache-dependency-path: setup.cfg + cache: 'pip' + + - name: Install Dependencies + run: | + pip install --upgrade pip + pip install codecov + + - name: Download coverage data + uses: actions/download-artifact@v3 + with: + name: coverage reports + path: coverage + + - name: combine coverage data + run: | + python -Im coverage combine coverage + python -Im coverage xml -o coverage.xml + + # Report and write to summary. + python -Im coverage report --format=markdown --skip-empty --skip-covered >> $GITHUB_STEP_SUMMARY + + - name: Upload coverage data + uses: codecov/codecov-action@v3 + with: + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} + deploy: name: Deploy needs: test diff --git a/pyproject.toml b/pyproject.toml index 7b7f65e4..3177397e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,6 +134,7 @@ pretty = true [tool.coverage.run] +parallel = true source = ["src"] omit = [ "src/npe2/manifest/contributions/_keybindings.py", @@ -145,6 +146,14 @@ omit = [ "src/npe2/_setuptools_plugin.py", ] +[tool.coverage.paths] +source = [ + "src", + "/Users/runner/work/npe2/npe2/src", + "/home/runner/work/npe2/npe2/src", + "D:\\a\\npe2\\npe2\\src", +] + # https://coverage.readthedocs.io/en/6.4/config.html [tool.coverage.report] exclude_lines = [ diff --git a/src/npe2/_inspection/_fetch.py b/src/npe2/_inspection/_fetch.py index 95a18826..b6cd45b8 100644 --- a/src/npe2/_inspection/_fetch.py +++ b/src/npe2/_inspection/_fetch.py @@ -204,8 +204,16 @@ def _get_manifest_from_git_url(url: str) -> PluginManifest: if url.startswith("git+"): url = url[4:] + branch = "" + if "@" in url: + url, branch = url.split("@") + with tempfile.TemporaryDirectory() as td: subprocess.run(["git", "clone", url, td], stdout=subprocess.DEVNULL) + if branch: + subprocess.run( + ["git", "checkout", branch], cwd=td, stdout=subprocess.DEVNULL + ) return _build_src_and_extract_manifest(td) diff --git a/src/npe2/_inspection/_full_install.py b/src/npe2/_inspection/_full_install.py deleted file mode 100644 index 27f59cdc..00000000 --- a/src/npe2/_inspection/_full_install.py +++ /dev/null @@ -1,121 +0,0 @@ -"""This module is mostly superceded by the static NPE1ModuleVisitor pattern. - -It is left here for reference, but could be removed in the future. -""" -from __future__ import annotations - -import site -import sys -import warnings -from contextlib import contextmanager -from importlib import metadata -from logging import getLogger -from typing import TYPE_CHECKING, Iterator, Optional - -if TYPE_CHECKING: - from build.env import IsolatedEnv - - from npe2.manifest import PluginManifest - -logger = getLogger(__name__) -__all__ = [ - "fetch_manifest_with_full_install", - "isolated_plugin_env", -] - -NPE1_ENTRY_POINT = "napari.plugin" -NPE2_ENTRY_POINT = "napari.manifest" - - -@contextmanager -def isolated_plugin_env( - package: str, - version: Optional[str] = None, - validate_npe1_imports: bool = True, - install_napari_if_necessary: bool = True, -) -> Iterator[IsolatedEnv]: - """Isolated env context with a plugin installed. - - The site-packages folder of the env is added to sys.path within the context. - - Parameters - ---------- - package : str - package name - version : Optional[str] - package version, by default, latest version. - validate_npe1_imports: bool - Whether to try to import an npe1 plugin's entry points. by default True. - install_napari_if_necessary: bool - If `validate_npe1_imports` is True, whether to install napari if the import - fails. (It's not uncommon for plugins to fail to specify napari as a - dependency. Othertimes, they simply need a qt backend.). by default True. - - Yields - ------ - build.env.IsolatedEnv - env object that has an `install` method. - """ - # it's important that this import be lazy, otherwise we'll get a circular - # import when serving as a setuptools plugin with `python -m build` - from build.env import DefaultIsolatedEnv - - with DefaultIsolatedEnv() as env: - # install the package - pkg = f"{package}=={version}" if version else package - logger.debug(f"installing {pkg} into virtual env") - env.install([pkg]) - - # temporarily add env site packages to path - prefixes = [getattr(env, "path")] # noqa - if not (site_pkgs := site.getsitepackages(prefixes=prefixes)): - raise ValueError("No site-packages found") # pragma: no cover - sys.path.insert(0, site_pkgs[0]) - try: - if validate_npe1_imports: - # try to import the plugin's entry points - dist = metadata.distribution(package) - ep_groups = {ep.group for ep in dist.entry_points} - if NPE1_ENTRY_POINT in ep_groups and NPE2_ENTRY_POINT not in ep_groups: - try: - _get_loaded_mf_or_die(package) - except Exception: # pragma: no cover - # if loading contributions fails, it can very often be fixed - # by installing `napari[all]` into the environment - if install_napari_if_necessary: - env.install(["napari[all]"]) - # force reloading of qtpy - sys.modules.pop("qtpy", None) - _get_loaded_mf_or_die(package) - else: - raise - yield env - finally: - # cleanup sys.path - sys.path.pop(0) - - -def _get_loaded_mf_or_die(package: str) -> PluginManifest: - """Return a fully loaded (if npe1) manifest, or raise an exception.""" - from npe2 import PluginManifest - from npe2.manifest._npe1_adapter import NPE1Adapter - - mf = PluginManifest.from_distribution(package) - if isinstance(mf, NPE1Adapter): - with warnings.catch_warnings(): - warnings.filterwarnings("error", message="Error importing contributions") - warnings.filterwarnings("error", message="Failed to convert") - warnings.filterwarnings("ignore", message="Found a multi-layer writer") - mf._load_contributions(save=False) - return mf - - -def fetch_manifest_with_full_install( - package: str, version: Optional[str] = None -) -> PluginManifest: - """Fetch manifest for plugin by installing into an isolated environment.""" - # create an isolated env in which to install npe1 plugin - with isolated_plugin_env( - package, version, validate_npe1_imports=True, install_napari_if_necessary=True - ): - return _get_loaded_mf_or_die(package) diff --git a/tests/test_all_plugins.py b/tests/test_all_plugins.py index deff755b..16aa251a 100644 --- a/tests/test_all_plugins.py +++ b/tests/test_all_plugins.py @@ -1,11 +1,8 @@ import os -from importlib import metadata -from subprocess import CalledProcessError from typing import TYPE_CHECKING import pytest -from npe2._inspection._full_install import isolated_plugin_env from npe2.cli import app if TYPE_CHECKING: @@ -16,34 +13,6 @@ pytest.skip("skipping plugin specific tests", allow_module_level=True) -@pytest.fixture(scope="session") -def plugin_env(): - try: - with isolated_plugin_env(PLUGIN) as env: - yield env - except CalledProcessError as e: - if "Failed building wheel" in str(e.output): - yield None - - -def test_entry_points_importable(plugin_env): - if plugin_env is None: - pytest.mark.xfail() - return - - entry_points = [ - ep - for ep in metadata.distribution(PLUGIN).entry_points - if ep.group in ("napari.plugin", "napari.manifest") - ] - if PLUGIN not in {"napari-console", "napari-error-reporter"}: - assert entry_points - - for ep in entry_points: - if ep.group == "napari.plugin": - ep.load() - - def test_fetch(tmp_path: "Path"): from typer.testing import CliRunner diff --git a/tests/test_fetch.py b/tests/test_fetch.py index 241b8569..a404edd8 100644 --- a/tests/test_fetch.py +++ b/tests/test_fetch.py @@ -13,8 +13,6 @@ get_pypi_plugins, get_pypi_url, ) -from npe2._inspection._full_install import fetch_manifest_with_full_install -from npe2.manifest._npe1_adapter import NPE1Adapter def test_fetch_npe2_manifest(): @@ -26,7 +24,7 @@ def test_fetch_npe2_manifest(): @pytest.mark.skip("package looks deleted from pypi") def test_fetch_npe1_manifest_with_writer(): - mf = fetch_manifest("example-plugin") + mf = fetch_manifest("dummy-test-plugin", version="0.1.3") assert mf.name == "example-plugin" assert mf.contributions.writers # Test will eventually fail when example-plugin is updated to npe2 @@ -69,16 +67,6 @@ def test_from_pypi_wheel_bdist_missing(): fetch_manifest("my-package") -@pytest.mark.skipif(not os.getenv("CI"), reason="slow, only run on CI") -def testfetch_manifest_with_full_install(): - # TODO: slowest of the tests ... would be nice to provide a local mock - mf = fetch_manifest_with_full_install("napari-ndtiffs", version="0.1.2") - # use version 0.1.2 which is npe1 - assert isinstance(mf, NPE1Adapter) - assert mf.name == "napari-ndtiffs" - assert mf.contributions - - @pytest.mark.skipif(not os.getenv("CI"), reason="slow, only run on CI") def test_manifest_from_sdist(): mf = _manifest_from_pypi_sdist("zarpaint") @@ -109,7 +97,7 @@ def test_get_pypi_plugins(): [ "https://files.pythonhosted.org/packages/fb/01/e59bc1d6ac96f84ce9d7a46cc5422250e047958ead6c5693ed386cf94003/napari_dv-0.3.0.tar.gz", # noqa "https://files.pythonhosted.org/packages/5d/ae/17779e12ce60d8329306963e1a8dec608465caee582440011ff0c1310715/example_plugin-0.0.7-py3-none-any.whl", # noqa - # "git+https://github.com/DragaDoncila/example-plugin.git", Draga hide package + "git+https://github.com/napari/dummy-test-plugin.git@npe1", # this one doesn't use setuptools_scm, can check direct zip without clone "https://github.com/jo-mueller/napari-stl-exporter/archive/refs/heads/main.zip", ], diff --git a/tests/test_setuptools_plugin.py b/tests/test_setuptools_plugin.py index 4780fbcf..b2502009 100644 --- a/tests/test_setuptools_plugin.py +++ b/tests/test_setuptools_plugin.py @@ -18,7 +18,9 @@ [tool.npe2] template="{TEMPLATE}" -""" +""".replace( + "\\", "\\\\" +) @pytest.mark.skipif(not os.getenv("CI"), reason="slow, only run on CI")