diff --git a/helm/blueapi/config_schema.json b/helm/blueapi/config_schema.json index 0755a2c6e..102fab4f3 100644 --- a/helm/blueapi/config_schema.json +++ b/helm/blueapi/config_schema.json @@ -399,6 +399,12 @@ }, "title": "Repositories", "type": "array" + }, + "use_uv_lock": { + "default": false, + "description": "Whether to install the repository using uv.lock (only valid for single repository)", + "title": "Use Uv Lock", + "type": "boolean" } }, "title": "ScratchConfig", diff --git a/helm/blueapi/values.schema.json b/helm/blueapi/values.schema.json index 3020e5502..9b4522612 100644 --- a/helm/blueapi/values.schema.json +++ b/helm/blueapi/values.schema.json @@ -824,6 +824,12 @@ "default": "/tmp/scratch/blueapi", "type": "string", "format": "path" + }, + "use_uv_lock": { + "title": "Use Uv Lock", + "description": "Whether to install the repository using uv.lock (only valid for single repository)", + "default": false, + "type": "boolean" } }, "additionalProperties": false diff --git a/src/blueapi/cli/scratch.py b/src/blueapi/cli/scratch.py index 6fa01d83e..a9a3d60a9 100644 --- a/src/blueapi/cli/scratch.py +++ b/src/blueapi/cli/scratch.py @@ -66,6 +66,7 @@ def setup_scratch( scratch_install( *(config.root / repo.name for repo in config.repositories), + use_uv_lock=config.use_uv_lock, timeout=install_timeout, ) @@ -116,7 +117,11 @@ def ensure_repo( ) -def scratch_install(*paths: Path, timeout: float = _DEFAULT_INSTALL_TIMEOUT) -> None: +def scratch_install( + *paths: Path, + use_uv_lock: bool = False, + timeout: float = _DEFAULT_INSTALL_TIMEOUT, +) -> None: """ Install scratch packages. Make blueapi aware of repositories checked out in the scratch area. Make it automatically follow code changes to those repositories @@ -125,21 +130,39 @@ def scratch_install(*paths: Path, timeout: float = _DEFAULT_INSTALL_TIMEOUT) -> Args: paths: List of Paths to the checked out repositories + use_uv_lock: Whether to install using uv.lock + (only applies if there a single repository) timeout: Time to wait for installation subprocess """ if not paths: return + LOGGER.info("Installing packages") - for path in paths: - _validate_directory(path) - args = ["uv", "pip", "install", "-e", str(path)] - process = Popen(args) + + def _run_install(args: list[str], cwd: Path | None = None) -> None: + if cwd is not None: + process = Popen(args, cwd=cwd) + else: + process = Popen(args) process.wait(timeout=timeout) if process.returncode != 0: raise RuntimeError( f"Failed to install packages: Exit Code: {process.returncode}" ) + if use_uv_lock and len(paths) == 1: + path = paths[0] + _validate_directory(path) + _run_install(["uv", "sync", "--inexact"], cwd=path) + else: + if use_uv_lock: + LOGGER.warning( + "use_uv_lock is ignored when installing multiple repositories" + ) + for path in paths: + _validate_directory(path) + _run_install(["uv", "pip", "install", "-e", str(path)]) + def _validate_root_directory(root_path: Path, required_gid: int | None) -> None: _validate_directory(root_path) diff --git a/src/blueapi/config.py b/src/blueapi/config.py index f65dd65f0..d0dda27ab 100644 --- a/src/blueapi/config.py +++ b/src/blueapi/config.py @@ -236,6 +236,13 @@ class ScratchConfig(BlueapiBaseModel): description="Details of repositories to be cloned and imported into blueapi", default_factory=list, ) + use_uv_lock: bool = Field( + description=( + "Whether to install the repository using uv.lock " + "(only valid for single repository)" + ), + default=False, + ) class OIDCConfig(BlueapiBaseModel): diff --git a/tests/unit_tests/cli/test_scratch.py b/tests/unit_tests/cli/test_scratch.py index 22f3ad3d8..2544c1cae 100644 --- a/tests/unit_tests/cli/test_scratch.py +++ b/tests/unit_tests/cli/test_scratch.py @@ -62,6 +62,53 @@ def test_scratch_install_installs_path( ) +@patch("blueapi.cli.scratch.Popen") +def test_scratch_install_with_uv_lock( + mock_popen: Mock, + directory_path_with_sgid: Path, +): + mock_process = Mock() + mock_process.returncode = 0 + mock_popen.return_value = mock_process + + scratch_install(directory_path_with_sgid, use_uv_lock=True, timeout=1.0) + + mock_popen.assert_called_once_with( + ["uv", "sync", "--inexact"], cwd=directory_path_with_sgid + ) + + +@patch("blueapi.cli.scratch.Popen") +@patch("blueapi.cli.scratch.LOGGER") +def test_scratch_install_ignores_uv_lock_with_multiple_repos( + mock_logger: Mock, + mock_popen: Mock, + directory_path_with_sgid: Path, +): + mock_process = Mock() + mock_process.returncode = 0 + mock_popen.return_value = mock_process + + scratch_install( + directory_path_with_sgid, + directory_path_with_sgid, + use_uv_lock=True, + timeout=1.0, + ) + + mock_logger.warning.assert_called_once_with( + "use_uv_lock is ignored when installing multiple repositories" + ) + mock_popen.assert_has_calls( + [ + call(["uv", "pip", "install", "-e", str(directory_path_with_sgid)]), + call().wait(timeout=1.0), + call(["uv", "pip", "install", "-e", str(directory_path_with_sgid)]), + call().wait(timeout=1.0), + ] + ) + + def test_scratch_install_fails_on_file(file_path: Path): with pytest.raises(KeyError): scratch_install(file_path, timeout=1.0) @@ -348,6 +395,7 @@ def test_setup_scratch_iterates_repos( call( directory_path_with_sgid / "foo", directory_path_with_sgid / "bar", + use_uv_lock=False, timeout=120.0, ), ] diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index a335434df..a4ae7e818 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -335,6 +335,7 @@ def test_config_yaml_parsed(temp_yaml_config_file): "remote_url": "https://github.com/DiamondLightSource/dodal.git", } ], + "use_uv_lock": False, }, "opa": { "root": "http://opa.example.com/", @@ -398,6 +399,7 @@ def test_config_yaml_parsed(temp_yaml_config_file): "remote_url": "https://github.com/DiamondLightSource/dodal.git", } ], + "use_uv_lock": False, }, "opa": None, },