Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.blackduck.integration.detectable.detectables.uv;


import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
Expand All @@ -9,20 +10,34 @@
public class UVDetectorOptions {
private final Set<String> excludedDependencyGroups;

private final Set<String> onlyDependencyGroups;

private final Set<String> includedWorkspaceMembers;

private final Set<String> excludedWorkspaceMembers;

public UVDetectorOptions(List<String> excludedDependencyGroups, List<String> includedWorkspaceMembers, List<String> excludedWorkspaceMembers) {
public UVDetectorOptions(List<String> excludedDependencyGroups, List<String> onlyDependencyGroups, List<String> includedWorkspaceMembers, List<String> excludedWorkspaceMembers) {
this.excludedDependencyGroups = new HashSet<>(excludedDependencyGroups);
this.onlyDependencyGroups = new HashSet<>(onlyDependencyGroups);
this.includedWorkspaceMembers = new HashSet<>(includedWorkspaceMembers);
this.excludedWorkspaceMembers = new HashSet<>(excludedWorkspaceMembers);
}
Comment thread
bd-spratikbharti marked this conversation as resolved.

/**
* Backward-compatible constructor that defaults to an empty onlyDependencyGroups list.
*/
public UVDetectorOptions(List<String> excludedDependencyGroups, List<String> includedWorkspaceMembers, List<String> excludedWorkspaceMembers) {
this(excludedDependencyGroups, Collections.emptyList(), includedWorkspaceMembers, excludedWorkspaceMembers);
}

public Set<String> getExcludedDependencyGroups() {
return excludedDependencyGroups.stream().map(String::toLowerCase).collect(Collectors.toSet());
}

public Set<String> getOnlyDependencyGroups() {
return onlyDependencyGroups.stream().map(String::toLowerCase).collect(Collectors.toSet());
}

public Set<String> getIncludedWorkspaceMembers() {
return includedWorkspaceMembers.stream().map(String::toLowerCase).collect(Collectors.toSet());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,23 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

public class UVBuildExtractor {

private static final String TREE_COMMAND = "tree";
private static final String NO_DEDUPE_FLAG = "--no-dedupe";
private static final String ALL_EXTRAS_FLAG = "--all-extras";
private static final String ALL_GROUPS_FLAG = "--all-groups";
private static final String NO_GROUP_FLAG = "--no-group";
private static final String ONLY_GROUP_FLAG = "--only-group";

private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final DetectableExecutableRunner executableRunner;
private final File sourceDirectory;
private final UVTreeDependencyGraphTransformer uvTreeDependencyGraphTransformer;


public UVBuildExtractor(DetectableExecutableRunner executableRunner, File sourceDirectory, UVTreeDependencyGraphTransformer uvTreeDependencyGraphTransformer) {
this.executableRunner = executableRunner;
this.sourceDirectory = sourceDirectory;
Expand All @@ -35,18 +43,8 @@ public UVBuildExtractor(DetectableExecutableRunner executableRunner, File source

public Extraction extract(ExecutableTarget uvExe, UVDetectorOptions uvDetectorOptions, UVTomlParser uvTomlParser) throws ExecutableRunnerException {
try {
List<String> arguments = new ArrayList<>();
arguments.add("tree");
arguments.add("--no-dedupe");

if(!uvDetectorOptions.getExcludedDependencyGroups().isEmpty()) {
for(String group : uvDetectorOptions.getExcludedDependencyGroups()) {
arguments.add("--no-group");
arguments.add(group);
}
}

// run uv tree command
List<String> arguments = buildTreeCommandArguments(uvDetectorOptions);

ExecutableOutput executableOutput = executableRunner.executeSuccessfully(ExecutableUtils.createFromTarget(sourceDirectory, uvExe, arguments));
List<String> uvTreeOutput = executableOutput.getStandardOutputAsList();

Expand All @@ -62,4 +60,54 @@ public Extraction extract(ExecutableTarget uvExe, UVDetectorOptions uvDetectorOp
return new Extraction.Builder().exception(e).build();
}
}

private List<String> buildTreeCommandArguments(UVDetectorOptions uvDetectorOptions) {
List<String> arguments = new ArrayList<>();
arguments.add(TREE_COMMAND);
arguments.add(NO_DEDUPE_FLAG);

Set<String> onlyGroups = uvDetectorOptions.getOnlyDependencyGroups();
Set<String> excludedGroups = uvDetectorOptions.getExcludedDependencyGroups();

if (!onlyGroups.isEmpty()) {
addOnlyGroupArguments(arguments, onlyGroups, excludedGroups);
} else {
addDefaultGroupArguments(arguments, excludedGroups);
}
Comment thread
bd-spratikbharti marked this conversation as resolved.

return arguments;
}

private void addOnlyGroupArguments(List<String> arguments, Set<String> onlyGroups, Set<String> excludedGroups) {
Set<String> conflictingGroups = onlyGroups.stream()
.filter(excludedGroups::contains)
.collect(Collectors.toSet());

if (!conflictingGroups.isEmpty()) {
logger.warn(
"Dependency groups {} are present in both 'detect.uv.dependency.groups.only' and 'detect.uv.dependency.groups.excluded'. "
+ "The exclusion setting takes precedence; these groups will be excluded.",
conflictingGroups
);
}

Set<String> effectiveOnlyGroups = onlyGroups.stream()
.filter(group -> !excludedGroups.contains(group))
.collect(Collectors.toSet());

for (String group : effectiveOnlyGroups) {
arguments.add(ONLY_GROUP_FLAG);
arguments.add(group);
}
}

private void addDefaultGroupArguments(List<String> arguments, Set<String> excludedGroups) {
arguments.add(ALL_EXTRAS_FLAG);
arguments.add(ALL_GROUPS_FLAG);

for (String group : excludedGroups) {
arguments.add(NO_GROUP_FLAG);
arguments.add(group);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ protected void setup() throws IOException {
"│ │ │ │ │ ├── aiohttp v3.12.15 (*)\n" +
"│ │ │ │ │ ├── cryptography v44.0.3 (*)");

addExecutableOutput(uvTreeDependencyOutput, new File("uv").getAbsolutePath(), "tree", "--no-dedupe");
addExecutableOutput(uvTreeDependencyOutput, new File("uv").getAbsolutePath(), "tree", "--no-dedupe", "--all-extras", "--all-groups");
}

@NotNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ protected void setup() throws IOException {
" └── typing-extensions v4.9.0");

// Note: with --no-group dev, pytest and mypy are excluded from output
addExecutableOutput(uvTreeDependencyOutput, new File("uv").getAbsolutePath(), "tree", "--no-dedupe", "--no-group", "dev");
addExecutableOutput(uvTreeDependencyOutput, new File("uv").getAbsolutePath(), "tree", "--no-dedupe", "--all-extras", "--all-groups", "--no-group", "dev");
}

@NotNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ void extractBuildsBasicArguments() throws Exception {
List<String> arguments = captor.getValue().getCommandWithArguments();
assertTrue(arguments.contains("tree"));
assertTrue(arguments.contains("--no-dedupe"));
assertTrue(arguments.contains("--all-extras"));
assertTrue(arguments.contains("--all-groups"));
}

// ==================== Excluded Groups Tests ====================
Expand All @@ -103,6 +105,8 @@ void extractAddsNoGroupFlagsForExcludedGroups() throws Exception {
List<String> arguments = captor.getValue().getCommandWithArguments();
assertTrue(arguments.contains("tree"));
assertTrue(arguments.contains("--no-dedupe"));
assertTrue(arguments.contains("--all-extras"));
assertTrue(arguments.contains("--all-groups"));
assertTrue(arguments.contains("--no-group"));

int devIndex = arguments.indexOf("dev");
Expand Down Expand Up @@ -130,6 +134,8 @@ void extractWithEmptyExcludedGroupsAddsNoFlags() throws Exception {

List<String> arguments = captor.getValue().getCommandWithArguments();

assertTrue(arguments.contains("--all-extras"));
assertTrue(arguments.contains("--all-groups"));
long noGroupCount = arguments.stream().filter(arg -> arg.equals("--no-group")).count();
assertEquals(0, noGroupCount);
}
Expand All @@ -151,11 +157,135 @@ void extractWithSingleExcludedGroup() throws Exception {

List<String> arguments = captor.getValue().getCommandWithArguments();

assertTrue(arguments.contains("--all-extras"));
assertTrue(arguments.contains("--all-groups"));
long noGroupCount = arguments.stream().filter(arg -> arg.equals("--no-group")).count();
assertEquals(1, noGroupCount);
assertTrue(arguments.contains("dev"));
}

// ==================== Only Groups Tests ====================

@Test
void extractAddsOnlyGroupFlagsAndSkipsAllExtrasAllGroups() throws Exception {
UVBuildExtractor extractor = new UVBuildExtractor(executableRunner, tempDir, transformer);
UVDetectorOptions options = new UVDetectorOptions(
Collections.emptyList(), // excludedGroups
Arrays.asList("dev", "lint"), // onlyGroups
Collections.emptyList(),
Collections.emptyList()
);

Extraction extraction = extractor.extract(uvExe, options, tomlParser);
assertExtractionSuccess(extraction);

ArgumentCaptor<Executable> captor = ArgumentCaptor.forClass(Executable.class);
verify(executableRunner).executeSuccessfully(captor.capture());

List<String> arguments = captor.getValue().getCommandWithArguments();
assertTrue(arguments.contains("tree"));
assertTrue(arguments.contains("--no-dedupe"));

// --all-extras and --all-groups should NOT be present when onlyGroups is set
assertTrue(!arguments.contains("--all-extras"), "Expected --all-extras to be absent when onlyGroups is set");
assertTrue(!arguments.contains("--all-groups"), "Expected --all-groups to be absent when onlyGroups is set");

// --only-group flags should be present for each group
long onlyGroupCount = arguments.stream().filter(arg -> arg.equals("--only-group")).count();
assertEquals(2, onlyGroupCount);
assertTrue(arguments.contains("dev"));
assertTrue(arguments.contains("lint"));

// Each group should be preceded by --only-group
int devIndex = arguments.indexOf("dev");
int lintIndex = arguments.indexOf("lint");
assertTrue(devIndex > 0);
assertTrue(lintIndex > 0);
assertEquals("--only-group", arguments.get(devIndex - 1));
assertEquals("--only-group", arguments.get(lintIndex - 1));
}

@Test
void extractWithSingleOnlyGroup() throws Exception {
UVBuildExtractor extractor = new UVBuildExtractor(executableRunner, tempDir, transformer);
UVDetectorOptions options = new UVDetectorOptions(
Collections.emptyList(),
Arrays.asList("dev"),
Collections.emptyList(),
Collections.emptyList()
);

Extraction extraction = extractor.extract(uvExe, options, tomlParser);
assertExtractionSuccess(extraction);

ArgumentCaptor<Executable> captor = ArgumentCaptor.forClass(Executable.class);
verify(executableRunner).executeSuccessfully(captor.capture());

List<String> arguments = captor.getValue().getCommandWithArguments();

assertTrue(!arguments.contains("--all-extras"));
assertTrue(!arguments.contains("--all-groups"));
long onlyGroupCount = arguments.stream().filter(arg -> arg.equals("--only-group")).count();
assertEquals(1, onlyGroupCount);
assertTrue(arguments.contains("dev"));
}

// ==================== Conflict Handling Tests ====================

@Test
void extractExclusionTakesPrecedenceOverOnlyGroup() throws Exception {
UVBuildExtractor extractor = new UVBuildExtractor(executableRunner, tempDir, transformer);
// "dev" is in both only and excluded — exclusion should take precedence
UVDetectorOptions options = new UVDetectorOptions(
Arrays.asList("dev"), // excludedGroups
Arrays.asList("dev", "lint"), // onlyGroups
Collections.emptyList(),
Collections.emptyList()
);

Extraction extraction = extractor.extract(uvExe, options, tomlParser);
assertExtractionSuccess(extraction);

ArgumentCaptor<Executable> captor = ArgumentCaptor.forClass(Executable.class);
verify(executableRunner).executeSuccessfully(captor.capture());

List<String> arguments = captor.getValue().getCommandWithArguments();

// --all-extras and --all-groups should NOT be present (onlyGroups path)
assertTrue(!arguments.contains("--all-extras"));
assertTrue(!arguments.contains("--all-groups"));

// "dev" should be excluded — only "lint" should remain as --only-group
long onlyGroupCount = arguments.stream().filter(arg -> arg.equals("--only-group")).count();
assertEquals(1, onlyGroupCount, "Only 'lint' should remain after 'dev' is excluded");
assertTrue(arguments.contains("lint"));
assertTrue(!arguments.contains("dev"), "'dev' should be excluded from --only-group flags");
}

@Test
void extractAllOnlyGroupsExcludedResultsInNoOnlyGroupFlags() throws Exception {
UVBuildExtractor extractor = new UVBuildExtractor(executableRunner, tempDir, transformer);
// All only-groups are also excluded
UVDetectorOptions options = new UVDetectorOptions(
Arrays.asList("dev", "lint"), // excludedGroups
Arrays.asList("dev", "lint"), // onlyGroups
Collections.emptyList(),
Collections.emptyList()
);

Extraction extraction = extractor.extract(uvExe, options, tomlParser);
assertExtractionSuccess(extraction);

ArgumentCaptor<Executable> captor = ArgumentCaptor.forClass(Executable.class);
verify(executableRunner).executeSuccessfully(captor.capture());

List<String> arguments = captor.getValue().getCommandWithArguments();

// No --only-group flags since all were excluded
long onlyGroupCount = arguments.stream().filter(arg -> arg.equals("--only-group")).count();
assertEquals(0, onlyGroupCount, "No --only-group flags should remain when all are excluded");
}


/**
* Asserts that the extraction completed on the success path.
Expand Down
6 changes: 4 additions & 2 deletions documentation/src/main/markdown/packagemgrs/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ UV has two detectors:

### UV CLI detector

UV CLI will run if the uv executable is found along with a pyproject.toml file. It will run uv tree commands to find dependencies for the project.
UV CLI will run if the uv executable is found along with a pyproject.toml file. It will run uv tree commands to find dependencies for the project. By default, the UV CLI detector includes main dependencies, all dependency groups, and all optional extras. Use the `detect.uv.dependency.groups.excluded` property to exclude specific dependency groups from the scan.

To scan only specific dependency groups and skip regular dependencies and optional extras, use the `detect.uv.dependency.groups.only` property. When set, Detect will include only the named dependency groups defined in the project's pyproject.toml. You can list multiple groups (for example: `detect.uv.dependency.groups.only='dev,lint'`). This property applies only to groups under the `[dependency-groups]` section; extras defined under `[project.optional-dependencies]` are not included when this option is used. If both this property and `detect.uv.dependency.groups.excluded` are configured, the exclusion setting takes precedence for overlapping groups, and Detect will log a warning.
Comment thread
bd-spratikbharti marked this conversation as resolved.
Outdated

### UV Lock detector

Expand All @@ -144,6 +146,6 @@ UV Lock detector will parse uv.lock, requirements.txt, or both to find project d

### Dependency and Workspace Inclusions/Exclusions

[UV Properties](../properties/detectors/uv.md) supports exclusion of all the dependency groups specified. Since uv has a concept of workspaces, they can be included and excluded using the properties provided.
[UV Properties](../properties/detectors/uv.md) supports exclusion of dependency groups. By default, the UV detector includes main dependencies, all dependency groups, and all optional extras. Use the `detect.uv.dependency.groups.excluded` property to exclude specific dependency groups from the scan. Since uv has a concept of workspaces, they can be included and excluded using the properties provided.
The workspace member provided in the property should be identical to the key name under tool.uv.sources since dependencies are created under the same key name in the tree and uv.lock file.
For excluding dependency groups and workspaces, presence of uv.lock or uv executable is required.
Original file line number Diff line number Diff line change
Expand Up @@ -2052,6 +2052,17 @@ private DetectProperties() {
.setGroups(DetectGroup.UV, DetectGroup.GLOBAL, DetectGroup.SOURCE_SCAN)
.build();

public static final CaseSensitiveStringListProperty DETECT_UV_DEPENDENCY_GROUPS_ONLY =
CaseSensitiveStringListProperty.newBuilder("detect.uv.dependency.groups.only")
.setInfo("uv Only Dependency Groups", DetectPropertyFromVersion.VERSION_12_0_0)
.setHelp(
"A comma-separated list of uv dependency groups to exclusively scan.",
"When set, Detect will include only the named dependency groups defined in a project's pyproject.toml. Regular dependencies and optional extras are skipped. You can list multiple groups (for example: detect.uv.dependency.groups.only='dev,lint'). This property is only supported for projects that use pyproject.toml (dependency groups are not available in setup.py or setup.cfg). If both this property and detect.uv.dependency.groups.excluded are configured, the exclusion setting takes precedence for overlapping groups, and Detect will log a warning."
)
.setGroups(DetectGroup.UV, DetectGroup.GLOBAL, DetectGroup.SOURCE_SCAN)
.setCategory(DetectCategory.Advanced)
.build();

public static final CaseSensitiveStringListProperty DETECT_UV_EXCLUDED_WORKSPACE_MEMBERS =
CaseSensitiveStringListProperty.newBuilder("detect.uv.excluded.workspace.members")
.setInfo("uv Exclude Workspace Members", DetectPropertyFromVersion.VERSION_10_5_0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ public enum DetectPropertyFromVersion implements PropertyVersion {
VERSION_11_0_0("11.0.0"),
VERSION_11_2_0("11.2.0"),
VERSION_11_3_0("11.3.0"),
VERSION_11_4_0("11.4.0");
VERSION_11_4_0("11.4.0"),
VERSION_12_0_0("12.0.0");

private final String version;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,9 +386,10 @@ private boolean getFollowSymLinks() {

public UVDetectorOptions createUVDetectorOptions() {
List<String> excludedDependencyGroups = detectConfiguration.getValue(DetectProperties.DETECT_UV_DEPENDENCY_GROUPS_EXCLUDED);
List<String> onlyDependencyGroups = detectConfiguration.getValue(DetectProperties.DETECT_UV_DEPENDENCY_GROUPS_ONLY);
List<String> includedWorkSpaceMembers = detectConfiguration.getValue(DetectProperties.DETECT_UV_INCLUDED_WORKSPACE_MEMBERS);
List<String> excludeWorkSpaceMembers = detectConfiguration.getValue(DetectProperties.DETECT_UV_EXCLUDED_WORKSPACE_MEMBERS);

return new UVDetectorOptions(excludedDependencyGroups, includedWorkSpaceMembers, excludeWorkSpaceMembers);
return new UVDetectorOptions(excludedDependencyGroups, onlyDependencyGroups, includedWorkSpaceMembers, excludeWorkSpaceMembers);
}
}