feat: Customizable Pip Installer (#5098)

Let you set "pip_installer": "pip" (or uv) to handle corner cases in install compatibilities
This commit is contained in:
William FH
2025-06-13 10:35:28 -07:00
committed by GitHub
parent 0171e9a323
commit 4548a0ebe8
6 changed files with 199 additions and 4 deletions
+27 -2
View File
@@ -383,6 +383,14 @@ class Config(TypedDict, total=False):
Only relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.
"""
pip_installer: Optional[str]
"""Optional. Python package installer to use ('auto', 'pip', 'uv').
- 'auto' (default): Use uv for supported base images, otherwise pip
- 'pip': Force use of pip regardless of base image support
- 'uv': Force use of uv (will fail if base image doesn't support it)
"""
dockerfile_lines: list[str]
"""Optional. Additional Docker instructions that will be appended to your base Dockerfile.
@@ -536,6 +544,7 @@ def validate_config(config: Config) -> Config:
"node_version": node_version,
"python_version": python_version,
"pip_config_file": config.get("pip_config_file"),
"pip_installer": config.get("pip_installer", "auto"),
"_INTERNAL_docker_tag": config.get("_INTERNAL_docker_tag"),
"base_image": config.get("base_image"),
"image_distro": image_distro,
@@ -600,6 +609,13 @@ def validate_config(config: Config) -> Config:
"Must be either 'debian' or 'wolfi'."
)
if pip_installer := config.get("pip_installer"):
if pip_installer not in ["auto", "pip", "uv"]:
raise click.UsageError(
f"Invalid pip_installer: '{pip_installer}'. "
"Must be 'auto', 'pip', or 'uv'."
)
# Validate auth config
if auth_conf := config.get("auth"):
if "path" in auth_conf:
@@ -1114,12 +1130,21 @@ def python_config_to_docker(
base_image: str,
) -> tuple[str, dict[str, str]]:
"""Generate a Dockerfile from the configuration."""
if _image_supports_uv(base_image):
pip_installer = config.get("pip_installer", "auto")
if pip_installer == "uv":
install_cmd = "uv pip install --system"
uv_removal = "RUN uv pip uninstall --system pip setuptools wheel && rm /usr/bin/uv /usr/bin/uvx"
else:
elif pip_installer == "pip":
install_cmd = "pip install"
uv_removal = ""
else:
if _image_supports_uv(base_image):
install_cmd = "uv pip install --system"
uv_removal = "RUN uv pip uninstall --system pip setuptools wheel && rm /usr/bin/uv /usr/bin/uvx"
else:
install_cmd = "pip install"
uv_removal = ""
# configure pip
pip_install = f"PYTHONDONTWRITEBYTECODE=1 {install_cmd} --no-cache-dir -c /api/constraints.txt"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-cli"
version = "0.3.1"
version = "0.3.2"
description = "CLI for interacting with LangGraph API"
authors = []
requires-python = ">=3.9"
+22
View File
@@ -134,6 +134,17 @@
],
"description": "Optional. Linux distribution for the base image.\n\nMust be either 'debian' or 'wolfi'. If omitted, defaults to 'debian'.\n"
},
"pip_installer": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional. Python package installer to use ('auto', 'pip', 'uv').\n\n"
},
"store": {
"anyOf": [
{
@@ -287,6 +298,17 @@
],
"description": "Optional. Linux distribution for the base image.\n\nMust be either 'debian' or 'wolfi'. If omitted, defaults to 'debian'.\n"
},
"pip_installer": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional. Python package installer to use ('auto', 'pip', 'uv').\n\n"
},
"store": {
"anyOf": [
{
+22
View File
@@ -134,6 +134,17 @@
],
"description": "Optional. Linux distribution for the base image.\n\nMust be either 'debian' or 'wolfi'. If omitted, defaults to 'debian'.\n"
},
"pip_installer": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional. Python package installer to use ('auto', 'pip', 'uv').\n\n"
},
"store": {
"anyOf": [
{
@@ -287,6 +298,17 @@
],
"description": "Optional. Linux distribution for the base image.\n\nMust be either 'debian' or 'wolfi'. If omitted, defaults to 'debian'.\n"
},
"pip_installer": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional. Python package installer to use ('auto', 'pip', 'uv').\n\n"
},
"store": {
"anyOf": [
{
+126
View File
@@ -1,3 +1,4 @@
import copy
import json
import os
import pathlib
@@ -40,6 +41,7 @@ def test_validate_config():
"python_version": "3.11",
"node_version": None,
"pip_config_file": None,
"pip_installer": "auto",
"image_distro": "debian",
"dockerfile_lines": [],
"env": {},
@@ -61,6 +63,7 @@ def test_validate_config():
"python_version": "3.12",
"node_version": None,
"pip_config_file": "pipconfig.txt",
"pip_installer": "auto",
"image_distro": "debian",
"dockerfile_lines": ["ARG meow"],
"dependencies": [".", "langchain"],
@@ -216,6 +219,74 @@ def test_validate_config_image_distro():
assert config["image_distro"] == "debian"
def test_validate_config_pip_installer():
"""Test validation of pip_installer field."""
# Valid pip_installer values should work
config = validate_config(
{
"python_version": "3.11",
"dependencies": ["."],
"graphs": {"agent": "./agent.py:graph"},
"pip_installer": "auto",
}
)
assert config["pip_installer"] == "auto"
config = validate_config(
{
"python_version": "3.11",
"dependencies": ["."],
"graphs": {"agent": "./agent.py:graph"},
"pip_installer": "pip",
}
)
assert config["pip_installer"] == "pip"
config = validate_config(
{
"python_version": "3.11",
"dependencies": ["."],
"graphs": {"agent": "./agent.py:graph"},
"pip_installer": "uv",
}
)
assert config["pip_installer"] == "uv"
# Missing pip_installer should default to "auto"
config = validate_config(
{
"python_version": "3.11",
"dependencies": ["."],
"graphs": {"agent": "./agent.py:graph"},
}
)
assert config["pip_installer"] == "auto"
# Invalid pip_installer values should raise error
with pytest.raises(click.UsageError) as exc_info:
validate_config(
{
"python_version": "3.11",
"dependencies": ["."],
"graphs": {"agent": "./agent.py:graph"},
"pip_installer": "conda",
}
)
assert "Invalid pip_installer: 'conda'" in str(exc_info.value)
assert "Must be 'auto', 'pip', or 'uv'" in str(exc_info.value)
with pytest.raises(click.UsageError) as exc_info:
validate_config(
{
"python_version": "3.11",
"dependencies": ["."],
"graphs": {"agent": "./agent.py:graph"},
"pip_installer": "invalid",
}
)
assert "Invalid pip_installer: 'invalid'" in str(exc_info.value)
def test_validate_config_file():
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir_path = pathlib.Path(tmpdir)
@@ -799,6 +870,61 @@ WORKDIR /deps/__outer_unit_tests/unit_tests"""
assert additional_contexts == {}
def test_config_to_docker_pip_installer():
"""Test that pip_installer setting affects the generated Dockerfile."""
graphs = {"agent": "./graphs/agent.py:graph"}
base_config = {
"python_version": "3.11",
"dependencies": ["."],
"graphs": graphs,
}
# Test default (auto) behavior with UV-supporting image
config_auto = validate_config(
{**copy.deepcopy(base_config), "pip_installer": "auto"}
)
docker_auto, _ = config_to_docker(
PATH_TO_CONFIG, config_auto, "langchain/langgraph-api:0.2.47"
)
assert "uv pip install --system" in docker_auto
assert "rm /usr/bin/uv /usr/bin/uvx" in docker_auto
# Test explicit pip setting
config_pip = validate_config({**copy.deepcopy(base_config), "pip_installer": "pip"})
docker_pip, _ = config_to_docker(
PATH_TO_CONFIG, config_pip, "langchain/langgraph-api:0.2.47"
)
assert "uv pip install --system" not in docker_pip
assert "pip install" in docker_pip
assert "rm /usr/bin/uv" not in docker_pip
# Test explicit uv setting
config_uv = validate_config({**copy.deepcopy(base_config), "pip_installer": "uv"})
docker_uv, _ = config_to_docker(
PATH_TO_CONFIG, config_uv, "langchain/langgraph-api:0.2.47"
)
assert "uv pip install --system" in docker_uv
assert "rm /usr/bin/uv /usr/bin/uvx" in docker_uv
# Test auto behavior with older image (should use pip)
config_auto_old = validate_config(
{**copy.deepcopy(base_config), "pip_installer": "auto"}
)
docker_auto_old, _ = config_to_docker(
PATH_TO_CONFIG, config_auto_old, "langchain/langgraph-api:0.2.46"
)
assert "uv pip install --system" not in docker_auto_old
assert "pip install" in docker_auto_old
assert "rm /usr/bin/uv" not in docker_auto_old
# Test that missing pip_installer defaults to auto behavior
config_default = validate_config(copy.deepcopy(base_config))
docker_default, _ = config_to_docker(
PATH_TO_CONFIG, config_default, "langchain/langgraph-api:0.2.47"
)
assert "uv pip install --system" in docker_default
# config_to_compose
def test_config_to_compose_simple_config():
graphs = {"agent": "./agent.py:graph"}
+1 -1
View File
@@ -501,7 +501,7 @@ wheels = [
[[package]]
name = "langgraph-cli"
version = "0.3.1"
version = "0.3.2"
source = { editable = "." }
dependencies = [
{ name = "click" },