Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions helm/blueapi/config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions helm/blueapi/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 28 additions & 5 deletions src/blueapi/cli/scratch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions src/blueapi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
48 changes: 48 additions & 0 deletions tests/unit_tests/cli/test_scratch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
),
]
Expand Down
2 changes: 2 additions & 0 deletions tests/unit_tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/",
Expand Down Expand Up @@ -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,
},
Expand Down
Loading