feat(python): add uv package manager support (TC-3855)#428
Conversation
…ackage manager support TC-3855 Add automatic detection and support for Python projects managed by the uv package manager. When uv.lock is present alongside pyproject.toml, the new PythonProviderFactory selects PythonUvProvider (using uv pip list/show for dependency resolution) instead of the pip-based PythonPyprojectProvider. - Add PythonProviderFactory with Map-based lock file detection pattern - Add PythonUvProvider with CycloneDX SBOM generation (stack + component) - Add PyprojectTomlUtils shared utility to deduplicate TOML parsing logic - Refactor PythonPyprojectProvider to use PyprojectTomlUtils - Fix PEP 508 extras stripping in getDependencyName (e.g. requests[security]) - Update Ecosystem.resolveProvider() to delegate to PythonProviderFactory - Add test fixtures and 15 unit tests for the uv provider - Document uv support, TRUSTIFY_DA_UV_PATH, and CLI examples in README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Assisted-by: Claude Code
TC-3855 The PythonUvProvider unit tests require the uv binary to be available, since the constructor eagerly validates it via Operations.getExecutable(). This follows the same pattern as other providers (cargo, go, gradle). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Assisted-by: Claude Code
Signed-off-by: Adva Oren <aoren@redhat.com>
|
/review |
1 similar comment
|
/review |
ruromero
left a comment
There was a problem hiding this comment.
Thanks for the PR this one is not trivial. I made some comments.
Replace the two-command approach (uv pip list + uv pip show) with a single uv export --format requirements.txt --frozen --no-hashes --no-dev --no-emit-project, aligning with the JS client implementation. Also extract duplicated canonicalize() method from PythonUvProvider and PythonPyprojectProvider into PyprojectTomlUtils, and convert UvPackage and UvDependencyData from static final classes to records. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@ruromero Thanks for the review |
ruromero
left a comment
There was a problem hiding this comment.
We need to look for the lock file path. The other changes are covered and look good.
… members The factory and validateLockFile only checked the manifest directory for uv.lock, which fails for uv workspace members where the lock file lives at the workspace root. Add parent directory walk-up matching the JS client's _findLockFileDir pattern, with TRUSTIFY_DA_WORKSPACE_DIR override, git root boundary, and uv workspace root detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@sourcery-ai review |
Reviewer's GuideAdds uv-based Python dependency resolution alongside existing pip/pyproject support, refactors shared pyproject.toml handling into utilities, fixes PEP 508 extras parsing for dependency names, wires uv into provider selection/CI/docs, and adds comprehensive tests for the new behavior. Sequence diagram for SBOM generation using uv for a pyproject.toml manifestsequenceDiagram
actor User
participant CLI as JavaClientCLI
participant Ecosystem as Ecosystem
participant PythonProviderFactory as PythonProviderFactory
participant PythonUvProvider as PythonUvProvider
participant Operations as Operations
participant SbomFactory as SbomFactory
participant Sbom as Sbom
User->>CLI: sbom /path/to/uv-project/pyproject.toml
CLI->>Ecosystem: resolveProvider(manifestPath)
Ecosystem->>PythonProviderFactory: create(manifestPath)
PythonProviderFactory->>PythonProviderFactory: check manifestDir for uv.lock
alt uv.lock found
PythonProviderFactory-->>Ecosystem: new PythonUvProvider(manifestPath)
else no uv.lock
PythonProviderFactory->>PythonProviderFactory: findLockFileDirInParents(startDir)
alt parent with uv.lock found
PythonProviderFactory-->>Ecosystem: new PythonUvProvider(manifestPath)
else no lock file
PythonProviderFactory-->>Ecosystem: new PythonPyprojectProvider(manifestPath)
end
end
CLI->>PythonUvProvider: provideStack()
PythonUvProvider->>PythonUvProvider: validateLockFile(manifestDir)
PythonUvProvider->>PythonUvProvider: rejectPoetryDependencies()
PythonUvProvider->>PythonUvProvider: collectIgnoredDeps()
PythonUvProvider->>Operations: getExecutable("uv", "--version")
Operations-->>PythonUvProvider: uvExecutable path
PythonUvProvider->>PythonUvProvider: getUvExportOutput(manifestDir)
PythonUvProvider->>Operations: runProcessGetFullOutput(cwd, cmd, env)
Operations-->>PythonUvProvider: ProcessExecOutput(uv export text)
PythonUvProvider->>PythonUvProvider: parseUvExport(exportOutput)
PythonUvProvider->>PythonUvProvider: getRootComponentName()
PythonUvProvider->>PythonUvProvider: getRootComponentVersion()
PythonUvProvider->>PythonUvProvider: readLicenseFromManifest()
PythonUvProvider->>SbomFactory: newInstance(BelongingCondition.PURL, "sensitive")
SbomFactory-->>PythonUvProvider: Sbom
PythonUvProvider->>Sbom: addRoot(purl, license)
loop for each direct dependency
PythonUvProvider->>PythonUvProvider: addDependencyTree(rootPurl, pkg, graph, sbom, visited)
PythonUvProvider->>Sbom: addDependency(source, target, null)
end
PythonUvProvider->>PythonUvProvider: handleIgnoredDependencies(manifestContent, sbom)
PythonUvProvider->>Sbom: getAsJsonString()
PythonUvProvider-->>CLI: Content(sbomJson, CYCLONEDX_MEDIA_TYPE)
CLI-->>User: SBOM JSON output
Class diagram for new Python uv support and shared pyproject utilitiesclassDiagram
direction LR
class PythonProvider {
<<abstract>>
Path manifestPath
+PythonProvider(Path manifestPath)
+Content provideStack()
+Content provideComponent()
+void validateLockFile(Path lockFileDir)
+PackageURL toPurl(String name, String version)
+void handleIgnoredDependencies(String manifestContent, Sbom sbom)
+String getRootComponentName()
+String getRootComponentVersion()
+String readLicenseFromManifest()
+Set~PackageURL~ getIgnoredDependencies(String manifestContent)
}
class PythonPyprojectProvider {
-TomlParseResult cachedToml
+PythonPyprojectProvider(Path manifestPath)
+Content provideStack()
+Content provideComponent()
+String readLicenseFromManifest()
+Set~PackageURL~ getIgnoredDependencies(String manifestContent)
-void rejectPoetryDependencies()
-void collectIgnoredDeps()
-TomlParseResult getToml()
}
class PythonUvProvider {
<<final>>
+static String LOCK_FILE
+static String PROP_TRUSTIFY_DA_UV_EXPORT
-String uvExecutable
-Set~String~ collectedIgnoredDeps
-TomlParseResult cachedToml
+PythonUvProvider(Path manifest)
+void validateLockFile(Path lockFileDir)
+Content provideStack()
+Content provideComponent()
+String readLicenseFromManifest()
+Set~PackageURL~ getIgnoredDependencies(String manifestContent)
-void addDependencyTree(PackageURL source, UvPackage pkg, Map~String, UvPackage~ graph, Sbom sbom, Set~String~ visited)
-UvDependencyData parseUvExport(String exportOutput)
-static void recordViaParent(String parentName, String childKey, String projectName, List~String~ directDeps, List~String[]~ parentChildPairs)
-String getUvExportOutput(Path manifestDir)
-TomlParseResult getToml()
-void rejectPoetryDependencies()
-void collectIgnoredDeps()
}
class PythonProviderFactory {
<<final>>
-static Map~String, Function~Path, PythonProvider~~ PYTHON_PROVIDERS
+static PythonProvider create(Path manifestPath)
+static Path findLockFileDirInParents(Path startDir)
}
class PyprojectTomlUtils {
<<final>>
+static TomlParseResult parseToml(Path manifest)
+static Set~String~ collectIgnoredDeps(Path manifest, TomlParseResult toml)
+static String getProjectName(TomlParseResult toml)
+static String getProjectVersion(TomlParseResult toml)
+static String getLicense(TomlParseResult toml)
+static boolean hasPoetryDependencies(TomlParseResult toml)
+static String canonicalize(String name)
+static boolean isUvWorkspaceRoot(Path dir)
}
class PythonControllerBase {
+static String getDependencyName(String dep)
+String getDependencyNameShow(String pipShowOutput)
}
class Ecosystem {
-static Provider resolveProvider(Path manifestPath)
}
class Operations {
+static String getExecutable(String binaryName, String versionArg)
+static Operations.ProcessExecOutput runProcessGetFullOutput(Path cwd, String[] cmd, Map~String,String~ env)
+static java.util.Optional~String~ getGitRootDir(String startDir)
}
class Environment {
+static String get(String key)
}
class SbomFactory {
+static Sbom newInstance()
+static Sbom newInstance(Sbom.BelongingCondition condition, String sensitivity)
}
class Sbom {
+static enum BelongingCondition
+void addRoot(PackageURL purl, String license)
+PackageURL getRoot()
+void addDependency(PackageURL source, PackageURL target, String scope)
+StringBuilder getAsJsonString()
}
class LicenseUtils {
+static String readLicenseFile(Path manifestPath)
}
class Provider {
<<interface>>
+Content provideStack()
+Content provideComponent()
+void validateLockFile(Path lockFileDir)
}
class Content {
+Content(byte[] data, String mediaType)
}
class UvPackage {
<<record>>
+String name
+String version
+List~String~ children
}
class UvDependencyData {
<<record>>
+List~String~ directDeps
+Map~String, UvPackage~ graph
}
class IgnorePatternDetector {
+static boolean containsIgnorePattern(String line)
}
PythonProvider <|-- PythonPyprojectProvider
PythonProvider <|-- PythonUvProvider
Provider <|.. PythonProvider
Ecosystem --> PythonProviderFactory : uses
PythonProviderFactory --> PythonUvProvider : returns
PythonProviderFactory --> PythonPyprojectProvider : returns
PythonUvProvider --> PyprojectTomlUtils : uses
PythonUvProvider --> Operations : uses
PythonUvProvider --> Environment : uses
PythonUvProvider --> SbomFactory : uses
PythonUvProvider --> Sbom : uses
PythonUvProvider --> LicenseUtils : uses
PythonUvProvider --> PythonControllerBase : uses
PythonPyprojectProvider --> PyprojectTomlUtils : uses
PythonProviderFactory --> Operations : uses
PythonProviderFactory --> Environment : uses
PythonProviderFactory --> PyprojectTomlUtils : uses
PyprojectTomlUtils --> IgnorePatternDetector : uses
SbomFactory --> Sbom : creates
Content --> Api : mediaType
class Api {
+static String CYCLONEDX_MEDIA_TYPE
}
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/main/java/io/github/guacsec/trustifyda/providers/PythonUvProvider.java" line_range="233-239" />
<code_context>
+ return new UvDependencyData(directDeps, packages);
+ }
+
+ private static void recordViaParent(
+ String parentName,
+ String childKey,
+ String projectName,
+ List<String> directDeps,
+ List<String[]> parentChildPairs) {
+ String parentKey = PyprojectTomlUtils.canonicalize(parentName);
+ if (parentKey.equals(projectName)) {
+ if (!directDeps.contains(childKey)) {
</code_context>
<issue_to_address>
**issue (bug_risk):** Parent names from `# via` lines likely include version or extra annotations and should be normalized before canonicalization.
In `recordViaParent`, `parentName` comes directly from the `# via` comment and can include versions/extras (e.g., `foo (>=1.0)`, `foo[extra]`). Canonicalizing that raw value produces keys that won’t match the bare names used for package lines, so some parent→child edges may be lost. Strip versions/extras from `parentName` first (as you do for package lines), then canonicalize the cleaned name before using it as a key.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
- Skip self-referencing editable installs (root project as its own dep) - Require both name and version for editable installs (skip if missing) - Fall back to tool.poetry.name/version for editable install metadata - Validate bare package names in # via comments (skip version specifiers/extras) - Throw on unpinned versions in uv export output - Prevent workspace root walk-up from escaping into parent workspace Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ruromero
left a comment
There was a problem hiding this comment.
Thanks Adva. Looks good
Verification Report for TC-3855 (commit 008eca0)
Overall: WARNIssues requiring attention:
This comment was AI-generated by sdlc-workflow/verify-pr v0.5.11. |
Summary
PythonUvProviderandPythonProviderFactoryfor automatic uv package manager detection viauv.lockpresenceuv pip list --format=json+uv pip showfor CycloneDX SBOM generation (stack + component)PyprojectTomlUtils, refactor bothPythonPyprojectProviderandPythonUvProviderto use itgetDependencyName()(e.g.requests[security]→requests) affecting all Python providersTRUSTIFY_DA_UV_PATH, and CLI examples in READMETest plan
mvn test -Dtest="Python_Uv_Provider_Test" -Dskip.junit_platform=true— 15 uv provider tests passmvn test -Dtest="Python_Pyproject_Provider_Test" -Dskip.junit_platform=true— 22 existing pyproject tests pass (regression)mvn test -Dtest="PythonControllerRealEnvTest#get_Dependency_Name*" -Dskip.junit_platform=true— extras fix tests passmvn spotless:check— formatting passesjava -jar trustify-da-java-client-cli.jar sbom /path/to/uv-project/pyproject.tomlproduces correct CycloneDX SBOMAcceptance Criteria
PythonProviderFactory.create()returnsPythonUvProviderwhenuv.lockexistsPythonProviderFactory.create()falls back toPythonPyprojectProviderwhen nouv.lockPythonUvProviderdiscovers uv binary viaOperations.getExecutable("uv", "--version")TRUSTIFY_DA_UV_PATHvalidateLockFile()throwsIllegalStateExceptionwhenuv.lockmissingprovideStack()andprovideComponent()generate valid CycloneDX SBOMsrequirements.txtandpyproject.tomlflows unchanged#trustify-da-ignore/#exhortignorepatterns work in uv context🤖 Generated with Claude Code
Summary by Sourcery
Add uv-based Python provider selection and shared pyproject.toml utilities while preserving existing pip/pyproject behavior.
New Features:
Bug Fixes:
Enhancements:
CI:
Documentation:
Tests: