Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions instsci/cloakbrowser_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@

from __future__ import annotations

import logging
import os
import platform
import sys
from pathlib import Path
from typing import Any

_CLOAKBROWSER_CACHE_ENV = "CLOAKBROWSER_CACHE_DIR"
_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,
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 27 additions & 0 deletions tests/test_python_guard.py
Original file line number Diff line number Diff line change
@@ -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()