From 1eb09678f3659e9c577430b737b23906712a1c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Wed, 17 Aug 2022 18:59:53 +0200 Subject: [PATCH 01/16] Set installability analysis workflow --- .github/workflows/installability.yml | 37 +++++++++++ ci_scripts/check_installability_plugins.py | 75 ++++++++++++++++++++++ environments/installability.yml | 7 ++ 3 files changed, 119 insertions(+) create mode 100644 .github/workflows/installability.yml create mode 100644 ci_scripts/check_installability_plugins.py create mode 100644 environments/installability.yml diff --git a/.github/workflows/installability.yml b/.github/workflows/installability.yml new file mode 100644 index 00000000..a43cea4d --- /dev/null +++ b/.github/workflows/installability.yml @@ -0,0 +1,37 @@ +name: Plugins + +on: + schedule: + # Every Monday and Thursday, at 6am + - "0 6 * * 1,4" + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + pull_request: + branches: + - main + +jobs: + packages: + name: Installability + runs-on: ubuntu-latest + if: startsWith(github.repository, 'napari') + strategy: + fail-fast: false + matrix: + subdir: + - "linux-64" + - "osx-64" + - "osx-arm64" + - "win-64" + + steps: + - uses: actions/checkout@v2 + - uses: mamba-org/provision-with-micromamba@main + with: + environment-file: environments/installability.yml + - name: Run script + shell: bash -el {0} + env: + CONDA_SUBDIR: ${{ matrix.subdir }} + run: python ci_scripts/check_installability_plugins.py + diff --git a/ci_scripts/check_installability_plugins.py b/ci_scripts/check_installability_plugins.py new file mode 100644 index 00000000..d7092645 --- /dev/null +++ b/ci_scripts/check_installability_plugins.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python + +""" +Check if all Hub-listed plugins can be installed together with latest napari +""" + +import json +from subprocess import CalledProcessError, check_output, STDOUT + +import requests + +NPE2API_CONDA = "https://npe2api.vercel.app/api/conda" + + +def check_output_verbose(*args, **kwargs): + """ + Check return code of a subprocess, capturing stdout and stderr. + If an error happened, print both streams, then raise. + """ + kwargs.pop("stderr", None) + kwargs.pop("text", None) + kwargs.pop("universal_newlines", None) + try: + return check_output(*args, stderr=STDOUT, text=True, **kwargs) + except CalledProcessError as exc: + print("Output:") + print(exc.stdout) + raise + + +def latest_napari_on_conda_forge(): + r = requests.get("https://api.anaconda.org/package/conda-forge/napari") + r.raise_for_status() + return r.json()["latest_version"] + + +def all_plugin_names(): + r = requests.get(NPE2API_CONDA) + r.raise_for_status() + return r.json() + + +def is_latest_version(name, version): + r = requests.get(f"{NPE2API_CONDA}/{name}") + r.raise_for_status() + latest_version = r.json()["latest_version"] + return version >= latest_version + + +def solve(specs): + resp = check_output_verbose( + [ + "micromamba", + "create", + "-n", + "notused", + "--dry-run", + "-c", + "conda-forge", + "--json", + *specs, + ] + ) + print(resp) + return json.loads(resp) + + +def main(): + specs = [f"napari={latest_napari_on_conda_forge()}"] + plugin_names = all_plugin_names() + for _, conda_name in plugin_names.items(): + if conda_name is not None: + specs.append(conda_name.replace("/", "::")) + + result = solve(specs) diff --git a/environments/installability.yml b/environments/installability.yml new file mode 100644 index 00000000..9ece0d74 --- /dev/null +++ b/environments/installability.yml @@ -0,0 +1,7 @@ +name: installability +channels: + - conda-forge +dependencies: + - conda + - mamba + - python \ No newline at end of file From b27c712511a87e9299f887a012bdb853fb050e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Wed, 17 Aug 2022 19:03:39 +0200 Subject: [PATCH 02/16] missing token --- .github/workflows/installability.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/installability.yml b/.github/workflows/installability.yml index a43cea4d..d30a2962 100644 --- a/.github/workflows/installability.yml +++ b/.github/workflows/installability.yml @@ -3,7 +3,7 @@ name: Plugins on: schedule: # Every Monday and Thursday, at 6am - - "0 6 * * 1,4" + - cron: "0 6 * * 1,4" # Allows you to run this workflow manually from the Actions tab workflow_dispatch: pull_request: From d9a2c1b60b3318e354280dc6bf4d072e9d246fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Wed, 17 Aug 2022 19:07:16 +0200 Subject: [PATCH 03/16] do call the function --- ci_scripts/check_installability_plugins.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ci_scripts/check_installability_plugins.py b/ci_scripts/check_installability_plugins.py index d7092645..54ae314c 100644 --- a/ci_scripts/check_installability_plugins.py +++ b/ci_scripts/check_installability_plugins.py @@ -73,3 +73,7 @@ def main(): specs.append(conda_name.replace("/", "::")) result = solve(specs) + + +if __name__ == "__main__": + main() From 4c50448179a7bf647f1d3d3a8a34338fd7d2613a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Thu, 8 Sep 2022 12:37:17 +0200 Subject: [PATCH 04/16] Add python versions too --- .github/workflows/installability.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/installability.yml b/.github/workflows/installability.yml index d30a2962..115975e1 100644 --- a/.github/workflows/installability.yml +++ b/.github/workflows/installability.yml @@ -12,7 +12,7 @@ on: jobs: packages: - name: Installability + name: Installability, ${{ matrix.subdir }}, Python ${{ matrix.python-version }} runs-on: ubuntu-latest if: startsWith(github.repository, 'napari') strategy: @@ -23,12 +23,18 @@ jobs: - "osx-64" - "osx-arm64" - "win-64" + python-version: + - "3.8" + - "3.9" + - "3.10" steps: - uses: actions/checkout@v2 - uses: mamba-org/provision-with-micromamba@main with: environment-file: environments/installability.yml + extra-specs: | + python=${{ matrix.python-version }} - name: Run script shell: bash -el {0} env: From 2ae1765634e65935a5aff3745afb44feaf0af0a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Thu, 8 Sep 2022 13:57:44 +0200 Subject: [PATCH 05/16] Enable one by one checks, ensuring latest version --- .github/workflows/installability.yml | 12 +- ci_scripts/check_installability_plugins.py | 144 +++++++++++++++------ environments/installability.yml | 3 +- 3 files changed, 111 insertions(+), 48 deletions(-) diff --git a/.github/workflows/installability.yml b/.github/workflows/installability.yml index 115975e1..fd39b577 100644 --- a/.github/workflows/installability.yml +++ b/.github/workflows/installability.yml @@ -33,11 +33,15 @@ jobs: - uses: mamba-org/provision-with-micromamba@main with: environment-file: environments/installability.yml - extra-specs: | - python=${{ matrix.python-version }} - - name: Run script + - name: Check installability one by one shell: bash -el {0} env: CONDA_SUBDIR: ${{ matrix.subdir }} + PYTHON_VERSION: ${{ matrix.python-version }} run: python ci_scripts/check_installability_plugins.py - + - name: Check installability of all plugins at once + shell: bash -el {0} + env: + CONDA_SUBDIR: ${{ matrix.subdir }} + PYTHON_VERSION: ${{ matrix.python-version }} + run: python ci_scripts/check_installability_plugins.py --all diff --git a/ci_scripts/check_installability_plugins.py b/ci_scripts/check_installability_plugins.py index 54ae314c..1efef8e3 100644 --- a/ci_scripts/check_installability_plugins.py +++ b/ci_scripts/check_installability_plugins.py @@ -2,78 +2,138 @@ """ Check if all Hub-listed plugins can be installed together with latest napari + +Environment variables that affect the script: + +- CONDA_SUBDIR: Platform to do checks against (linux-64, osx-64, etc) +- PYTHON_VERSION: Which Python version we are testing against """ +from argparse import ArgumentParser +from collections import defaultdict +from functools import lru_cache +from subprocess import run, PIPE import json -from subprocess import CalledProcessError, check_output, STDOUT +import os +import sys +import time +from tqdm import tqdm +from conda.models.version import VersionOrder import requests NPE2API_CONDA = "https://npe2api.vercel.app/api/conda" -def check_output_verbose(*args, **kwargs): - """ - Check return code of a subprocess, capturing stdout and stderr. - If an error happened, print both streams, then raise. - """ - kwargs.pop("stderr", None) - kwargs.pop("text", None) - kwargs.pop("universal_newlines", None) - try: - return check_output(*args, stderr=STDOUT, text=True, **kwargs) - except CalledProcessError as exc: - print("Output:") - print(exc.stdout) - raise - - +@lru_cache def latest_napari_on_conda_forge(): r = requests.get("https://api.anaconda.org/package/conda-forge/napari") r.raise_for_status() return r.json()["latest_version"] +@lru_cache def all_plugin_names(): r = requests.get(NPE2API_CONDA) r.raise_for_status() return r.json() -def is_latest_version(name, version): +@lru_cache +def latest_version(name): r = requests.get(f"{NPE2API_CONDA}/{name}") r.raise_for_status() - latest_version = r.json()["latest_version"] - return version >= latest_version - - -def solve(specs): - resp = check_output_verbose( - [ - "micromamba", - "create", - "-n", - "notused", - "--dry-run", - "-c", - "conda-forge", - "--json", - *specs, - ] - ) - print(resp) - return json.loads(resp) + time.sleep(0.1) + return r.json()["latest_version"] + + +@lru_cache +def patched_environment(): + platform = os.environ.get("CONDA_SUBDIR") + if not platform: + return + env = os.environ.copy() + if platform.startswith("linux-"): + env.setdefault("CONDA_OVERRIDE_GLIBC", "2.17") + env.setdefault("CONDA_OVERRIDE_CUDA", "11.2") + elif platform.startswith("osx-"): + env.setdefault("CONDA_OVERRIDE_OSX", "11.2") + return env + + +def solve(*args): + command = [ + "micromamba", + "create", + "-n", + "notused", + "--dry-run", + "-c", + "conda-forge", + "--json", + *args, + ] + resp = run(command, stdout=PIPE, stderr=PIPE, text=True, env=patched_environment()) + try: + return json.loads(resp.stdout) + except json.JSONDecodeError: + print("Command:", command) + print("Output:", resp.stdout) + raise + + +def cli(): + p = ArgumentParser() + p.add_argument("--all", action="store_true") + return p.parse_args() def main(): - specs = [f"napari={latest_napari_on_conda_forge()}"] + pyver = os.environ.get( + "PYTHON_VERSION", f"{sys.version_info.major}.{sys.version_info.minor}" + ) + python_spec = f"python={pyver}" + napari_spec = f"napari={latest_napari_on_conda_forge()}" plugin_names = all_plugin_names() + plugin_specs = [] for _, conda_name in plugin_names.items(): if conda_name is not None: - specs.append(conda_name.replace("/", "::")) + plugin_specs.append(conda_name.replace("/", "::")) + + args = cli() + if args.all: + tasks = [(python_spec, napari_spec, *plugin_specs)] + else: + tasks = [ + (python_spec, napari_spec, plugin_spec) for plugin_spec in plugin_specs[:10] + ] - result = solve(specs) + failures = defaultdict(list) + n_tasks = len(tasks) + for i, task in enumerate(tasks, 1): + print(f"Task {i:4d}/{n_tasks}:", *task) + result = solve(*task) + if result["success"] is True: + for pkg in result["actions"]["LINK"]: + if pkg["name"] in plugin_names: + pkg_latest_version = latest_version(pkg["name"]) + if VersionOrder(pkg["version"]) < VersionOrder(pkg_latest_version): + failures[task].append( + f'{pkg["name"]}=={pkg["version"]} ' + f"is not the latest version ({pkg_latest_version})" + ) + else: + failures[task].extend(result["solver_problems"]) + print("-" * 20) + for task, failure_list in failures.items(): + print("Installation attempt for", *task, "did not succeed!") + print("Reasons:") + for failure in failure_list: + print(" - ", failure) + print("-" * 20) + + return len(failures) if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/environments/installability.yml b/environments/installability.yml index 9ece0d74..f7ef38e7 100644 --- a/environments/installability.yml +++ b/environments/installability.yml @@ -2,6 +2,5 @@ name: installability channels: - conda-forge dependencies: + - python=3.9 - conda - - mamba - - python \ No newline at end of file From 279ce57a7824a961cef611fe19c5e0aaf17268fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Thu, 8 Sep 2022 14:01:34 +0200 Subject: [PATCH 06/16] warn against pyqt* stuff --- ci_scripts/check_installability_plugins.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ci_scripts/check_installability_plugins.py b/ci_scripts/check_installability_plugins.py index 1efef8e3..44a907d6 100644 --- a/ci_scripts/check_installability_plugins.py +++ b/ci_scripts/check_installability_plugins.py @@ -93,7 +93,7 @@ def main(): "PYTHON_VERSION", f"{sys.version_info.major}.{sys.version_info.minor}" ) python_spec = f"python={pyver}" - napari_spec = f"napari={latest_napari_on_conda_forge()}" + napari_spec = f"napari={latest_napari_on_conda_forge()}=*pyside*" plugin_names = all_plugin_names() plugin_specs = [] for _, conda_name in plugin_names.items(): @@ -122,11 +122,15 @@ def main(): f'{pkg["name"]}=={pkg["version"]} ' f"is not the latest version ({pkg_latest_version})" ) + elif pkg["name"].lower().startswith("pyqt"): + failures[task].append( + f"Solution includes {pkg['name']}=={pkg['version']}" + ) else: failures[task].extend(result["solver_problems"]) print("-" * 20) for task, failure_list in failures.items(): - print("Installation attempt for", *task, "did not succeed!") + print("Installation attempt for", *task, "has errors!") print("Reasons:") for failure in failure_list: print(" - ", failure) From 0be8811506996e71e6155e74cd890ffc604f287f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Thu, 8 Sep 2022 14:09:09 +0200 Subject: [PATCH 07/16] all plugins, always --- .github/workflows/installability.yml | 1 + ci_scripts/check_installability_plugins.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/installability.yml b/.github/workflows/installability.yml index fd39b577..b0e72bde 100644 --- a/.github/workflows/installability.yml +++ b/.github/workflows/installability.yml @@ -41,6 +41,7 @@ jobs: run: python ci_scripts/check_installability_plugins.py - name: Check installability of all plugins at once shell: bash -el {0} + if: always() env: CONDA_SUBDIR: ${{ matrix.subdir }} PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/ci_scripts/check_installability_plugins.py b/ci_scripts/check_installability_plugins.py index 44a907d6..5284ea1a 100644 --- a/ci_scripts/check_installability_plugins.py +++ b/ci_scripts/check_installability_plugins.py @@ -105,7 +105,7 @@ def main(): tasks = [(python_spec, napari_spec, *plugin_specs)] else: tasks = [ - (python_spec, napari_spec, plugin_spec) for plugin_spec in plugin_specs[:10] + (python_spec, napari_spec, plugin_spec) for plugin_spec in plugin_specs ] failures = defaultdict(list) From 7c5179c91622f32de4e489395f3a16eea0141f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Thu, 8 Sep 2022 14:20:09 +0200 Subject: [PATCH 08/16] some more env vars --- ci_scripts/check_installability_plugins.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ci_scripts/check_installability_plugins.py b/ci_scripts/check_installability_plugins.py index 5284ea1a..eba9e7e2 100644 --- a/ci_scripts/check_installability_plugins.py +++ b/ci_scripts/check_installability_plugins.py @@ -54,10 +54,13 @@ def patched_environment(): return env = os.environ.copy() if platform.startswith("linux-"): + env.setdefault("CONDA_OVERRIDE_LINUX", "1") env.setdefault("CONDA_OVERRIDE_GLIBC", "2.17") env.setdefault("CONDA_OVERRIDE_CUDA", "11.2") elif platform.startswith("osx-"): env.setdefault("CONDA_OVERRIDE_OSX", "11.2") + elif platform.startswith("win-"): + env.setdefault("CONDA_OVERRIDE_WIN", "1") return env From 8573ee89a5305f8ddbb993c1869f07ae4d348de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Thu, 8 Sep 2022 16:37:02 +0200 Subject: [PATCH 09/16] more resilience to network errors on version checks --- ci_scripts/check_installability_plugins.py | 47 ++++++++++++++++------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/ci_scripts/check_installability_plugins.py b/ci_scripts/check_installability_plugins.py index eba9e7e2..410d96de 100644 --- a/ci_scripts/check_installability_plugins.py +++ b/ci_scripts/check_installability_plugins.py @@ -42,9 +42,24 @@ def all_plugin_names(): @lru_cache def latest_version(name): r = requests.get(f"{NPE2API_CONDA}/{name}") - r.raise_for_status() - time.sleep(0.1) - return r.json()["latest_version"] + if r.ok: + return r.json()["latest_version"] + + +def _check_if_latest(pkg): + failures = [] + pkg_latest_version = latest_version(pkg["name"]) + if pkg_latest_version: + if VersionOrder(pkg["version"]) < VersionOrder(pkg_latest_version): + failures.append( + f'{pkg["name"]}=={pkg["version"]} ' + f"is not the latest version ({pkg_latest_version})" + ) + else: + failures.append( + f'Warning: Could not check version for {pkg["name"]}=={pkg["version"]}' + ) + return failures @lru_cache @@ -74,7 +89,8 @@ def solve(*args): "-c", "conda-forge", "--json", - *args, + # we only process truthy args + *(arg for arg in args if arg), ] resp = run(command, stdout=PIPE, stderr=PIPE, text=True, env=patched_environment()) try: @@ -107,25 +123,32 @@ def main(): if args.all: tasks = [(python_spec, napari_spec, *plugin_specs)] else: + # We add an empty string plugin to test for just napari with no plugins tasks = [ - (python_spec, napari_spec, plugin_spec) for plugin_spec in plugin_specs + (python_spec, napari_spec, plugin_spec) + for plugin_spec in ("", *plugin_specs) ] failures = defaultdict(list) n_tasks = len(tasks) + names_to_check = {"napari", *plugin_names} for i, task in enumerate(tasks, 1): print(f"Task {i:4d}/{n_tasks}:", *task) result = solve(*task) if result["success"] is True: + # Even if the solver is able to find a solution + # it doesn't mean it's a valid one because metadata + # can have errors! for pkg in result["actions"]["LINK"]: - if pkg["name"] in plugin_names: - pkg_latest_version = latest_version(pkg["name"]) - if VersionOrder(pkg["version"]) < VersionOrder(pkg_latest_version): - failures[task].append( - f'{pkg["name"]}=={pkg["version"]} ' - f"is not the latest version ({pkg_latest_version})" - ) + if pkg["name"] in names_to_check: + # 1) We should have obtained the latest version + # of the plugin. If not, metadata is faulty! + maybe_failures = _check_if_latest(pkg) + if maybe_failures: + failures[task].extend(maybe_failures) elif pkg["name"].lower().startswith("pyqt"): + # 2) We want pyside only. If pyqt lands in the env + # it was pulled by the plugin or its dependencies. failures[task].append( f"Solution includes {pkg['name']}=={pkg['version']}" ) From 7c124814055f1e880ae5ac1b2db2e9536af32579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Thu, 8 Sep 2022 16:48:34 +0200 Subject: [PATCH 10/16] napari's version comes from another api --- ci_scripts/check_installability_plugins.py | 43 +++++++++++----------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/ci_scripts/check_installability_plugins.py b/ci_scripts/check_installability_plugins.py index 410d96de..2cf57824 100644 --- a/ci_scripts/check_installability_plugins.py +++ b/ci_scripts/check_installability_plugins.py @@ -18,7 +18,6 @@ import sys import time -from tqdm import tqdm from conda.models.version import VersionOrder import requests @@ -26,44 +25,44 @@ @lru_cache -def latest_napari_on_conda_forge(): +def _latest_napari_on_conda_forge(): r = requests.get("https://api.anaconda.org/package/conda-forge/napari") r.raise_for_status() return r.json()["latest_version"] @lru_cache -def all_plugin_names(): +def _all_plugin_names(): r = requests.get(NPE2API_CONDA) r.raise_for_status() return r.json() @lru_cache -def latest_version(name): +def _latest_plugin_version(name): r = requests.get(f"{NPE2API_CONDA}/{name}") + time.sleep(0.1) if r.ok: return r.json()["latest_version"] def _check_if_latest(pkg): failures = [] - pkg_latest_version = latest_version(pkg["name"]) - if pkg_latest_version: - if VersionOrder(pkg["version"]) < VersionOrder(pkg_latest_version): - failures.append( - f'{pkg["name"]}=={pkg["version"]} ' - f"is not the latest version ({pkg_latest_version})" - ) + name, version = pkg["name"], pkg["version"] + if name == "napari": + latest_v = _latest_napari_on_conda_forge() else: - failures.append( - f'Warning: Could not check version for {pkg["name"]}=={pkg["version"]}' - ) + latest_v = _latest_plugin_version(name) + if latest_v: + if VersionOrder(version) < VersionOrder(latest_v): + failures.append(f"{name}=={version} is not the latest version ({latest_v})") + else: + failures.append(f"Warning: Could not check version for {name}=={version}") return failures @lru_cache -def patched_environment(): +def _patched_environment(): platform = os.environ.get("CONDA_SUBDIR") if not platform: return @@ -79,7 +78,7 @@ def patched_environment(): return env -def solve(*args): +def _solve(*args): command = [ "micromamba", "create", @@ -92,7 +91,7 @@ def solve(*args): # we only process truthy args *(arg for arg in args if arg), ] - resp = run(command, stdout=PIPE, stderr=PIPE, text=True, env=patched_environment()) + resp = run(command, stdout=PIPE, stderr=PIPE, text=True, env=_patched_environment()) try: return json.loads(resp.stdout) except json.JSONDecodeError: @@ -101,7 +100,7 @@ def solve(*args): raise -def cli(): +def _cli(): p = ArgumentParser() p.add_argument("--all", action="store_true") return p.parse_args() @@ -112,14 +111,14 @@ def main(): "PYTHON_VERSION", f"{sys.version_info.major}.{sys.version_info.minor}" ) python_spec = f"python={pyver}" - napari_spec = f"napari={latest_napari_on_conda_forge()}=*pyside*" - plugin_names = all_plugin_names() + napari_spec = f"napari={_latest_napari_on_conda_forge()}=*pyside*" + plugin_names = _all_plugin_names() plugin_specs = [] for _, conda_name in plugin_names.items(): if conda_name is not None: plugin_specs.append(conda_name.replace("/", "::")) - args = cli() + args = _cli() if args.all: tasks = [(python_spec, napari_spec, *plugin_specs)] else: @@ -134,7 +133,7 @@ def main(): names_to_check = {"napari", *plugin_names} for i, task in enumerate(tasks, 1): print(f"Task {i:4d}/{n_tasks}:", *task) - result = solve(*task) + result = _solve(*task) if result["success"] is True: # Even if the solver is able to find a solution # it doesn't mean it's a valid one because metadata From ac835352d1925219dbdf00d75b978cc4f109af3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Fri, 9 Sep 2022 15:27:54 +0200 Subject: [PATCH 11/16] limit to cpython --- ci_scripts/check_installability_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci_scripts/check_installability_plugins.py b/ci_scripts/check_installability_plugins.py index 2cf57824..03b79a02 100644 --- a/ci_scripts/check_installability_plugins.py +++ b/ci_scripts/check_installability_plugins.py @@ -110,7 +110,7 @@ def main(): pyver = os.environ.get( "PYTHON_VERSION", f"{sys.version_info.major}.{sys.version_info.minor}" ) - python_spec = f"python={pyver}" + python_spec = f"python={pyver}=*cpython" napari_spec = f"napari={_latest_napari_on_conda_forge()}=*pyside*" plugin_names = _all_plugin_names() plugin_specs = [] From b620e5493336148aae45a455892c46078d7fa742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Fri, 9 Sep 2022 15:48:31 +0200 Subject: [PATCH 12/16] refine cpython spec --- ci_scripts/check_installability_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci_scripts/check_installability_plugins.py b/ci_scripts/check_installability_plugins.py index 03b79a02..5ccf7aa2 100644 --- a/ci_scripts/check_installability_plugins.py +++ b/ci_scripts/check_installability_plugins.py @@ -110,7 +110,7 @@ def main(): pyver = os.environ.get( "PYTHON_VERSION", f"{sys.version_info.major}.{sys.version_info.minor}" ) - python_spec = f"python={pyver}=*cpython" + python_spec = f"python={pyver}.*=*cpython" napari_spec = f"napari={_latest_napari_on_conda_forge()}=*pyside*" plugin_names = _all_plugin_names() plugin_specs = [] From 16a12ad328b659ab0f51a160ae41b87270c85b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Wed, 14 Sep 2022 15:54:47 +0200 Subject: [PATCH 13/16] pyqtgraph is ok --- ci_scripts/check_installability_plugins.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ci_scripts/check_installability_plugins.py b/ci_scripts/check_installability_plugins.py index 5ccf7aa2..9bb79b41 100644 --- a/ci_scripts/check_installability_plugins.py +++ b/ci_scripts/check_installability_plugins.py @@ -130,7 +130,7 @@ def main(): failures = defaultdict(list) n_tasks = len(tasks) - names_to_check = {"napari", *plugin_names} + names_to_check = {"napari", *[name.lower() for name in plugin_names]} for i, task in enumerate(tasks, 1): print(f"Task {i:4d}/{n_tasks}:", *task) result = _solve(*task) @@ -139,15 +139,17 @@ def main(): # it doesn't mean it's a valid one because metadata # can have errors! for pkg in result["actions"]["LINK"]: - if pkg["name"] in names_to_check: + pkg_name_lower = pkg["name"].lower() + if pkg_name_lower in names_to_check: # 1) We should have obtained the latest version # of the plugin. If not, metadata is faulty! maybe_failures = _check_if_latest(pkg) if maybe_failures: failures[task].extend(maybe_failures) - elif pkg["name"].lower().startswith("pyqt"): + elif pkg_name_lower.startswith("pyqt") and pkg_name_lower != "pyqtgraph": # 2) We want pyside only. If pyqt lands in the env # it was pulled by the plugin or its dependencies. + # Note that pyqtgraph, despite the name, can use pyside too. failures[task].append( f"Solution includes {pkg['name']}=={pkg['version']}" ) From 591698504074d1757bcc25a420a1ae7330232a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Wed, 14 Sep 2022 16:30:42 +0200 Subject: [PATCH 14/16] more efficient checks --- ci_scripts/check_installability_plugins.py | 33 ++++++++++++++-------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/ci_scripts/check_installability_plugins.py b/ci_scripts/check_installability_plugins.py index 9bb79b41..cabbfcbf 100644 --- a/ci_scripts/check_installability_plugins.py +++ b/ci_scripts/check_installability_plugins.py @@ -107,18 +107,27 @@ def _cli(): def main(): - pyver = os.environ.get( - "PYTHON_VERSION", f"{sys.version_info.major}.{sys.version_info.minor}" - ) + args = _cli() + current_pyver = f"{sys.version_info.major}.{sys.version_info.minor}" + pyver = os.environ.get("PYTHON_VERSION", current_pyver) python_spec = f"python={pyver}.*=*cpython" napari_spec = f"napari={_latest_napari_on_conda_forge()}=*pyside*" plugin_names = _all_plugin_names() + names_to_check = {"napari"} plugin_specs = [] - for _, conda_name in plugin_names.items(): + print("Preparing tasks...") + for i, (pypi_name, conda_name) in enumerate(plugin_names.items()): if conda_name is not None: - plugin_specs.append(conda_name.replace("/", "::")) + plugin_spec = conda_name.replace("/", "::") + if not args.all: + latest_version = _latest_plugin_version(pypi_name) + if latest_version: + plugin_spec += f"=={latest_version}" + plugin_specs.append(plugin_spec) + names_to_check.add(conda_name.split("/")[1]) + if i > 9: + break - args = _cli() if args.all: tasks = [(python_spec, napari_spec, *plugin_specs)] else: @@ -130,7 +139,6 @@ def main(): failures = defaultdict(list) n_tasks = len(tasks) - names_to_check = {"napari", *[name.lower() for name in plugin_names]} for i, task in enumerate(tasks, 1): print(f"Task {i:4d}/{n_tasks}:", *task) result = _solve(*task) @@ -140,19 +148,20 @@ def main(): # can have errors! for pkg in result["actions"]["LINK"]: pkg_name_lower = pkg["name"].lower() - if pkg_name_lower in names_to_check: + if args.all and pkg_name_lower in names_to_check: # 1) We should have obtained the latest version # of the plugin. If not, metadata is faulty! + # In one by one tests, this is forced in the spec. + # In "all at once", we don't force it, so better check. maybe_failures = _check_if_latest(pkg) if maybe_failures: failures[task].extend(maybe_failures) - elif pkg_name_lower.startswith("pyqt") and pkg_name_lower != "pyqtgraph": + elif pkg_name_lower[:4] == "pyqt" and pkg_name_lower != "pyqtgraph": # 2) We want pyside only. If pyqt lands in the env # it was pulled by the plugin or its dependencies. # Note that pyqtgraph, despite the name, can use pyside too. - failures[task].append( - f"Solution includes {pkg['name']}=={pkg['version']}" - ) + failure = f"solution has {pkg['name']}=={pkg['version']}" + failures[task].append(failure) else: failures[task].extend(result["solver_problems"]) print("-" * 20) From 1add49f475f0c4c126333e0a7c22b022d62e6559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Wed, 14 Sep 2022 16:36:55 +0200 Subject: [PATCH 15/16] now all plugins --- ci_scripts/check_installability_plugins.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ci_scripts/check_installability_plugins.py b/ci_scripts/check_installability_plugins.py index cabbfcbf..ddf00c16 100644 --- a/ci_scripts/check_installability_plugins.py +++ b/ci_scripts/check_installability_plugins.py @@ -116,7 +116,7 @@ def main(): names_to_check = {"napari"} plugin_specs = [] print("Preparing tasks...") - for i, (pypi_name, conda_name) in enumerate(plugin_names.items()): + for pypi_name, conda_name in plugin_names.items(): if conda_name is not None: plugin_spec = conda_name.replace("/", "::") if not args.all: @@ -125,8 +125,6 @@ def main(): plugin_spec += f"=={latest_version}" plugin_specs.append(plugin_spec) names_to_check.add(conda_name.split("/")[1]) - if i > 9: - break if args.all: tasks = [(python_spec, napari_spec, *plugin_specs)] From b6a034401b7b74da3cfbe9f90b43c732b07acf64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Rodr=C3=ADguez-Guerra?= Date: Wed, 14 Sep 2022 17:15:02 +0200 Subject: [PATCH 16/16] change exit code for "all plugins" test --- ci_scripts/check_installability_plugins.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ci_scripts/check_installability_plugins.py b/ci_scripts/check_installability_plugins.py index ddf00c16..5e09bd96 100644 --- a/ci_scripts/check_installability_plugins.py +++ b/ci_scripts/check_installability_plugins.py @@ -169,8 +169,12 @@ def main(): for failure in failure_list: print(" - ", failure) print("-" * 20) - - return len(failures) + if args.all: + # single task, the exit code is the number of problems + return sum(len(problems) for problems in failures.values()) + else: + # several tasks, the exit code is the number of tasks that failed + return len(failures) if __name__ == "__main__":