PYTHONPython

conftest

PYTHON
conftest.py🐍
"""Pytest configuration to avoid module name collisions for exercises files.

Every chapter folder has an `exercises.py`, and pytest's default Module collector
registers each under the module name `exercises`, causing collisions.  We hook into
pytest_pycollect_makemodule to intercept these files and ensure each is imported
under a unique, path-based module name.
"""

from __future__ import annotations

import hashlib
import importlib.util
import sys
from pathlib import Path

import pytest

ROOT_DIR = Path(__file__).parent


def _unique_module_name(file_path: Path) -> str:
    """Generate a unique, import-safe module name based on relative path."""
    try:
        relative = file_path.relative_to(ROOT_DIR)
    except ValueError:
        relative = file_path
    # e.g. "01_python_basics/exercises.py" -> "01_python_basics__exercises"
    stem = "__".join(relative.with_suffix("").parts)
    safe = "".join(c if c.isalnum() or c == "_" else "_" for c in stem)
    digest = hashlib.md5(str(relative).encode()).hexdigest()[:8]
    return f"exercises_{safe}_{digest}"


class UniqueExercisesModule(pytest.Module):
    """Module collector that imports exercises.py under a unique module name."""

    def _getobj(self):
        path = self.path
        module_name = _unique_module_name(path)

        # Remove any prior 'exercises' entry to avoid mismatch errors
        sys.modules.pop("exercises", None)

        spec = importlib.util.spec_from_file_location(module_name, str(path))
        if spec is None or spec.loader is None:
            raise ImportError(f"Cannot load {path}")

        module = importlib.util.module_from_spec(spec)
        sys.modules[module_name] = module
        spec.loader.exec_module(module)

        self.config.pluginmanager.consider_module(module)
        return module


def pytest_pycollect_makemodule(
    module_path: Path, parent: pytest.Collector
) -> pytest.Module | None:
    """Intercept exercises.py files and use our unique importer."""
    if module_path.name == "exercises.py":
        return UniqueExercisesModule.from_parent(parent, path=module_path)
    return None  # Let pytest handle other modules normally
PreviousNext