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
11 changes: 9 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ The project provides type-safe builder classes for different environment types:

**UvBuilder** - Fast Python virtual environments via uv
- Created via `Appose.uv()` or `Appose.uv(source)`
- Type-safe methods: `include(packages...)`, `python(version)`
- Supports `requirements.txt` files
- Type-safe methods: `include(packages...)`, `python(version)`, `group(groups...)`
- Supports `requirements.txt` and `pyproject.toml`
- Standard Python venv structure (no special activation needed)
- Environment structure: `<envDir>/bin` (or `Scripts` on Windows)
- Location: `org.apposed.appose.builder.UvBuilder`
Expand Down Expand Up @@ -230,6 +230,13 @@ Environment env = Appose.uv()
.name("my-env")
.build();

// uv builder with dependency groups
Environment env = Appose.uv()
.content(pyprojectContent)
.scheme("pyproject.toml")
.group("appose")
.build();

// Dynamic builder (auto-detects)
Environment env = Appose.file("path/to/environment.yml")
.logDebug()
Expand Down
22 changes: 21 additions & 1 deletion src/main/java/org/apposed/appose/builder/UvBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public final class UvBuilder extends BaseBuilder<UvBuilder> {

private String pythonVersion;
private final List<String> packages = new ArrayList<>();
private final List<String> groups = new ArrayList<>();

// -- UvBuilder methods --

Expand All @@ -80,6 +81,18 @@ public UvBuilder include(String... packages) {
return this;
}

/**
* Adds PEP 735 dependency groups to install via {@code uv sync --group}.
* Only supported with {@code pyproject.toml} scheme.
*
* @param groups Dependency group names defined in {@code [dependency-groups]}.
* @return This builder instance, for fluent-style programming.
*/
public UvBuilder group(String... groups) {
this.groups.addAll(Arrays.asList(groups));
return this;
}

// -- Builder methods --

@Override
Expand All @@ -92,6 +105,7 @@ protected void addStateFields(Map<String, Object> state) {
super.addStateFields(state);
state.put("pythonVersion", pythonVersion);
state.put("packages", packages);
if (!groups.isEmpty()) state.put("groups", groups);
}

@Override
Expand Down Expand Up @@ -136,6 +150,12 @@ public Environment build() throws BuildException {
}
}

// Validate groups are only used with pyproject.toml.
if (!groups.isEmpty() && !"pyproject.toml".equals(scheme == null ? null : scheme.name())) {
throw new IllegalArgumentException(
"Dependency groups are only supported with pyproject.toml scheme");
}

try {
// If the env state matches our current configuration,
// skip all package management and return immediately.
Expand Down Expand Up @@ -164,7 +184,7 @@ public Environment build() throws BuildException {
Files.write(pyprojectFile.toPath(), content.getBytes(StandardCharsets.UTF_8));

// Run uv sync to create .venv and install dependencies.
uv.sync(envDir, pythonVersion);
uv.sync(envDir, pythonVersion, groups);
} else {
// Handle requirements.txt - traditional venv + pip install.
// Create virtual environment if it doesn't exist.
Expand Down
8 changes: 7 additions & 1 deletion src/main/java/org/apposed/appose/tool/Uv.java
Original file line number Diff line number Diff line change
Expand Up @@ -262,13 +262,19 @@ public void pipInstallFromRequirements(final File envDir, String requirementsFil
* @throws InterruptedException If the current thread is interrupted.
* @throws IllegalStateException if uv has not been installed
*/
public void sync(final File projectDir, String pythonVersion) throws IOException, InterruptedException {
public void sync(final File projectDir, String pythonVersion, List<String> groups) throws IOException, InterruptedException {
List<String> args = new ArrayList<>();
args.add("sync");
if (pythonVersion != null && !pythonVersion.isEmpty()) {
args.add("--python");
args.add(pythonVersion);
}
if (groups != null) {
for (String group : groups) {
args.add("--group");
args.add(group);
}
}

// Run uv sync with working directory set to projectDir.
exec(projectDir, args.toArray(new String[0]));
Expand Down
62 changes: 62 additions & 0 deletions src/test/java/org/apposed/appose/builder/UvBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,18 @@
import org.apposed.appose.Appose;
import org.apposed.appose.Environment;
import org.apposed.appose.TestBase;
import org.apposed.appose.util.Json;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;

import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/** End-to-end tests for {@link UvBuilder}. */
public class UvBuilderTest extends TestBase {
Expand Down Expand Up @@ -71,4 +80,57 @@ public void testUvPyproject() throws Exception {
.build();
cowsayAndAssert(env, "pyproject");
}

@Test
public void testUvPyprojectWithGroup() throws Exception {
Environment env = Appose
.uv("src/test/resources/envs/cowsay-pyproject-groups.toml")
.group("cowsay")
.base("target/envs/uv-cowsay-groups")
.logDebug()
.build();
cowsayAndAssert(env, "groups");
}

@Test
public void testUvGroupRejectsWithoutPyproject() {
assertThrows(IllegalArgumentException.class, () ->
Appose.uv()
.content("appose\n")
.group("cowsay")
.base("target/envs/uv-group-no-pyproject")
.build());
}

@Test
public void testUvStateNoGroupField() throws Exception {
Environment env = Appose
.uv("src/test/resources/envs/cowsay-pyproject.toml")
.base("target/envs/uv-state-no-groups")
.logDebug()
.build();
File apposeJson = new File(env.base(), "appose.json");
assertTrue(apposeJson.isFile(), "appose.json should exist");
String json = new String(Files.readAllBytes(apposeJson.toPath()), StandardCharsets.UTF_8);
@SuppressWarnings("unchecked")
java.util.Map<String, Object> state = (java.util.Map<String, Object>) Json.parseJson(json);
assertFalse(state.containsKey("groups"), "appose.json should not contain 'groups' when none specified");
assertNotNull(state.get("packages"), "appose.json should contain 'packages'");
}

@Test
public void testUvStateGroupFieldPresent() throws Exception {
Environment env = Appose
.uv("src/test/resources/envs/cowsay-pyproject-groups.toml")
.group("cowsay")
.base("target/envs/uv-state-with-groups")
.logDebug()
.build();
File apposeJson = new File(env.base(), "appose.json");
assertTrue(apposeJson.isFile(), "appose.json should exist");
String json = new String(Files.readAllBytes(apposeJson.toPath()), StandardCharsets.UTF_8);
@SuppressWarnings("unchecked")
java.util.Map<String, Object> state = (java.util.Map<String, Object>) Json.parseJson(json);
assertTrue(state.containsKey("groups"), "appose.json should contain 'groups' when specified");
}
}
13 changes: 13 additions & 0 deletions src/test/resources/envs/cowsay-pyproject-groups.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[project]
name = "cowsay-groups-test"
version = "0.1.0"
description = "Test project for cowsay with dependency groups"
requires-python = ">=3.10"
dependencies = [
"appose>=0.1.0",
]

[dependency-groups]
cowsay = [
"cowsay>=6.0",
]