From e7a08c059b79bc5c03e16d921f1bfefd1d328181 Mon Sep 17 00:00:00 2001 From: Liang Wu <18244712+wuliang229@users.noreply.github.com> Date: Tue, 4 Aug 2026 00:26:08 +0000 Subject: [PATCH] fix(deps): exclude nltk 3.10.1 from the extras that pull rouge-score nltk 3.10.1 added nltk/inisec.py, an import hook that breaks CI and any in-project virtualenv. It reaches ADK transitively through rouge-score, which the eval and test extras pull in. The hook does two independently harmful things: 1. find_spec refuses any import whose origin resolves under the CWD. For the standard in-project venv layout (uv, in-project Poetry, plain `python -m venv .venv`) site-packages *is* under the CWD, so nltk blocks its own dependency and `import regex` raises. 2. _install() calls os.environ.setdefault("PYTHONSAFEPATH", "1"), which mutates the importing process's environment. Every subprocess later spawned with os.environ.copy() inherits -P semantics it never opted into. This fires even when the nltk import itself fails, is invisible in the child's traceback, and only manifests on Python 3.11+ because PYTHONSAFEPATH is a no-op on 3.10. Together these account for all 28 unit-test failures currently on main: the evaluation/CLI collection errors from (1), and the subprocess import-isolation failures from (2), which is why the latter reproduce on 3.11-3.14 but not on 3.10. Upstream tracked in nltk/nltk#3730. The maintainers' preferred fix, nltk/nltk#3732, removes inisec.py entirely and therefore clears both problems; `!=` rather than an upper bound so the fixed release is picked up automatically once it ships. --- pyproject.toml | 2 + tests/unittests/test_release_dependencies.py | 44 ++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 21cd6a1f11..a080ef9db9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -148,6 +148,7 @@ optional-dependencies.eval = [ "google-cloud-aiplatform[evaluation]>=1.148", "google-cloud-texttospeech>=2.37", "jinja2>=3.1.4,<4", # For eval template rendering + "nltk!=3.10.1", # Transitive via rouge-score; 3.10.1 ships an import hook that breaks in-project venvs. "pandas>=2.2.3", "rouge-score>=0.1.2", "tabulate>=0.9", @@ -244,6 +245,7 @@ optional-dependencies.test = [ "llama-index-readers-file>=0.4", "lxml>=5.3", "mcp>=1.24,<2", + "nltk!=3.10.1", # Transitive via rouge-score; 3.10.1 ships an import hook that breaks in-project venvs. "openai>=2.20,<3", "opentelemetry-exporter-gcp-logging>=1.9.0a0,<=1.12.0a0", "opentelemetry-exporter-gcp-monitoring>=1.9.0a0,<2", diff --git a/tests/unittests/test_release_dependencies.py b/tests/unittests/test_release_dependencies.py index 27d90b4df0..6192ecc717 100644 --- a/tests/unittests/test_release_dependencies.py +++ b/tests/unittests/test_release_dependencies.py @@ -27,6 +27,9 @@ objects while deserializing checkpoint data. * ``google-genai`` MUST exclude 2.11 and include 2.12.1, whose types module defers the optional MCP server stack instead of importing it at Agent startup. +* The extras that pull ``rouge-score`` MUST exclude ``nltk`` 3.10.1, whose + import hook breaks any in-project virtualenv and leaks ``PYTHONSAFEPATH`` + into the host process. """ from __future__ import annotations @@ -52,6 +55,15 @@ 'langgraph-checkpoint': (('2.1.0', '3.0.0', '4.0.0', '4.1.0'), '4.1.1'), } +# nltk 3.10.1 added nltk/inisec.py, whose import hook (a) refuses imports whose +# origin resolves under the CWD -- which includes site-packages for every +# in-project virtualenv -- and (b) sets PYTHONSAFEPATH=1 in the *host* process +# environment, so subprocesses inherit it. Extras that pull rouge-score (which +# depends on nltk) must resolve around it. See nltk/nltk#3730 and nltk/nltk#3732. +_NLTK_BROKEN_IMPORT_HOOK_RELEASE = '3.10.1' +_NLTK_LAST_GOOD_RELEASE = '3.10.0' +_NLTK_EXTRAS = ('eval', 'test') + def _find_pyproject() -> Path: """Locates pyproject.toml by walking up from this file's directory. @@ -164,6 +176,38 @@ def test_langgraph_extras_exclude_unsafe_checkpoint_releases( ) +@pytest.mark.parametrize('extra', _NLTK_EXTRAS) +def test_rouge_score_extras_exclude_broken_nltk_import_hook( + pyproject: dict, extra: str +) -> None: + """Every extra that pulls rouge-score resolves around nltk 3.10.1. + + rouge-score depends on nltk without an upper bound, so the exclusion has to + be declared by each extra that pulls it. + """ + specifier = _requirement_specifier( + pyproject['project']['optional-dependencies'][extra], 'nltk' + ) + + assert specifier is not None, ( + f'The {extra!r} extra pulls rouge-score, which depends on nltk without ' + 'an upper bound, so it must declare nltk itself to keep 3.10.1 out.' + ) + assert not specifier.contains(_NLTK_BROKEN_IMPORT_HOOK_RELEASE), ( + f'The {extra!r} extra admits nltk ' + f'{_NLTK_BROKEN_IMPORT_HOOK_RELEASE}, whose nltk/inisec.py import hook ' + 'blocks imports resolved from under the CWD (which includes ' + 'site-packages in any in-project virtualenv) and sets PYTHONSAFEPATH=1 ' + 'in the host process environment, changing import behavior for every ' + 'subprocess the host later spawns.' + ) + assert specifier.contains(_NLTK_LAST_GOOD_RELEASE), ( + f'The {extra!r} extra must still admit nltk ' + f'{_NLTK_LAST_GOOD_RELEASE}; excluding it too would leave the resolver ' + 'with no working nltk for rouge-score.' + ) + + def test_main_deps_require_lazy_mcp_google_genai_release( pyproject: dict, ) -> None: