diff --git a/lib/cli/src/crewai_cli/run_crew.py b/lib/cli/src/crewai_cli/run_crew.py index 549a8c035..03eeb2a92 100644 --- a/lib/cli/src/crewai_cli/run_crew.py +++ b/lib/cli/src/crewai_cli/run_crew.py @@ -258,8 +258,24 @@ def _run_json_crew(trained_agents_file: str | None = None) -> Any: def _has_lockfile(project_root: Path | None = None) -> bool: """Return True when the project already has a dependency lockfile.""" + return _has_uv_lockfile(project_root) or _has_poetry_lockfile(project_root) + + +def _has_uv_lockfile(project_root: Path | None = None) -> bool: + """Return True when the project has a uv lockfile.""" root = project_root or Path.cwd() - return (root / "uv.lock").is_file() or (root / "poetry.lock").is_file() + return (root / "uv.lock").is_file() + + +def _has_poetry_lockfile(project_root: Path | None = None) -> bool: + """Return True when the project has a Poetry lockfile.""" + root = project_root or Path.cwd() + return (root / "poetry.lock").is_file() + + +def _uses_poetry_lockfile(project_root: Path | None = None) -> bool: + """Return True when Poetry is the only available lock source.""" + return _has_poetry_lockfile(project_root) and not _has_uv_lockfile(project_root) def _has_project_venv(project_root: Path | None = None) -> bool: @@ -274,14 +290,17 @@ def _install_json_crew_dependencies_if_needed() -> None: if not (project_root / "pyproject.toml").is_file(): return - has_lockfile = _has_lockfile(project_root) + has_uv_lockfile = _has_uv_lockfile(project_root) + has_lockfile = has_uv_lockfile or _has_poetry_lockfile(project_root) if has_lockfile and _has_project_venv(project_root): return + if _uses_poetry_lockfile(project_root): + return from crewai_cli.install_crew import install_crew try: - if has_lockfile: + if has_uv_lockfile: click.echo("Syncing dependencies from lockfile...") install_crew(["--frozen"], raise_on_error=True) else: @@ -302,6 +321,13 @@ def _find_local_crewai_source_dir() -> Path | None: return None +def _json_crew_run_command(project_root: Path | None = None) -> list[str]: + """Return the project-environment command for running JSON crews.""" + if _uses_poetry_lockfile(project_root): + return ["poetry", "run", "python", "-c", _JSON_CREW_RUNNER_CODE] + return ["uv", "run", "--no-sync", "python", "-c", _JSON_CREW_RUNNER_CODE] + + def _run_json_crew_in_project_env(trained_agents_file: str | None = None) -> Any: """Run JSON crews from the project's uv-managed environment.""" if not (Path.cwd() / "pyproject.toml").is_file(): @@ -309,7 +335,7 @@ def _run_json_crew_in_project_env(trained_agents_file: str | None = None) -> Any _install_json_crew_dependencies_if_needed() - command = ["uv", "run", "--no-sync", "python", "-c", _JSON_CREW_RUNNER_CODE] + command = _json_crew_run_command() env = build_env_with_all_tool_credentials() env[_CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV] = str(Path(__file__).resolve().parent) if local_crewai_source_dir := _find_local_crewai_source_dir(): diff --git a/lib/cli/tests/test_run_crew.py b/lib/cli/tests/test_run_crew.py index 49f2310a0..d6565c2cc 100644 --- a/lib/cli/tests/test_run_crew.py +++ b/lib/cli/tests/test_run_crew.py @@ -90,6 +90,60 @@ def test_json_run_uses_project_env_when_pyproject_exists(monkeypatch, tmp_path: ] +def test_json_run_uses_poetry_run_for_poetry_lock_without_uv_lock( + monkeypatch, tmp_path: Path +): + monkeypatch.chdir(tmp_path) + (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") + (tmp_path / "poetry.lock").write_text("# lock\n") + monkeypatch.setattr( + run_crew_module, + "_install_json_crew_dependencies_if_needed", + lambda: None, + ) + monkeypatch.setattr( + run_crew_module, + "build_env_with_all_tool_credentials", + lambda: {}, + ) + subprocess_calls = [] + + def fake_subprocess_run(command, **kwargs): + subprocess_calls.append((command, kwargs)) + + monkeypatch.setattr(run_crew_module.subprocess, "run", fake_subprocess_run) + + run_crew_module._run_json_crew_in_project_env() + + expected_env = { + run_crew_module._CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV: str( + Path(run_crew_module.__file__).resolve().parent + ), + } + if local_crewai_source_dir := run_crew_module._find_local_crewai_source_dir(): + expected_env[run_crew_module._CREWAI_RUNNER_SOURCE_DIR_ENV] = str( + local_crewai_source_dir + ) + + assert subprocess_calls == [ + ( + [ + "poetry", + "run", + "python", + "-c", + run_crew_module._JSON_CREW_RUNNER_CODE, + ], + { + "capture_output": False, + "text": True, + "check": True, + "env": expected_env, + }, + ) + ] + + def test_json_runner_code_loads_current_cli_package_over_project_env(tmp_path: Path): old_parent = tmp_path / "old" old_pkg = old_parent / "crewai_cli" @@ -199,13 +253,12 @@ def test_json_run_installs_dependencies_when_pyproject_has_no_lockfile( assert calls == [([], True, None)] -@pytest.mark.parametrize("lockfile", ["uv.lock", "poetry.lock"]) -def test_json_run_syncs_frozen_when_lockfile_exists_without_venv( - monkeypatch, tmp_path: Path, lockfile: str +def test_json_run_syncs_frozen_when_uv_lock_exists_without_venv( + monkeypatch, tmp_path: Path ): monkeypatch.chdir(tmp_path) (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") - (tmp_path / lockfile).write_text("# lock\n") + (tmp_path / "uv.lock").write_text("# lock\n") calls = [] def fake_install_crew( @@ -220,6 +273,26 @@ def test_json_run_syncs_frozen_when_lockfile_exists_without_venv( assert calls == [(["--frozen"], True, None)] +def test_json_run_skips_uv_sync_when_only_poetry_lock_exists_without_venv( + monkeypatch, tmp_path: Path +): + monkeypatch.chdir(tmp_path) + (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") + (tmp_path / "poetry.lock").write_text("# lock\n") + calls = [] + + def fake_install_crew( + proxy_options, *, raise_on_error=False, install_project=None + ): + calls.append((proxy_options, raise_on_error, install_project)) + + monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew) + + run_crew_module._install_json_crew_dependencies_if_needed() + + assert calls == [] + + @pytest.mark.parametrize("lockfile", ["uv.lock", "poetry.lock"]) def test_json_run_skips_dependency_install_when_lockfile_and_venv_exist( monkeypatch, tmp_path: Path, lockfile: str