Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Skip optional-dependency-heavy test modules when their imports aren't
available, so the suite can still run a useful subset on a minimal install.

Without the optional ``alphafold`` extra (and its transitive deps biopython,
jax, haiku, absl, etc.) installed, pytest used to abort at collection time
because ``tests/test_msa.py``, ``tests/test_utils.py``, and
``tests/test_colabfold.py`` all import ``colabfold.batch`` at module load,
which in turn imports ``Bio`` and the rest.

This conftest probes the required imports up front and adds any unmet
modules to ``collect_ignore``, reporting what was skipped and why in the
pytest header. Install with ``poetry install -E alphafold`` (or the
equivalent pip extras) to enable the full suite.
"""

import importlib


# Module-level imports each test file performs at collection time. If any of
# these can't be imported, that test file is skipped from collection. We try
# the actual import rather than just locating the module because
# ``colabfold.batch`` raises ``RuntimeError`` (not ``ImportError``) when the
# alphafold extra is missing, which a spec lookup would not catch.
_OPTIONAL_REQUIREMENTS = {
"test_msa.py": ["colabfold.batch"],
"test_utils.py": ["colabfold.batch"],
"test_colabfold.py": [
"colabfold.batch",
"alphafold.model.data",
"haiku",
"absl",
],
}


# Top-level package names whose absence we treat as "this optional extra was
# not installed". Anything else — a missing submodule of an installed
# package, or a non-ModuleNotFoundError ImportError — points at API drift or
# a real import-time bug and must surface, not be hidden behind a skip.
#
# Sources:
# - alphafold extra in pyproject.toml: alphafold-colabfold (→ ``alphafold``),
# jax, absl-py (→ ``absl``), dm-tree (→ ``tree``), dm-haiku (→ ``haiku``),
# tensorflow / tensorflow-cpu (→ ``tensorflow``), py3Dmol.
# - Base deps that are nevertheless commonly absent in minimal local venvs:
# biopython (→ ``Bio``), importlib_metadata. We accept these so that
# ``pytest`` is usable on a bare checkout without ``poetry install``.
_OPTIONAL_TOP_LEVEL_PACKAGES = frozenset({
"Bio",
"absl",
"alphafold",
"haiku",
"importlib_metadata",
"jax",
"jaxlib",
"py3Dmol",
"tensorflow",
"tree",
})

# colabfold.batch deliberately raises RuntimeError (not ModuleNotFoundError)
# when the alphafold extra is missing — see colabfold/batch.py. We treat that
# exact signal as a missing-extras failure; any other RuntimeError indicates
# a real bug that must surface, not be swallowed by collection.
_BATCH_MISSING_EXTRA_MARKER = "alphafold is not installed"


def _format_failure(exc):
"""Render an exception as a single, capped line for the pytest header."""
message = " ".join(str(exc).split())
if not message:
return type(exc).__name__
if len(message) > 160:
message = message[:157] + "..."
return f"{type(exc).__name__}: {message}"


def _import_failure(module_name):
"""Return a short failure description if importing fails with a known
missing-extras signal, or ``None`` on success.

Only two failure shapes are treated as "missing optional extra":

1. ``ModuleNotFoundError`` whose ``name`` attribute is exactly one of the
packages in :data:`_OPTIONAL_TOP_LEVEL_PACKAGES`. A submodule miss
(``name == "alphafold.common"``) implies the root is installed but
its layout doesn't match what the code expects — that's API drift.
2. ``RuntimeError`` raised by ``colabfold.batch`` with the canonical
"alphafold is not installed" marker.

Every other exception — including plain ``ImportError``
("cannot import name 'X' from 'Y'"), ``AttributeError``, ``TypeError``,
or unexpected ``RuntimeError`` — propagates so genuine bugs surface
during collection instead of being misreported as a missing extra.
"""
try:
importlib.import_module(module_name)
return None
except ModuleNotFoundError as e:
if e.name in _OPTIONAL_TOP_LEVEL_PACKAGES:
return _format_failure(e)
raise
except RuntimeError as e:
if (
module_name == "colabfold.batch"
and _BATCH_MISSING_EXTRA_MARKER in str(e)
):
return _format_failure(e)
raise


_missing_by_file = {}
collect_ignore = []

for _filename, _requirements in _OPTIONAL_REQUIREMENTS.items():
_failures = {}
for _module in _requirements:
_reason = _import_failure(_module)
if _reason is not None:
_failures[_module] = _reason
if _failures:
collect_ignore.append(_filename)
_missing_by_file[_filename] = _failures


def pytest_report_header(config):
if not _missing_by_file:
return None
lines = [
"Skipping test modules that depend on the optional `alphafold` extra "
"(install with `poetry install -E alphafold`):"
]
for filename in sorted(_missing_by_file):
for module, reason in _missing_by_file[filename].items():
lines.append(f" tests/{filename}: cannot import {module} ({reason})")
return "\n".join(lines)
143 changes: 143 additions & 0 deletions tests/test_conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"""Tests for ``tests/conftest.py``'s import-probe classifier.

