From 5a8624fdfdb7e8d7f9ca1c293bb1540307b846c7 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 17 Feb 2025 11:43:55 -0800 Subject: [PATCH] cli: Add support for dependencies in parent directories - Now supporting local dependencies in directories that are not contained in the docker context (ie. outside the folder containing langgraph.json) - This is achieved by passing each parent directorty as an additional context to docker build - This makes it a lot easier to build projects contained in monorepos where you need to include some sibling/parent folder as a dependency - Also include additional comments in the generated dockerfile to delimit each section --- libs/cli/langgraph_cli/cli.py | 43 +++++-- libs/cli/langgraph_cli/config.py | 123 ++++++++++++++----- libs/cli/tests/unit_tests/cli/langgraph.json | 0 libs/cli/tests/unit_tests/cli/pyproject.toml | 0 libs/cli/tests/unit_tests/cli/test_cli.py | 21 +++- libs/cli/tests/unit_tests/test_config.py | 101 ++++++++++++++- 6 files changed, 232 insertions(+), 56 deletions(-) create mode 100644 libs/cli/tests/unit_tests/cli/langgraph.json create mode 100644 libs/cli/tests/unit_tests/cli/pyproject.toml diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py index 6a53cbca9..33ff804e9 100644 --- a/libs/cli/langgraph_cli/cli.py +++ b/libs/cli/langgraph_cli/cli.py @@ -303,7 +303,15 @@ def _build( tag, ] # apply config - stdin = langgraph_cli.config.config_to_docker(config, config_json, base_image) + stdin, additional_contexts = langgraph_cli.config.config_to_docker( + config, config_json, base_image + ) + # add additional_contexts + if additional_contexts: + additional_contexts_str = ",".join( + f"{k}={v}" for k, v in additional_contexts.items() + ) + args.extend(["--build-context", additional_contexts_str]) # run docker build runner.run( subp_exec( @@ -439,20 +447,28 @@ def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) - secho("✅ Configuration validated!", fg="green") secho(f"📝 Generating Dockerfile at {save_path}", fg="yellow") + dockerfile, additional_contexts = langgraph_cli.config.config_to_docker( + config, + config_json, + ( + "langchain/langgraphjs-api" + if config_json.get("node_version") + else "langchain/langgraph-api" + ), + ) with open(str(save_path), "w", encoding="utf-8") as f: - f.write( - langgraph_cli.config.config_to_docker( - config, - config_json, - ( - "langchain/langgraphjs-api" - if config_json.get("node_version") - else "langchain/langgraph-api" - ), - ) - ) + f.write(dockerfile) secho("✅ Created: Dockerfile", fg="green") + if additional_contexts: + additional_contexts_str = ",".join( + f"{k}={v}" for k, v in additional_contexts.items() + ) + secho( + f"""📝 Run docker build with these additional build contexts `--build-context {additional_contexts_str}`""", + fg="yellow", + ) + if add_docker_compose: # Add docker compose and related files # Add .dockerignore file in the same directory as the Dockerfile @@ -575,7 +591,7 @@ def dev( ): """CLI entrypoint for running the LangGraph API server.""" try: - from langgraph_api.cli import run_server + from langgraph_api.cli import run_server # type: ignore except ImportError: py_version_msg = "" if sys.version_info < (3, 11): @@ -662,6 +678,7 @@ def prepare_args_and_stdin( debugger_base_url: Optional[str] = None, postgres_uri: Optional[str] = None, ) -> Tuple[List[str], str]: + assert config_path.exists(), f"Config file not found: {config_path}" # prepare args stdin = langgraph_cli.docker.compose( capabilities, diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index 590a90559..8ba773779 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -2,6 +2,7 @@ import json import os import pathlib import textwrap +from collections import Counter from typing import NamedTuple, Optional, TypedDict, Union import click @@ -294,10 +295,10 @@ class LocalDeps(NamedTuple): tuples. Each entry points to a local `requirements.txt` file and where it should be placed inside the Docker container before running `pip install`. - real_pkgs: A dictionary mapping a local directory path (host side) to the - same dependency string from the config. These directories contain the - necessary files (e.g., `pyproject.toml` or `setup.py`) to be installed - as a standard Python package with pip. + real_pkgs: A dictionary mapping a local directory path (host side) to a + tuple of (dependency_string, container_package_path). These directories + contain the necessary files (e.g., `pyproject.toml` or `setup.py`) to be + installed as a standard Python package with pip. faux_pkgs: A dictionary mapping a local directory path (host side) to a tuple of (dependency_string, container_package_path). For these @@ -310,16 +311,23 @@ class LocalDeps(NamedTuple): directory. If the local dependency `"."` is present in the config, this field captures the path where that dependency will appear in the container (e.g., `/deps/` or similar). Otherwise, it may be `None`. + + additional_contexts: A list of paths to directories that contain local + dependencies in parent directories. These directories are added to the + Docker build context to ensure that the Dockerfile can access them. """ pip_reqs: list[tuple[str, str]] - real_pkgs: dict[pathlib.Path, str] + real_pkgs: dict[pathlib.Path, tuple[str, str]] faux_pkgs: dict[pathlib.Path, tuple[str, str]] # if . is in dependencies, use it as working_dir working_dir: Optional[str] = None + # if there are local dependencies in parent directories, use additional_contexts + additional_contexts: list[pathlib.Path] = None def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps: + config_path = config_path.resolve() # ensure reserved package names are not used reserved = { "src", @@ -336,6 +344,7 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps "httpx", "langsmith", } + counter = Counter() def check_reserved(name: str, ref: str): if name in reserved: @@ -348,7 +357,8 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps pip_reqs = [] real_pkgs = {} faux_pkgs = {} - working_dir = None + working_dir: Optional[str] = None + additional_contexts: list[pathlib.Path] = [] for local_dep in config["dependencies"]: if not local_dep.startswith("."): @@ -357,7 +367,7 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps # Verify that the local dependency can be resolved # (e.g., this would raise an informative error if a user mistyped a path). - resolved = config_path.parent / local_dep + resolved = (config_path.parent / local_dep).resolve() # validate local dependency if not resolved.exists(): @@ -366,25 +376,28 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps raise NotADirectoryError( f"Local dependency must be a directory: {resolved}" ) - elif not resolved.is_relative_to(config_path.parent): - raise ValueError( - f"Local dependency '{resolved}' must be a subdirectory of '{config_path.parent}'" - ) + elif resolved == config_path.parent: + pass + elif config_path.parent not in resolved.parents: + additional_contexts.append(resolved) # Check for pyproject.toml or setup.py # If found, treat as a real package, if not treat as a faux package. # For faux packages, we'll also check for presence of requirements.txt. files = os.listdir(resolved) - if "pyproject.toml" in files: + if "pyproject.toml" in files or "setup.py" in files: # real package - real_pkgs[resolved] = local_dep + + # assign a unique folder name + container_name = resolved.name + if counter[container_name] > 0: + container_name += f"_{counter[container_name]}" + counter[container_name] += 1 + # add to deps + real_pkgs[resolved] = (local_dep, container_name) + # set working_dir if local_dep == ".": - working_dir = f"/deps/{resolved.name}" - elif "setup.py" in files: - # real package - real_pkgs[resolved] = local_dep - if local_dep == ".": - working_dir = f"/deps/{resolved.name}" + working_dir = f"/deps/{container_name}" else: # We could not find a pyproject.toml or setup.py, so treat as a faux package if any(file == "__init__.py" for file in files): @@ -428,7 +441,7 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps ) ) - return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir) + return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir, additional_contexts) def _update_graph_paths( @@ -556,7 +569,7 @@ def _update_auth_path( def python_config_to_docker( config_path: pathlib.Path, config: Config, base_image: str -) -> str: +) -> tuple[str, dict[str, str]]: """Generate a Dockerfile from the configuration.""" # configure pip pip_install = ( @@ -591,7 +604,14 @@ def python_config_to_docker( # https://setuptools.pypa.io/en/latest/userguide/datafiles.html#package-data # https://til.simonwillison.net/python/pyproject faux_pkgs_str = f"{os.linesep}{os.linesep}".join( - f"""ADD {relpath} {destpath} + ( + f"""# -- Adding non-package dependency {fullpath.name} -- +COPY --from=__outer_{fullpath.name} . {destpath}""" + if fullpath in local_deps.additional_contexts + else f"""# -- Adding non-package dependency {fullpath.name} -- +ADD {relpath} {destpath}""" + ) + + f""" RUN set -ex && \\ for line in '[project]' \\ 'name = "{fullpath.name}"' \\ @@ -599,12 +619,20 @@ RUN set -ex && \\ '[tool.setuptools.package-data]' \\ '"*" = ["**/*"]'; do \\ echo "$line" >> /deps/__outer_{fullpath.name}/pyproject.toml; \\ - done""" + done +# -- End of non-package dependency {fullpath.name} --""" for fullpath, (relpath, destpath) in local_deps.faux_pkgs.items() ) + local_pkgs_str = os.linesep.join( - f"ADD {relpath} /deps/{fullpath.name}" - for fullpath, relpath in local_deps.real_pkgs.items() + f"""# -- Adding local package {relpath} -- +COPY --from={name} . /deps/{name} +# -- End of local package {relpath} --""" + if fullpath in local_deps.additional_contexts + else f"""# -- Adding local package {relpath} -- +ADD {relpath} /deps/{name} +# -- End of local package {relpath} --""" + for fullpath, (relpath, name) in local_deps.real_pkgs.items() ) installs = f"{os.linesep}{os.linesep}".join( @@ -638,15 +666,30 @@ RUN set -ex && \\ "", installs, "", + "# -- Installing all local dependencies --", f"RUN {pip_install} -e /deps/*", + "# -- End of local dependencies install --", os.linesep.join(env_vars), "", f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "", ] - return os.linesep.join(docker_file_contents) + + additional_contexts: dict[str, str] = {} + for p in local_deps.additional_contexts: + if p in local_deps.real_pkgs: + name = local_deps.real_pkgs[p][1] + elif p in local_deps.faux_pkgs: + name = f"__outer_{p.name}" + else: + raise RuntimeError(f"Unknown additional context: {p}") + additional_contexts[name] = str(p) + + return os.linesep.join(docker_file_contents), additional_contexts -def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str): +def node_config_to_docker( + config_path: pathlib.Path, config: Config, base_image: str +) -> tuple[str, dict[str, str]]: faux_path = f"/deps/{config_path.parent.name}" def test_file(file_name): @@ -686,7 +729,8 @@ ENV LANGGRAPH_STORE='{json.dumps(store_config)}' ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}' """ - return f"""FROM {base_image}:{config['node_version']} + return ( + f"""FROM {base_image}:{config['node_version']} {os.linesep.join(config["dockerfile_lines"])} @@ -698,10 +742,14 @@ ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}' WORKDIR {faux_path} -RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts""" +RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts""", + {}, + ) -def config_to_docker(config_path: pathlib.Path, config: Config, base_image: str): +def config_to_docker( + config_path: pathlib.Path, config: Config, base_image: str +) -> tuple[str, dict[str, str]]: if config.get("node_version"): return node_config_to_docker(config_path, config, base_image) @@ -737,13 +785,24 @@ def config_to_compose( else: watch_str = "" + dockerfile, additional_contexts = config_to_docker(config_path, config, base_image) + + additional_contexts_str = "\n".join( + f" - {name}: {path}" + for name, path in additional_contexts.items() + ) + if additional_contexts_str: + additional_contexts_str = f""" + additional_contexts: +{additional_contexts_str}""" + return f""" {textwrap.indent(env_vars_str, " ")} {env_file_str} pull_policy: build build: - context: . + context: .{additional_contexts_str} dockerfile_inline: | -{textwrap.indent(config_to_docker(config_path, config, base_image), " ")} +{textwrap.indent(dockerfile, " ")} {watch_str} """ diff --git a/libs/cli/tests/unit_tests/cli/langgraph.json b/libs/cli/tests/unit_tests/cli/langgraph.json new file mode 100644 index 000000000..e69de29bb diff --git a/libs/cli/tests/unit_tests/cli/pyproject.toml b/libs/cli/tests/unit_tests/cli/pyproject.toml new file mode 100644 index 000000000..e69de29bb diff --git a/libs/cli/tests/unit_tests/cli/test_cli.py b/libs/cli/tests/unit_tests/cli/test_cli.py index 3b0e0dae6..100101fc3 100644 --- a/libs/cli/tests/unit_tests/cli/test_cli.py +++ b/libs/cli/tests/unit_tests/cli/test_cli.py @@ -40,9 +40,9 @@ def temporary_config_folder(config_content: dict): 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("./langgraph.json") + config_path = pathlib.Path(__file__).parent / "langgraph.json" config = validate_config( - Config(dependencies=["."], graphs={"agent": "agent.py:graph"}) + Config(dependencies=[".", "../../.."], graphs={"agent": "agent.py:graph"}) ) port = 8000 debugger_port = 8001 @@ -61,7 +61,7 @@ def test_prepare_args_and_stdin() -> None: expected_args = [ "--project-directory", - ".", + str(pathlib.Path(__file__).parent.absolute()), "-f", "custom-docker-compose.yml", "-f", @@ -129,18 +129,29 @@ services: pull_policy: build build: context: . + additional_contexts: + - cli_1: /Users/nuno/dev/langgraph/libs/cli dockerfile_inline: | FROM langchain/langgraph-api:3.11 - ADD . /deps/ + # -- 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 PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* + # -- End of local dependencies install -- ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}' - WORKDIR /deps/ + WORKDIR /deps/cli develop: watch: - path: langgraph.json action: rebuild - path: . + action: rebuild + - path: ../../.. action: rebuild\ """ assert actual_args == expected_args diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index 27aea10bd..6613f9f1d 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -177,13 +177,14 @@ def test_validate_config_file(): # config_to_docker def test_config_to_docker_simple(): graphs = {"agent": "./agent.py:graph"} - actual_docker_stdin = config_to_docker( + actual_docker_stdin, additional_contexts = config_to_docker( PATH_TO_CONFIG, validate_config({"dependencies": ["."], "graphs": graphs}), "langchain/langgraph-api", ) expected_docker_stdin = """\ FROM langchain/langgraph-api:3.11 +# -- Adding non-package dependency unit_tests -- ADD . /deps/__outer_unit_tests/unit_tests RUN set -ex && \\ for line in '[project]' \\ @@ -193,16 +194,63 @@ RUN set -ex && \\ '"*" = ["**/*"]'; do \\ echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ done +# -- End of non-package dependency unit_tests -- +# -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* +# -- End of local dependencies install -- ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' WORKDIR /deps/__outer_unit_tests/unit_tests\ """ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin + assert additional_contexts == {} + + +def test_config_to_docker_outside_path(): + graphs = {"agent": "./agent.py:graph"} + actual_docker_stdin, additional_contexts = config_to_docker( + PATH_TO_CONFIG, + validate_config({"dependencies": [".", ".."], "graphs": graphs}), + "langchain/langgraph-api", + ) + expected_docker_stdin = """\ +FROM langchain/langgraph-api:3.11 +# -- Adding non-package dependency unit_tests -- +ADD . /deps/__outer_unit_tests/unit_tests +RUN set -ex && \\ + for line in '[project]' \\ + 'name = "unit_tests"' \\ + 'version = "0.1"' \\ + '[tool.setuptools.package-data]' \\ + '"*" = ["**/*"]'; do \\ + echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ + done +# -- End of non-package dependency unit_tests -- +# -- Adding non-package dependency tests -- +COPY --from=__outer_tests . /deps/__outer_tests/tests +RUN set -ex && \\ + for line in '[project]' \\ + 'name = "tests"' \\ + 'version = "0.1"' \\ + '[tool.setuptools.package-data]' \\ + '"*" = ["**/*"]'; do \\ + echo "$line" >> /deps/__outer_tests/pyproject.toml; \\ + done +# -- End of non-package dependency tests -- +# -- Installing all local dependencies -- +RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* +# -- End of local dependencies install -- +ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' +WORKDIR /deps/__outer_unit_tests/unit_tests\ +""" + assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin + assert additional_contexts == { + "__outer_tests": "/Users/nuno/dev/langgraph/libs/cli/tests" + } def test_config_to_docker_pipconfig(): graphs = {"agent": "./agent.py:graph"} - actual_docker_stdin = config_to_docker( + actual_docker_stdin, additional_contexts = config_to_docker( PATH_TO_CONFIG, validate_config( { @@ -216,6 +264,7 @@ def test_config_to_docker_pipconfig(): expected_docker_stdin = """\ FROM langchain/langgraph-api:3.11 ADD pipconfig.txt /pipconfig.txt +# -- Adding non-package dependency unit_tests -- ADD . /deps/__outer_unit_tests/unit_tests RUN set -ex && \\ for line in '[project]' \\ @@ -225,11 +274,15 @@ RUN set -ex && \\ '"*" = ["**/*"]'; do \\ echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ done +# -- End of non-package dependency unit_tests -- +# -- Installing all local dependencies -- RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* +# -- End of local dependencies install -- ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' WORKDIR /deps/__outer_unit_tests/unit_tests\ """ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin + assert additional_contexts == {} def test_config_to_docker_invalid_inputs(): @@ -254,7 +307,7 @@ def test_config_to_docker_invalid_inputs(): def test_config_to_docker_local_deps(): graphs = {"agent": "./graphs/agent.py:graph"} - actual_docker_stdin = config_to_docker( + actual_docker_stdin, additional_contexts = config_to_docker( PATH_TO_CONFIG, validate_config( { @@ -266,6 +319,7 @@ def test_config_to_docker_local_deps(): ) expected_docker_stdin = """\ FROM langchain/langgraph-api-custom:3.11 +# -- Adding non-package dependency graphs -- ADD ./graphs /deps/__outer_graphs/src RUN set -ex && \\ for line in '[project]' \\ @@ -275,10 +329,14 @@ RUN set -ex && \\ '"*" = ["**/*"]'; do \\ echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\ done +# -- End of non-package dependency graphs -- +# -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* +# -- End of local dependencies install -- ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'\ """ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin + assert additional_contexts == {} def test_config_to_docker_pyproject(): @@ -291,7 +349,7 @@ dependencies = ["langchain"]""" f.write(pyproject_str) graphs = {"agent": "./graphs/agent.py:graph"} - actual_docker_stdin = config_to_docker( + actual_docker_stdin, additional_contexts = config_to_docker( PATH_TO_CONFIG, validate_config( { @@ -303,16 +361,21 @@ dependencies = ["langchain"]""" ) os.remove(pyproject_path) expected_docker_stdin = """FROM langchain/langgraph-api:3.11 +# -- Adding local package . -- ADD . /deps/unit_tests +# -- End of local package . -- +# -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* +# -- End of local dependencies install -- ENV LANGSERVE_GRAPHS='{"agent": "/deps/unit_tests/graphs/agent.py:graph"}' WORKDIR /deps/unit_tests""" assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin + assert additional_contexts == {} def test_config_to_docker_end_to_end(): graphs = {"agent": "./graphs/agent.py:graph"} - actual_docker_stdin = config_to_docker( + actual_docker_stdin, additional_contexts = config_to_docker( PATH_TO_CONFIG, validate_config( { @@ -330,6 +393,7 @@ ARG meow ARG foo ADD pipconfig.txt /pipconfig.txt RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain langchain_openai +# -- Adding non-package dependency graphs -- ADD ./graphs/ /deps/__outer_graphs/src RUN set -ex && \\ for line in '[project]' \\ @@ -339,15 +403,19 @@ RUN set -ex && \\ '"*" = ["**/*"]'; do \\ echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\ done +# -- End of non-package dependency graphs -- +# -- Installing all local dependencies -- RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* +# -- End of local dependencies install -- ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'""" assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin + assert additional_contexts == {} # node.js build used for LangGraph Cloud def test_config_to_docker_nodejs(): graphs = {"agent": "./graphs/agent.js:graph"} - actual_docker_stdin = config_to_docker( + actual_docker_stdin, additional_contexts = config_to_docker( PATH_TO_CONFIG, validate_config( { @@ -368,6 +436,7 @@ WORKDIR /deps/unit_tests RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts""" assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin + assert additional_contexts == {} # config_to_compose @@ -380,6 +449,7 @@ def test_config_to_compose_simple_config(): context: . dockerfile_inline: | FROM langchain/langgraph-api:3.11 + # -- Adding non-package dependency unit_tests -- ADD . /deps/__outer_unit_tests/unit_tests RUN set -ex && \\ for line in '[project]' \\ @@ -389,7 +459,10 @@ def test_config_to_compose_simple_config(): '"*" = ["**/*"]'; do \\ echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ done + # -- End of non-package dependency unit_tests -- + # -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* + # -- End of local dependencies install -- ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' WORKDIR /deps/__outer_unit_tests/unit_tests """ @@ -410,6 +483,7 @@ def test_config_to_compose_env_vars(): context: . dockerfile_inline: | FROM langchain/langgraph-api-custom:3.11 + # -- Adding non-package dependency unit_tests -- ADD . /deps/__outer_unit_tests/unit_tests RUN set -ex && \\ for line in '[project]' \\ @@ -419,7 +493,10 @@ def test_config_to_compose_env_vars(): '"*" = ["**/*"]'; do \\ echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ done + # -- End of non-package dependency unit_tests -- + # -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* + # -- End of local dependencies install -- ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' WORKDIR /deps/__outer_unit_tests/unit_tests """ @@ -447,6 +524,7 @@ def test_config_to_compose_env_file(): context: . dockerfile_inline: | FROM langchain/langgraph-api:3.11 + # -- Adding non-package dependency unit_tests -- ADD . /deps/__outer_unit_tests/unit_tests RUN set -ex && \\ for line in '[project]' \\ @@ -456,7 +534,10 @@ def test_config_to_compose_env_file(): '"*" = ["**/*"]'; do \\ echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ done + # -- End of non-package dependency unit_tests -- + # -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* + # -- End of local dependencies install -- ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' WORKDIR /deps/__outer_unit_tests/unit_tests """ @@ -477,6 +558,7 @@ def test_config_to_compose_watch(): context: . dockerfile_inline: | FROM langchain/langgraph-api:3.11 + # -- Adding non-package dependency unit_tests -- ADD . /deps/__outer_unit_tests/unit_tests RUN set -ex && \\ for line in '[project]' \\ @@ -486,7 +568,10 @@ def test_config_to_compose_watch(): '"*" = ["**/*"]'; do \\ echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ done + # -- End of non-package dependency unit_tests -- + # -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* + # -- End of local dependencies install -- ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' WORKDIR /deps/__outer_unit_tests/unit_tests @@ -516,6 +601,7 @@ def test_config_to_compose_end_to_end(): context: . dockerfile_inline: | FROM langchain/langgraph-api:3.11 + # -- Adding non-package dependency unit_tests -- ADD . /deps/__outer_unit_tests/unit_tests RUN set -ex && \\ for line in '[project]' \\ @@ -525,7 +611,10 @@ def test_config_to_compose_end_to_end(): '"*" = ["**/*"]'; do \\ echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ done + # -- End of non-package dependency unit_tests -- + # -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* + # -- End of local dependencies install -- ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' WORKDIR /deps/__outer_unit_tests/unit_tests