./alembic הסתיר את alembic
אחרי תיקון קטן ב־notifier ניסיתי להריץ את הטסטים. הלינט עבר, ה־compileall עבר, ו־pytest נפל מיד על:
ImportError while loading conftest 'tests/conftest.py'.
E ImportError: cannot import name 'command' from 'alembic' (unknown location)
הדבר המוזר היה ש־uv run python -c "from alembic import command" מהשורה שלי עבד מצוין. אותה venv, אותו interpreter, אבל תחת pytest — alembic הופך ל־unknown location.
מה שקרה
יש לי בריפו תיקייה ./alembic/ עם הגירציות (env.py, versions/, וכו’). תחת Python 3.14 ועם pytest שמוסיף את שורש הריפו ל־sys.path, התיקייה הזו נטענת כ־namespace package. היא לא מכילה את command או config — היא בסך הכל תיקייה עם קבצי SQL ו־env.py — אבל היא קודמת בחיפוש ל־site-packages.
זה הוצף רק עכשיו כי דברים אחרים השתנו במקביל: שדרגתי ל־Python 3.14, ה־pytest שאני מריץ הוא היה מערכתי 3.11 שהתבלבל עם ה־venv, וה־import של alembic ב־conftest הוא חדש יחסית (נוסף כשהתחלתי להריץ migrations אוטומטית בטסטים).
התיקון
ב־conftest.py, לפני ה־import:
def _migrate_test_db() -> None:
import sys
repo_root = Path(__file__).parent.parent
blocked = {str(repo_root), str(repo_root.resolve()), "", "."}
saved = sys.path[:]
sys.path[:] = [p for p in sys.path if p not in blocked]
sys.modules.pop("alembic", None)
try:
from alembic import command
from alembic.config import Config
finally:
sys.path[:] = saved
...
מסלקים את שורש הריפו מ־sys.path, מוחקים כל cache של alembic ב־sys.modules, מייבאים את האמיתי, ומחזירים את הכל למקום. מכוער, אבל יעבוד עד שאעביר את הגירציות לתיקייה עם שם שלא מתנגש (db_migrations/?).
הלקח
namespace packages ב־Python הם מנגנון שקוף עד שהוא לא. תיקייה ריקה־כמעט בשורש הריפו, עם שם של חבילה מותקנת, ובום — ה־import שלך טוען את הקליפה במקום החבילה. אם פעם תקבל cannot import name X from package (unknown location) והחבילה נראית מותקנת תקין — חפש תיקייה עם אותו שם ב־sys.path שלך.