PYTHON
conftest.py🐍python
"""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