The probe must distinguish two cases:

1. "Optional extra not installed" — expected, and the affected test module
should be skipped from collection with a helpful header message.
2. "Optional extra is installed but importing it broke" — a real bug that
must surface as a collection error, not be silently hidden behind the
same "missing extra" message.

These tests pin that discrimination so a regression to a broader
``except Exception:`` / ``except ImportError:`` would fail loudly here.
"""

import pytest

from tests.conftest import _import_failure


@pytest.fixture
def fake_import(monkeypatch):
"""Replace ``importlib.import_module`` with a stub that raises ``exc``."""

def install(exc):
def _stub(name):
raise exc

monkeypatch.setattr("tests.conftest.importlib.import_module", _stub)

return install


# -- happy path --------------------------------------------------------------


def test_returns_none_when_import_succeeds(monkeypatch):
monkeypatch.setattr(
"tests.conftest.importlib.import_module", lambda name: object()
)
assert _import_failure("colabfold.batch") is None


# -- expected missing-extras shapes -----------------------------------------


def test_module_not_found_for_known_optional_root_skips(fake_import):
# Bare ``import haiku`` when dm-haiku isn't installed.
fake_import(ModuleNotFoundError("No module named 'haiku'", name="haiku"))
result = _import_failure("haiku")
assert result is not None
assert "ModuleNotFoundError" in result
assert "haiku" in result


def test_module_not_found_for_biopython_skips(fake_import):
# ``from Bio import ...`` failing in a bare minimal venv.
fake_import(ModuleNotFoundError("No module named 'Bio'", name="Bio"))
result = _import_failure("colabfold.batch")
assert result is not None
assert "ModuleNotFoundError" in result


def test_batch_alphafold_runtime_error_skips(fake_import):
# Exact shape colabfold/batch.py raises when alphafold extra is missing.
fake_import(
RuntimeError(
"\n\nalphafold is not installed. "
"Please run `pip install colabfold[alphafold]`\n"
)
)
result = _import_failure("colabfold.batch")
assert result is not None
assert "RuntimeError" in result
assert "alphafold is not installed" in result


# -- adversarial cases: must NOT be hidden as missing-extras ---------------


def test_module_not_found_for_unknown_root_re_raises(fake_import):
# A test-only helper module not in the optional set — must surface,
# never be silently skipped under the missing-extras banner.
err = ModuleNotFoundError(
"No module named 'colabfold.unreleased_helper'",
name="colabfold.unreleased_helper",
)
fake_import(err)
with pytest.raises(ModuleNotFoundError):
_import_failure("colabfold.unreleased_helper")


def test_module_not_found_for_submodule_of_known_root_re_raises(fake_import):
# alphafold is installed but ``alphafold.common`` is somehow missing —
# that's API drift / a layout problem, not a missing extra.
err = ModuleNotFoundError(
"No module named 'alphafold.common'", name="alphafold.common"
)
fake_import(err)
with pytest.raises(ModuleNotFoundError):
_import_failure("alphafold.model.data")


def test_import_error_cannot_import_name_re_raises(fake_import):
# The reviewer's exact scenario: package installed, but a symbol is
# missing because of version skew. Must surface — installing
# ``-E alphafold`` should not produce silent test-collection skips.
fake_import(
ImportError(
"cannot import name 'data' from 'alphafold.model' "
"(/opt/venv/lib/.../alphafold/model/__init__.py)"
)
)
with pytest.raises(ImportError):
_import_failure("alphafold.model.data")


def test_runtime_error_unrelated_to_extras_re_raises(fake_import):
# A genuine bug in batch.py top-level code — must NOT be hidden by the
# batch-specific RuntimeError allowance.
fake_import(RuntimeError("some unrelated runtime failure"))
with pytest.raises(RuntimeError, match="some unrelated runtime failure"):
_import_failure("colabfold.batch")


def test_runtime_error_alphafold_message_from_non_batch_re_raises(fake_import):
# The batch-specific allowance must not extend to other modules, even
# if someone elsewhere happens to raise a similarly-worded RuntimeError.
fake_import(RuntimeError("alphafold is not installed"))
with pytest.raises(RuntimeError):
_import_failure("alphafold.model.data")


def test_attribute_error_re_raises(fake_import):
# Classic "code bug in a module's top-level execution" — must surface.
fake_import(AttributeError("module 'jax' has no attribute 'tree_util'"))
with pytest.raises(AttributeError):
_import_failure("colabfold.batch")


def test_type_error_re_raises(fake_import):
fake_import(TypeError("metaclass conflict"))
with pytest.raises(TypeError):
_import_failure("colabfold.batch")