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
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
44 changes: 44 additions & 0 deletions tests/unittests/test_release_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
Loading