From f7c31346bbea4edaefec4f272d2ddaaf54b42d76 Mon Sep 17 00:00:00 2001 From: kert Date: Fri, 11 Sep 2026 16:20:23 -0400 Subject: [PATCH] =?UTF-8?q?fix(pfs,cli):=20the=20CPT=20parser=20imports=20?= =?UTF-8?q?rex=20lazily=20so=20the=20stack=20CLI=20starts=20in=20the=20api?= =?UTF-8?q?=20image=20(no=20fsspec=20there)=20=E2=80=94=20the=20nightly=20?= =?UTF-8?q?Zotero=20sync=20and=20mail=20poller=20run=20inside=20it=20(refs?= =?UTF-8?q?=20#718)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pfs/cpt_epub.py | 6 +++- .../test_cli_import_without_optional_deps.py | 32 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/cli/test_cli_import_without_optional_deps.py diff --git a/src/pfs/cpt_epub.py b/src/pfs/cpt_epub.py index e9642e1..4f64a83 100644 --- a/src/pfs/cpt_epub.py +++ b/src/pfs/cpt_epub.py @@ -47,7 +47,6 @@ from pfs.cpt_model import ( CptReference, CptSection, ) -from rex.comments.epub_text import content_root # One CPT code: 4 digits + a digit/letter (99490, 0202U, 99213F, 0042T, 0001A). _CODE = re.compile(r"\b\d{4}[0-9A-Z]\b") @@ -906,6 +905,11 @@ def parse_epub(path: Path, *, year: int | None = None) -> CptEdition: # Resolved once via container.xml -> the OPF's own path, not # hard-coded — falls back to "OPS/" (every edition checked so # far) when container.xml is missing/malformed (F2). + # Lazy: importing rex.comments pulls in the rex package (fsspec), + # which the api image's `cli` extra does not carry — the CLI must + # import without it (tests/cli/test_cli_import_without_optional_deps.py). + from rex.comments.epub_text import content_root + root = content_root(zf) toc_ranges: dict[str, tuple[str, str]] = {} diff --git a/tests/cli/test_cli_import_without_optional_deps.py b/tests/cli/test_cli_import_without_optional_deps.py new file mode 100644 index 0000000..57c0520 --- /dev/null +++ b/tests/cli/test_cli_import_without_optional_deps.py @@ -0,0 +1,32 @@ +"""The api image installs the ``cli`` extra only (no fsspec/s3fs/obstore — +those belong to the lake/rex extras). ``stack`` must still start there: +the nightly Zotero sync and the mail poller run inside that image. On +2026-09-11 a module-level ``pfs.cpt_load → pfs.cpt_epub → rex`` import +chain made every ``stack`` invocation in the api image die with +``ModuleNotFoundError: fsspec``.""" + +from __future__ import annotations + +import subprocess +import sys + +_PROBE = """ +import builtins +_real = builtins.__import__ +def _fake(name, *a, **k): + if name.split('.')[0] in {'fsspec', 's3fs', 'obstore'}: + raise ModuleNotFoundError(name) + return _real(name, *a, **k) +builtins.__import__ = _fake +import cli # the `stack` entry point package +import cli.pfs, cli.bib, cli.llm +print('cli ok') +""" + + +def test_stack_cli_imports_without_lake_extras(): + r = subprocess.run( + [sys.executable, "-c", _PROBE], capture_output=True, text=True, check=False + ) + assert r.returncode == 0, r.stderr[-1200:] + assert "cli ok" in r.stdout