From d2499b7e2f8f2337a417530c469e4af5b8d519b7 Mon Sep 17 00:00:00 2001 From: enieuwy Date: Mon, 29 Jun 2026 22:36:29 +0800 Subject: [PATCH] fix: guard browser path against unsupported Python (3.14+) On Python 3.14 every CloakBrowser/Playwright launch dies with the cryptic 'Sync API inside the asyncio loop', breaking all institutional fetches (issue #9). Two guards: - requires-python capped to '>=3.10,<3.14' so new installs fail fast with a clear pip message until Playwright supports 3.14. - prepare_cloakbrowser_runtime() now logs a clear, actionable warning on Python >= 3.14 (browser needs 3.10-3.13; OA/arXiv still work), so an existing 3.14 env explains the failure instead of crashing cryptically. Adds a unit test for the version check. No effect on supported Pythons. --- instsci/cloakbrowser_compat.py | 33 +++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- tests/test_python_guard.py | 27 +++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 tests/test_python_guard.py diff --git a/instsci/cloakbrowser_compat.py b/instsci/cloakbrowser_compat.py index b7ea680..8998873 100644 --- a/instsci/cloakbrowser_compat.py +++ b/instsci/cloakbrowser_compat.py @@ -2,8 +2,10 @@ from __future__ import annotations +import logging import os import platform +import sys from pathlib import Path from typing import Any @@ -11,6 +13,14 @@ _INSTSCI_CACHE_ENV = "INSTSCI_CLOAKBROWSER_CACHE_DIR" _BUILTIN_CACHE_DIR = Path(__file__).resolve().parent / "_browsers" / "cloakbrowser" +logger = logging.getLogger(__name__) + +# Playwright's sync API (driven by CloakBrowser) does not run on Python 3.14, +# where it raises "Sync API inside the asyncio loop" and breaks every browser +# fetch. Guard the browser path so the failure is explained, not cryptic. +_MAX_BROWSER_PYTHON = (3, 13) +_python_warning_emitted = False + def configure_builtin_cloakbrowser( cache_dir: str | os.PathLike[str] | None = None, @@ -35,8 +45,31 @@ def configure_builtin_cloakbrowser( return target +def browser_python_warning(version: tuple[int, ...] | None = None) -> str | None: + """Return a message if the running Python is too new for the browser path. + + Playwright's sync API (driven by CloakBrowser) fails on Python >= 3.14 with + "Sync API inside the asyncio loop", breaking every institutional/browser + fetch. Open Access and arXiv (HTTP) fetches are unaffected. + """ + ver = tuple((version or sys.version_info[:2])[:2]) + if ver > _MAX_BROWSER_PYTHON: + return ( + f"Python {ver[0]}.{ver[1]} is not supported for InstSci's browser " + "(CloakBrowser/Playwright) workflows, which require Python 3.10-3.13. " + "Use a 3.12/3.13 environment for institutional access. Open Access and " + "arXiv fetches still work on any supported Python." + ) + return None + + def prepare_cloakbrowser_runtime(config_module: Any | None = None) -> Path: """Configure InstSci's CloakBrowser runtime before importing launch APIs.""" + global _python_warning_emitted + warning = browser_python_warning() + if warning and not _python_warning_emitted: + logger.warning("%s", warning) + _python_warning_emitted = True cache_dir = configure_builtin_cloakbrowser() ensure_cloakbrowser_platform_compatible(config_module) return cache_dir diff --git a/pyproject.toml b/pyproject.toml index d671789..f78ed9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "instsci" version = "0.1.1" description = "Academic paper fetcher with institutional access support" -requires-python = ">=3.10" +requires-python = ">=3.10,<3.14" dependencies = [ "typer>=0.9.0", "rich>=13.0.0", diff --git a/tests/test_python_guard.py b/tests/test_python_guard.py new file mode 100644 index 0000000..1f57aaa --- /dev/null +++ b/tests/test_python_guard.py @@ -0,0 +1,27 @@ +import unittest + +from instsci.cloakbrowser_compat import browser_python_warning + + +class BrowserPythonGuardTests(unittest.TestCase): + """Browser path requires Python 3.10-3.13; 3.14+ must warn clearly.""" + + def test_supported_versions_no_warning(self): + for v in [(3, 10), (3, 11), (3, 12), (3, 13), (3, 13, 2)]: + with self.subTest(v=v): + self.assertIsNone(browser_python_warning(v)) + + def test_unsupported_versions_warn(self): + for v in [(3, 14), (3, 14, 1), (3, 15), (4, 0)]: + with self.subTest(v=v): + msg = browser_python_warning(v) + self.assertIsNotNone(msg) + self.assertIn(f"{v[0]}.{v[1]}", msg) + + def test_default_uses_running_interpreter(self): + # Must not raise regardless of the interpreter running the suite. + browser_python_warning() + + +if __name__ == "__main__": + unittest.main()