From 5fc24bf07b1041c146c75da07a2b93fa7e718d32 Mon Sep 17 00:00:00 2001 From: Neclow <32112754+Neclow@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:11:18 +0000 Subject: [PATCH] refactor(py)!: make phylo2vec.opt importable without the GradME extras phylo2vec.opt imported GradME eagerly, so any import from it needed jax, optax, tqdm and rpy2. GradME is now imported in a try block: without these, the rest of phylo2vec.opt imports, and asking for GradME raises an ImportError that says what to install. Since only GradME needs them, the [opt] extras are renamed [gradme]. joblib is dropped from them, as it is already a core dependency. The test task now runs the docstring examples of the phylo2vec.opt modules that need no extras, instead of skipping the whole package. BREAKING CHANGE: the [opt] extras are now [gradme]. `pip install phylo2vec[opt]` only warns that the extra does not exist and installs phylo2vec without the GradME dependencies. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci-python.yml | 2 +- README.md | 9 +++--- docs/_config.yml | 2 +- docs/api.rst | 3 +- docs/installation.md | 9 +++--- docs/notebooks/optimisation.ipynb | 2 +- pixi.toml | 4 +-- py-phylo2vec/phylo2vec/opt/__init__.py | 27 ++++++++++++++--- py-phylo2vec/pyproject.toml | 3 +- py-phylo2vec/tests/test_opt.py | 42 ++++++++++++++++++++++++++ 10 files changed, 81 insertions(+), 22 deletions(-) create mode 100644 py-phylo2vec/tests/test_opt.py diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml index b9d76db6..7f61d2bf 100644 --- a/.github/workflows/ci-python.yml +++ b/.github/workflows/ci-python.yml @@ -48,7 +48,7 @@ jobs: run: | pixi run -e py-phylo2vec install-python pixi run -e py-phylo2vec pip install nbconvert - # optimisation.ipynb is skipped as it needs the [opt] extras, RAxML-NG and R + # optimisation.ipynb is skipped as it needs the [gradme] extras, RAxML-NG and R - name: Execute notebooks run: | cd docs/notebooks diff --git a/README.md b/README.md index 66cc7b73..9483bb9d 100644 --- a/README.md +++ b/README.md @@ -45,14 +45,13 @@ The easiest way to install the standard Python package is using pip: pip install phylo2vec ``` -Several optimization schemes based on Phylo2Vec are also available, but require -extra dependencies. (See this +Several optimization schemes based on Phylo2Vec are also available (see this [notebook](https://phylo2vec.readthedocs.io/en/latest/notebooks/optimisation.html) -for a demo). To avoid bloating the standard package, these dependencies must be -installed separately. To do so, run: +for a demo). GradME requires extra dependencies, which are not installed by +default to avoid bloating the standard package. To install them, run: ```bash -pip install "phylo2vec[opt]" +pip install "phylo2vec[gradme]" ``` #### Manual installation diff --git a/docs/_config.yml b/docs/_config.yml index 69fe54df..7f45a407 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -38,5 +38,5 @@ sphinx: config: add_module_names: True autosummary_generate: True - # phylo2vec.opt dependencies (the [opt] extras) are not installed in the docs env + # The [gradme] extras and numba (for phylo2vec.opt) are not installed in the docs env autodoc_mock_imports: ['jax', 'numba', 'optax', 'rpy2', 'tqdm'] diff --git a/docs/api.rst b/docs/api.rst index af3cde60..38e652f6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -90,7 +90,8 @@ Datasets Optimisation ---------------- -Requires the optional dependencies: ``pip install phylo2vec[opt]`` +``opt.GradME`` and ``opt.gradme_loss`` require the optional dependencies: +``pip install phylo2vec[gradme]`` .. autosummary:: :nosignatures: diff --git a/docs/installation.md b/docs/installation.md index 4acedf7d..bd9b6b69 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -10,14 +10,13 @@ The easiest way to install the standard Python package is using pip: pip install phylo2vec ``` -Several optimization schemes based on Phylo2Vec are also available, but require -extra dependencies. (See this +Several optimization schemes based on Phylo2Vec are also available (see this [notebook](https://phylo2vec.readthedocs.io/en/latest/notebooks/optimisation.html) -for a demo). To avoid bloating the standard package, these dependencies must be -installed separately. To do so, run: +for a demo). GradME requires extra dependencies, which are not installed by +default to avoid bloating the standard package. To install them, run: ```bash -pip install phylo2vec[opt] +pip install phylo2vec[gradme] ``` ### Manual installation diff --git a/docs/notebooks/optimisation.ipynb b/docs/notebooks/optimisation.ipynb index 579635f8..c0371c8a 100644 --- a/docs/notebooks/optimisation.ipynb +++ b/docs/notebooks/optimisation.ipynb @@ -11,7 +11,7 @@ "* A hill-climbing scheme using Phylo2Vec vectors, with branch length optimisation using [RAxML-NG](https://github.com/amkozlov/raxml-ng)\n", "* [GradME](https://doi.org/10.1093/gbe/evad213), a continuous optimisation scheme based on a probabilistic version of Phylo2Vec.\n", "\n", - "Running this notebook requires the optional dependencies (`pip install phylo2vec[opt]`), [RAxML-NG](https://github.com/amkozlov/raxml-ng) for hill-climbing, and R with the [ape](https://cran.r-project.org/package=ape) package for GradME." + "Running this notebook requires [RAxML-NG](https://github.com/amkozlov/raxml-ng) for hill-climbing. GradME needs the optional dependencies (`pip install phylo2vec[gradme]`) and R with the [ape](https://cran.r-project.org/package=ape) package." ] }, { diff --git a/pixi.toml b/pixi.toml index 5d9ed6de..5d414c9a 100644 --- a/pixi.toml +++ b/pixi.toml @@ -111,8 +111,8 @@ libxcrypt = ">=4.4.36,<5" cmd = "maturin develop --release --manifest-path ./py-phylo2vec/Cargo.toml" [feature.python.tasks.test] -# Also run docstring examples; phylo2vec.opt is skipped as it needs the [opt] extras -cmd = "pytest ./py-phylo2vec/tests ./py-phylo2vec/phylo2vec --doctest-modules --ignore=./py-phylo2vec/phylo2vec/opt" +# Also run docstring examples, except in the phylo2vec.opt modules that need the [gradme] extras or numba +cmd = "pytest ./py-phylo2vec/tests ./py-phylo2vec/phylo2vec --doctest-modules --ignore=./py-phylo2vec/phylo2vec/opt/_gradme.py --ignore=./py-phylo2vec/phylo2vec/opt/_gradme_losses.py --ignore=./py-phylo2vec/phylo2vec/opt/utils.py --ignore=./py-phylo2vec/phylo2vec/opt/_beagle_losses.py" depends-on = ["install-python"] [feature.python.tasks.benchmark] diff --git a/py-phylo2vec/phylo2vec/opt/__init__.py b/py-phylo2vec/phylo2vec/opt/__init__.py index 02c634cf..161346a6 100644 --- a/py-phylo2vec/phylo2vec/opt/__init__.py +++ b/py-phylo2vec/phylo2vec/opt/__init__.py @@ -1,8 +1,27 @@ -"""Phylo2Vec-based optimisation methods.""" +"""Phylo2Vec-based optimisation methods. + +GradME needs the optional dependencies: ``pip install phylo2vec[gradme]``. +""" from ._api import list_methods -from ._gradme import GradME -from ._gradme_losses import gradme_loss from ._hc import HillClimbing -__all__ = ["GradME", "gradme_loss", "HillClimbing", "list_methods"] +__all__ = ["HillClimbing", "list_methods"] + +try: + from ._gradme import GradME + from ._gradme_losses import gradme_loss + + __all__ += ["GradME", "gradme_loss"] +except ImportError as err: + # Without the [gradme] extras, the rest of phylo2vec.opt still imports. + # Python calls a module's __getattr__ only for names it cannot find, + # so asking for GradME then raises an error that says what to install. + _GRADME_IMPORT_ERROR = err + + def __getattr__(name): + if name in ("GradME", "gradme_loss"): + raise ImportError( + f"{name} needs the optional dependencies: pip install phylo2vec[gradme]" + ) from _GRADME_IMPORT_ERROR + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/py-phylo2vec/pyproject.toml b/py-phylo2vec/pyproject.toml index 1688290e..42a76c15 100644 --- a/py-phylo2vec/pyproject.toml +++ b/py-phylo2vec/pyproject.toml @@ -34,9 +34,8 @@ dependencies = [ ] [project.optional-dependencies] -opt = [ +gradme = [ "jax", - "joblib", "optax", "rpy2", "tqdm" diff --git a/py-phylo2vec/tests/test_opt.py b/py-phylo2vec/tests/test_opt.py new file mode 100644 index 00000000..7aed2f59 --- /dev/null +++ b/py-phylo2vec/tests/test_opt.py @@ -0,0 +1,42 @@ +"""Tests for phylo2vec.opt""" + +import subprocess +import sys + +# Hide jax, so that the [gradme] extras look missing even if they are installed +HIDE_JAX = "import sys; sys.modules['jax'] = None; " + + +def run_without_gradme_extras(code): + """Run code in a fresh interpreter, leaving the test session's imports as is.""" + return subprocess.run( + [sys.executable, "-c", HIDE_JAX + code], + capture_output=True, + text=True, + check=False, + ) + + +def test_opt_imports_without_gradme_extras(): + """Without the [gradme] extras, phylo2vec.opt imports and leaves GradME out.""" + result = run_without_gradme_extras( + "import phylo2vec.opt as opt; print(opt.list_methods(), opt.__all__)" + ) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "['HillClimbing'] ['HillClimbing', 'list_methods']" + + +def test_gradme_without_extras_says_what_to_install(): + """Asking for GradME without the [gradme] extras names the extras to install.""" + result = run_without_gradme_extras("from phylo2vec.opt import GradME") + assert result.returncode != 0 + assert "ImportError: GradME needs the optional dependencies" in result.stderr + assert "pip install phylo2vec[gradme]" in result.stderr + + +def test_unknown_name_without_gradme_extras(): + """Other missing names still raise Python's usual AttributeError.""" + result = run_without_gradme_extras("import phylo2vec.opt as opt; opt.missing") + assert result.returncode != 0 + expected = "AttributeError: module 'phylo2vec.opt' has no attribute 'missing'" + assert expected in result.stderr