diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 28e97ee..5267274 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,13 @@ name: Check Build on: push: - branches: [main] + branches: [main, "[0-9]+.[0-9]+.x"] pull_request: - branches: [main] + branches: [main, "[0-9]+.[0-9]+.x"] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: package: diff --git a/.github/workflows/test_linux_cuda.yml b/.github/workflows/test_linux_cuda.yml new file mode 100644 index 0000000..6926ab2 --- /dev/null +++ b/.github/workflows/test_linux_cuda.yml @@ -0,0 +1,70 @@ +name: PopV (cuda) + +on: + push: + branches: [main, "[0-9]+.[0-9]+.x"] #this is new + pull_request: + branches: [main, "[0-9]+.[0-9]+.x"] + types: [labeled, synchronize, opened] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + # if PR has label "cuda tests" or "all tests" or if scheduled or manually triggered + if: >- + ( + contains(github.event.pull_request.labels.*.name, 'cuda tests') || + contains(github.event.pull_request.labels.*.name, 'all tests') || + contains(github.event_name, 'schedule') || + contains(github.event_name, 'workflow_dispatch') + ) + + runs-on: [self-hosted, Linux, X64, CUDA] + + defaults: + run: + shell: bash -e {0} # -e to fail on error + + container: + image: ghcr.io/yoseflab/popv:py3.10-cu12-base + options: --user root --gpus all --pull always + + name: integration + + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python }} + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + cache: "pip" + cache-dependency-path: "**/pyproject.toml" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip wheel uv + python -m uv pip install --system "PopV[tests] @ ." + python -m pip install jax[cuda] + python -m pip install nvidia-nccl-cu12 + + - name: Run pytest + env: + MPLBACKEND: agg + PLATFORM: ${{ matrix.os }} + DISPLAY: :42 + COLUMNS: 120 + run: | + coverage run -m pytest -v --color=yes --accelerator cuda --devices auto + coverage report + + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 69897c3..cdc6534 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,16 +1,17 @@ -# https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 build: os: ubuntu-20.04 tools: - python: "3.10" + python: "3.11" sphinx: configuration: docs/conf.py - # disable this for more lenient docs builds - fail_on_warning: true python: install: - method: pip path: . extra_requirements: - - doc + - docsbuild +submodules: + include: + - "docs/notebooks" + recursive: true diff --git a/docs/conf.py b/docs/conf.py index 7274633..8ba3576 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,6 +9,15 @@ from datetime import datetime from importlib.metadata import metadata from pathlib import Path +import importlib.util +import inspect +import os +import re +import subprocess +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any HERE = Path(__file__).parent sys.path.insert(0, str(HERE / "extensions")) @@ -48,17 +57,20 @@ # They can be extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "myst_nb", - "sphinx_copybutton", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", - "sphinx.ext.autosummary", + "sphinx.ext.linkcode", + "sphinx.ext.mathjax", "sphinx.ext.napoleon", + "sphinx_autodoc_typehints", # needs to be after napoleon + "sphinx.ext.extlinks", + "sphinx.ext.autosummary", "sphinxcontrib.bibtex", - "sphinx_autodoc_typehints", - "sphinx.ext.mathjax", - "IPython.sphinxext.ipython_console_highlighting", - "sphinxext.opengraph", *[p.stem for p in (HERE / "extensions").glob("*.py")], + "sphinx_copybutton", + "sphinx_design", + "sphinxext.opengraph", + "hoverxref.extension", ] autosummary_generate = True @@ -127,3 +139,78 @@ # you can add an exception to this list. # ("py:class", "igraph.Graph"), ] + + +# -- Config for linkcode ------------------------------------------- + + +def git(*args): + """Run git command and return output as string.""" + return subprocess.check_output(["git", *args]).strip().decode() + + +# https://github.com/DisnakeDev/disnake/blob/7853da70b13fcd2978c39c0b7efa59b34d298186/docs/conf.py#L192 +# Current git reference. Uses branch/tag name if found, otherwise uses commit hash +git_ref = None +try: + git_ref = git("name-rev", "--name-only", "--no-undefined", "HEAD") + git_ref = re.sub(r"^(remotes/[^/]+|tags)/", "", git_ref) +except Exception: # noqa: BLE001 + pass + +# (if no name found or relative ref, use commit hash instead) +if not git_ref or re.search(r"[\^~]", git_ref): + try: + git_ref = git("rev-parse", "HEAD") + except Exception: # noqa: BLE001 + git_ref = "main" + +# https://github.com/DisnakeDev/disnake/blob/7853da70b13fcd2978c39c0b7efa59b34d298186/docs/conf.py#L192 +_scvi_tools_module_path = os.path.dirname(importlib.util.find_spec("scvi").origin) # type: ignore + + +def linkcode_resolve(domain, info): + """Determine the URL corresponding to Python object.""" + if domain != "py": + return None + + try: + obj: Any = sys.modules[info["module"]] + for part in info["fullname"].split("."): + obj = getattr(obj, part) + obj = inspect.unwrap(obj) + + if isinstance(obj, property): + obj = inspect.unwrap(obj.fget) # type: ignore + + path = os.path.relpath(inspect.getsourcefile(obj), start=_scvi_tools_module_path) # type: ignore + src, lineno = inspect.getsourcelines(obj) + except Exception: # noqa: BLE001 + return None + + path = f"{path}#L{lineno}-L{lineno + len(src) - 1}" + return f"{repository_url}/blob/{git_ref}/src/scvi/{path}" + + +# -- Config for hoverxref ------------------------------------------- + +hoverx_default_type = "tooltip" +hoverxref_domains = ["py"] +hoverxref_role_types = dict.fromkeys( + ["ref", "class", "func", "meth", "attr", "exc", "data", "mod"], + "tooltip", +) +hoverxref_intersphinx = [ + "python", + "numpy", + "scanpy", + "anndata", + "pytorch_lightning", + "scipy", + "pandas", + "ml_collections", + "ray", +] +# use proxied API endpoint on rtd to avoid CORS issues +if os.environ.get("READTHEDOCS"): + hoverxref_api_host = "/_" diff --git a/pyproject.toml b/pyproject.toml index 77f3bd4..8c20cb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,20 +50,21 @@ dev = [ "pre-commit", "twine>=4.0.2" ] -doc = [ - "docutils>=0.8,!=0.18.*,!=0.19.*", - "sphinx>=4", - "sphinx-book-theme>=1.0.0", - "myst-nb", +docs = [ + "docutils>=0.8,!=0.18.*,!=0.19.*", # see https://github.com/scverse/cookiecutter-scverse/pull/205 + "sphinx>=4.1", + "ipython", + "sphinx-book-theme>=1.0.1", + "sphinx_copybutton", + "sphinx-design", + "sphinxext-opengraph", + "sphinx-hoverxref", "sphinxcontrib-bibtex>=1.0.0", + "myst-parser", + "myst-nb", "sphinx-autodoc-typehints", - "sphinxext-opengraph", - # For notebooks - "ipykernel", - "ipython", - "sphinx-copybutton", - "pandas", ] +docsbuild = ["popv[docs]"] test = [ "pytest", "coverage", diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..519a49f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,29 @@ +import pytest + + +def pytest_addoption(parser): + """Docstring for pytest_addoption.""" + parser.addoption( + "--accelerator", + action="store", + default="cpu", + help="Option to specify which accelerator to use for tests.", + ) + parser.addoption( + "--devices", + action="store", + default="auto", + help="Option to specify which devices to use for tests.", + ) + + +@pytest.fixture(scope="session") +def accelerator(request): + """Docstring for accelerator.""" + return request.config.getoption("--accelerator") + + +@pytest.fixture(scope="session") +def devices(request): + """Docstring for devices.""" + return request.config.getoption("--devices")