mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-28 20:45:05 +02:00
implement cache optimization for requirements generation
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
@@ -6,6 +7,7 @@ import tempfile
|
||||
import textwrap
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
@@ -47,6 +49,7 @@ def temporary_config_folder(config_content: dict, levels: int = 0):
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"LANGGRAPH_CACHE_OPTIMIZE": "lock"})
|
||||
def test_prepare_args_and_stdin() -> None:
|
||||
# this basically serves as an end-to-end test for using config and docker helpers
|
||||
config_path = pathlib.Path(__file__).parent / "langgraph.json"
|
||||
@@ -143,15 +146,29 @@ services:
|
||||
dockerfile_inline: |
|
||||
# syntax=docker/dockerfile:1.4
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Generate requirements.txt for packages without one --
|
||||
# Copy packaging metadata files
|
||||
ADD pyproject.toml /tmp/dep_metadata/cli/pyproject.toml
|
||||
COPY --from=cli_1 pyproject.toml /tmp/dep_metadata/cli_1/pyproject.toml
|
||||
COPY --from=cli_1 uv.lock /tmp/dep_metadata/cli_1/uv.lock
|
||||
# Generate requirements.txt from packaging metadata
|
||||
RUN cd '/tmp/dep_metadata/cli' && uv pip compile pyproject.toml -o 'requirements.txt' --constraint /api/constraints.txt
|
||||
RUN cd '/tmp/dep_metadata/cli_1' && uv export --no-hashes --no-dev --no-emit-local -o 'requirements.txt'
|
||||
# -- End of requirements.txt generation --
|
||||
# -- Installing from requirements.txt files --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -r '/tmp/dep_metadata/cli/requirements.txt'
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -r '/tmp/dep_metadata/cli_1/requirements.txt'
|
||||
# -- End of requirements.txt install --
|
||||
# -- Adding local package . --
|
||||
ADD . /deps/cli
|
||||
# -- End of local package . --
|
||||
# -- Adding local package ../../.. --
|
||||
COPY --from=cli_1 . /deps/cli_1
|
||||
# -- End of local package ../../.. --
|
||||
# -- Installing all local dependencies --
|
||||
RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
# -- Adding full source for local package ../../.. --
|
||||
COPY --from=cli_1 . /deps/cli_1
|
||||
RUN cd /deps/cli_1 && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt --no-deps -e .
|
||||
# -- End of full source for local package ../../.. --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||
WORKDIR /deps/cli
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from langgraph_cli.config import config_to_docker, validate_config
|
||||
|
||||
FIXTURES_ROOT = (
|
||||
pathlib.Path(__file__).parent.parent / "local_cache_validation" / "projects"
|
||||
)
|
||||
CACHE_ENV = "LANGGRAPH_CACHE_OPTIMIZE"
|
||||
TIERS = ("off", "lock", "pyproject", "all")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
os.getenv("RUN_LOCAL_CACHE_VALIDATION") != "1",
|
||||
reason="Local-only cache optimization TDD tests.",
|
||||
)
|
||||
|
||||
|
||||
def _dockerfile_for_fixture(fixture_name: str, tier: str) -> str:
|
||||
config_path = FIXTURES_ROOT / fixture_name / "langgraph.json"
|
||||
with open(config_path, encoding="utf-8") as f:
|
||||
raw = json.load(f)
|
||||
config = validate_config(raw)
|
||||
|
||||
with patch.dict(os.environ, {CACHE_ENV: tier}):
|
||||
dockerfile, _ = config_to_docker(
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
return dockerfile
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fixture_name,expected",
|
||||
[
|
||||
(
|
||||
"real_pyproject_lock",
|
||||
{"off": False, "lock": True, "pyproject": True, "all": True},
|
||||
),
|
||||
(
|
||||
"real_pyproject_no_lock",
|
||||
{"off": False, "lock": False, "pyproject": True, "all": True},
|
||||
),
|
||||
(
|
||||
"real_setup_py",
|
||||
{"off": False, "lock": False, "pyproject": False, "all": True},
|
||||
),
|
||||
(
|
||||
"faux_package",
|
||||
{"off": False, "lock": False, "pyproject": False, "all": False},
|
||||
),
|
||||
(
|
||||
"pip_installer_fallback",
|
||||
{"off": False, "lock": False, "pyproject": False, "all": False},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_tier_deferred_copy_matrix(
|
||||
fixture_name: str, expected: dict[str, bool]
|
||||
) -> None:
|
||||
for tier in TIERS:
|
||||
dockerfile = _dockerfile_for_fixture(fixture_name, tier)
|
||||
has_no_deps_editable = "--no-deps -e ." in dockerfile
|
||||
assert has_no_deps_editable is expected[tier], (
|
||||
f"{fixture_name=} {tier=} expected deferred copy {expected[tier]}, "
|
||||
f"got {has_no_deps_editable}"
|
||||
)
|
||||
|
||||
|
||||
def test_requirements_before_no_deps_for_eligible_tier() -> None:
|
||||
dockerfile = _dockerfile_for_fixture("real_pyproject_lock", "lock")
|
||||
reqs_idx = dockerfile.find("Installing from requirements.txt files")
|
||||
no_deps_idx = dockerfile.find("--no-deps -e .")
|
||||
assert reqs_idx != -1
|
||||
assert no_deps_idx != -1
|
||||
assert reqs_idx < no_deps_idx
|
||||
|
||||
|
||||
def test_generation_command_selection_by_fixture() -> None:
|
||||
docker_lock = _dockerfile_for_fixture("real_pyproject_lock", "lock")
|
||||
assert "uv export --no-hashes --no-dev --no-emit-local" in docker_lock
|
||||
|
||||
docker_pyproject = _dockerfile_for_fixture("real_pyproject_no_lock", "pyproject")
|
||||
assert "uv pip compile pyproject.toml" in docker_pyproject
|
||||
|
||||
docker_setup = _dockerfile_for_fixture("real_setup_py", "all")
|
||||
assert "uv pip compile setup.py" in docker_setup
|
||||
|
||||
|
||||
def test_pip_installer_fallback_ignores_tier() -> None:
|
||||
outputs = {
|
||||
tier: _dockerfile_for_fixture("pip_installer_fallback", tier) for tier in TIERS
|
||||
}
|
||||
first = outputs["off"]
|
||||
for tier, dockerfile in outputs.items():
|
||||
assert dockerfile == first, f"Expected identical dockerfile for {tier=}"
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
import pathlib
|
||||
import tempfile
|
||||
import textwrap
|
||||
from unittest.mock import patch
|
||||
|
||||
import click
|
||||
import pytest
|
||||
@@ -420,6 +421,7 @@ def test_validate_config_multiplatform():
|
||||
|
||||
|
||||
# config_to_docker
|
||||
@patch.dict(os.environ, {"LANGGRAPH_CACHE_OPTIMIZE": "lock"})
|
||||
def test_config_to_docker_simple():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
@@ -436,10 +438,17 @@ def test_config_to_docker_simple():
|
||||
expected_docker_stdin = f"""\
|
||||
# syntax=docker/dockerfile:1.4
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Installing local requirements --
|
||||
COPY --from=outer-requirements.txt requirements.txt /deps/outer-graphs_reqs_a/graphs_reqs_a/requirements.txt
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -r /deps/outer-graphs_reqs_a/graphs_reqs_a/requirements.txt
|
||||
# -- End of local requirements install --
|
||||
# -- Generate requirements.txt for packages without one --
|
||||
# Copy packaging metadata files
|
||||
COPY --from=examples pyproject.toml /tmp/dep_metadata/examples/pyproject.toml
|
||||
# Generate requirements.txt from packaging metadata
|
||||
RUN cd '/tmp/dep_metadata/examples' && uv pip compile pyproject.toml -o 'requirements.txt' --constraint /api/constraints.txt
|
||||
# -- End of requirements.txt generation --
|
||||
# -- Installing from requirements.txt files --
|
||||
COPY --from=outer-graphs_reqs_a requirements.txt /deps/outer-graphs_reqs_a/graphs_reqs_a/requirements.txt
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -r '/deps/outer-graphs_reqs_a/graphs_reqs_a/requirements.txt'
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -r '/tmp/dep_metadata/examples/requirements.txt'
|
||||
# -- End of requirements.txt install --
|
||||
# -- Adding local package ../../examples --
|
||||
COPY --from=examples . /deps/examples
|
||||
# -- End of local package ../../examples --
|
||||
@@ -647,6 +656,7 @@ ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-graphs/src/agent.py:graph"}}'
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"LANGGRAPH_CACHE_OPTIMIZE": "lock"})
|
||||
def test_config_to_docker_pyproject():
|
||||
pyproject_str = """[project]
|
||||
name = "custom"
|
||||
@@ -670,6 +680,15 @@ dependencies = ["langchain"]"""
|
||||
os.remove(pyproject_path)
|
||||
expected_docker_stdin = (
|
||||
"""FROM langchain/langgraph-api:3.11
|
||||
# -- Generate requirements.txt for packages without one --
|
||||
# Copy packaging metadata files
|
||||
ADD pyproject.toml /tmp/dep_metadata/unit_tests/pyproject.toml
|
||||
# Generate requirements.txt from packaging metadata
|
||||
RUN cd '/tmp/dep_metadata/unit_tests' && uv pip compile pyproject.toml -o 'requirements.txt' --constraint /api/constraints.txt
|
||||
# -- End of requirements.txt generation --
|
||||
# -- Installing from requirements.txt files --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -r '/tmp/dep_metadata/unit_tests/requirements.txt'
|
||||
# -- End of requirements.txt install --
|
||||
# -- Adding local package . --
|
||||
ADD . /deps/unit_tests
|
||||
# -- End of local package . --
|
||||
|
||||
Reference in New Issue
Block a user