diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py index 4460f3f41..2fe9ef834 100644 --- a/libs/cli/langgraph_cli/cli.py +++ b/libs/cli/langgraph_cli/cli.py @@ -45,7 +45,7 @@ OPT_CONFIG = click.option( - "graphs": mapping from graph ID to path where the compiled graph is defined, i.e. ./your_package/your_file.py:variable, where "variable" is an instance of langgraph.graph.graph.CompiledGraph - "env": (optional) path to .env file or a mapping from environment variable to its value - - "python_version": (optional) 3.11 or 3.12. Defaults to 3.11 + - "python_version": (optional) 3.11, 3.12, or 3.13. Defaults to 3.11 - "pip_config_file": (optional) path to pip config file - "dockerfile_lines": (optional) array of additional lines to add to Dockerfile following the import from parent image diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index 16473083f..549fbffbc 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -17,6 +17,18 @@ class Config(TypedDict): env: Union[dict[str, str], str] +MIN_PYTHON_VERSION = "3.11" + + +def _parse_version(version_str: str) -> tuple[int, int]: + """Parse a version string into a tuple of (major, minor).""" + try: + major, minor = map(int, version_str.split(".")) + return (major, minor) + except ValueError: + raise click.UsageError(f"Invalid version format: {version_str}") from None + + def validate_config(config: Config) -> Config: config = ( { @@ -44,14 +56,21 @@ def validate_config(config: Config) -> Config: ) if config.get("python_version"): - if config["python_version"] not in ( - "3.11", - "3.12", + pyversion = config["python_version"] + if not pyversion.count(".") == 1 or not all( + part.isdigit() for part in pyversion.split(".") ): raise click.UsageError( - f"Unsupported Python version: {config['python_version']}. " - "Supported versions are 3.11 and 3.12." + f"Invalid Python version format: {pyversion}. " + "Use 'major.minor' format (e.g., '3.11'). " + "Patch version cannot be specified." ) + if _parse_version(pyversion) < _parse_version(MIN_PYTHON_VERSION): + raise click.UsageError( + f"Python version {pyversion} is not supported. " + f"Minimum required version is {MIN_PYTHON_VERSION}." + ) + if not config["dependencies"]: raise click.UsageError( "No dependencies found in config. " diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index cff16ead6..d12c660cd 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -42,6 +42,9 @@ def test_validate_config(): } actual_config = validate_config(expected_config) assert actual_config == expected_config + expected_config["python_version"] = "3.13" + actual_config = validate_config(expected_config) + assert actual_config == expected_config # check wrong python version raises with pytest.raises(click.UsageError): @@ -61,6 +64,22 @@ def test_validate_config(): with pytest.raises(click.UsageError): validate_config({"python_version": "3.9", "dependencies": ["."]}) + with pytest.raises(click.UsageError) as exc_info: + validate_config({"python_version": "3.11.0"}) + assert "Invalid Python version format" in str(exc_info.value) + + with pytest.raises(click.UsageError) as exc_info: + validate_config({"python_version": "3"}) + assert "Invalid Python version format" in str(exc_info.value) + + with pytest.raises(click.UsageError) as exc_info: + validate_config({"python_version": "abc.def"}) + assert "Invalid Python version format" in str(exc_info.value) + + with pytest.raises(click.UsageError) as exc_info: + validate_config({"python_version": "3.10"}) + assert "Minimum required version" in str(exc_info.value) + # config_to_docker def test_config_to_docker_simple():