Skip to content

Commit 5047f15

Browse files
authored
fix(python-uv): exclude project self-reference from dependency graph (guacsec#482)
When uv export includes the project itself as an editable install (-e .), the global_opt handler resolves it to the root pyproject.toml and adds it as a dependency, causing an off-by-one error in the total dependency count. Two fixes applied: - Add --no-emit-project flag to uv export command - Skip editable installs that resolve to the root project name Test fixture uses the reporter's pyproject.toml (with [build-system]) which triggers uv to emit -e . in the output. Implements TC-4097 Assisted-by: Claude Code
1 parent 1684e79 commit 5047f15

6 files changed

Lines changed: 622 additions & 1 deletion

File tree

src/providers/python_uv.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default class Python_uv extends Base_pyproject {
4444
return Buffer.from(process.env['TRUSTIFY_DA_UV_EXPORT'], 'base64').toString('ascii')
4545
}
4646
let uvBin = getCustomPath('uv', opts)
47-
return invokeCommand(uvBin, ['export', '--format', 'requirements.txt', '--frozen', '--no-hashes', '--no-dev'], { cwd: manifestDir }).toString()
47+
return invokeCommand(uvBin, ['export', '--format', 'requirements.txt', '--frozen', '--no-hashes', '--no-dev', '--no-emit-project'], { cwd: manifestDir }).toString()
4848
}
4949

5050
/**
@@ -81,6 +81,7 @@ export default class Python_uv extends Base_pyproject {
8181
let version = memberParsed.project?.version || memberParsed.tool?.poetry?.version
8282
if (name && version) {
8383
let key = this._canonicalize(name)
84+
if (key === canonProjectName) { continue }
8485
currentPkg = { name, version, parents: new Set() }
8586
packages.set(key, currentPkg)
8687
collectingVia = false

test/providers/python_pyproject.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,24 @@ suite('testing the python-pyproject data provider', () => {
121121
}).timeout(TIMEOUT)
122122
})
123123

124+
suite('uv projects - self-reference excluded (TC-4097)', () => {
125+
const fixtureDir = `${MANIFESTS}/uv_self_ref`
126+
127+
/** Verifies stack and component SBOM output excludes the project itself. */
128+
SBOM_CASES.forEach(({type, method, fixture}) => {
129+
test(`project self-reference excluded from ${type} analysis`, async () => {
130+
let expectedSbom = fs.readFileSync(path.join(fixtureDir, fixture)).toString().trim()
131+
expectedSbom = JSON.stringify(JSON.parse(expectedSbom))
132+
let result = await uvProvider[method](path.join(fixtureDir, 'pyproject.toml'))
133+
expect(result).to.deep.equal({
134+
ecosystem: 'pip',
135+
contentType: 'application/vnd.cyclonedx+json',
136+
content: expectedSbom
137+
})
138+
}).timeout(TIMEOUT)
139+
})
140+
})
141+
124142
suite('uv projects - uv_lock manifest', () => {
125143
const fixtureDir = `${MANIFESTS}/uv_lock`
126144

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.4",
4+
"version": 1,
5+
"metadata": {
6+
"timestamp": "2023-10-01T00:00:00.000Z",
7+
"component": {
8+
"name": "test-project",
9+
"version": "0.1.0",
10+
"purl": "pkg:pypi/test-project@0.1.0",
11+
"type": "application",
12+
"bom-ref": "pkg:pypi/test-project@0.1.0"
13+
}
14+
},
15+
"components": [
16+
{
17+
"name": "beautifulsoup4",
18+
"version": "4.12.2",
19+
"purl": "pkg:pypi/beautifulsoup4@4.12.2",
20+
"type": "library",
21+
"bom-ref": "pkg:pypi/beautifulsoup4@4.12.2"
22+
},
23+
{
24+
"name": "certifi",
25+
"version": "2023.7.22",
26+
"purl": "pkg:pypi/certifi@2023.7.22",
27+
"type": "library",
28+
"bom-ref": "pkg:pypi/certifi@2023.7.22"
29+
},
30+
{
31+
"name": "click",
32+
"version": "8.0.4",
33+
"purl": "pkg:pypi/click@8.0.4",
34+
"type": "library",
35+
"bom-ref": "pkg:pypi/click@8.0.4"
36+
},
37+
{
38+
"name": "flask",
39+
"version": "2.0.3",
40+
"purl": "pkg:pypi/flask@2.0.3",
41+
"type": "library",
42+
"bom-ref": "pkg:pypi/flask@2.0.3"
43+
},
44+
{
45+
"name": "requests",
46+
"version": "2.25.1",
47+
"purl": "pkg:pypi/requests@2.25.1",
48+
"type": "library",
49+
"bom-ref": "pkg:pypi/requests@2.25.1"
50+
},
51+
{
52+
"name": "uvicorn",
53+
"version": "0.17.0",
54+
"purl": "pkg:pypi/uvicorn@0.17.0",
55+
"type": "library",
56+
"bom-ref": "pkg:pypi/uvicorn@0.17.0"
57+
}
58+
],
59+
"dependencies": [
60+
{
61+
"ref": "pkg:pypi/test-project@0.1.0",
62+
"dependsOn": [
63+
"pkg:pypi/beautifulsoup4@4.12.2",
64+
"pkg:pypi/certifi@2023.7.22",
65+
"pkg:pypi/click@8.0.4",
66+
"pkg:pypi/flask@2.0.3",
67+
"pkg:pypi/requests@2.25.1",
68+
"pkg:pypi/uvicorn@0.17.0"
69+
]
70+
},
71+
{
72+
"ref": "pkg:pypi/beautifulsoup4@4.12.2",
73+
"dependsOn": []
74+
},
75+
{
76+
"ref": "pkg:pypi/certifi@2023.7.22",
77+
"dependsOn": []
78+
},
79+
{
80+
"ref": "pkg:pypi/click@8.0.4",
81+
"dependsOn": []
82+
},
83+
{
84+
"ref": "pkg:pypi/flask@2.0.3",
85+
"dependsOn": []
86+
},
87+
{
88+
"ref": "pkg:pypi/requests@2.25.1",
89+
"dependsOn": []
90+
},
91+
{
92+
"ref": "pkg:pypi/uvicorn@0.17.0",
93+
"dependsOn": []
94+
}
95+
]
96+
}
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.4",
4+
"version": 1,
5+
"metadata": {
6+
"timestamp": "2023-10-01T00:00:00.000Z",
7+
"component": {
8+
"name": "test-project",
9+
"version": "0.1.0",
10+
"purl": "pkg:pypi/test-project@0.1.0",
11+
"type": "application",
12+
"bom-ref": "pkg:pypi/test-project@0.1.0"
13+
}
14+
},
15+
"components": [
16+
{
17+
"name": "beautifulsoup4",
18+
"version": "4.12.2",
19+
"purl": "pkg:pypi/beautifulsoup4@4.12.2",
20+
"type": "library",
21+
"bom-ref": "pkg:pypi/beautifulsoup4@4.12.2"
22+
},
23+
{
24+
"name": "certifi",
25+
"version": "2023.7.22",
26+
"purl": "pkg:pypi/certifi@2023.7.22",
27+
"type": "library",
28+
"bom-ref": "pkg:pypi/certifi@2023.7.22"
29+
},
30+
{
31+
"name": "click",
32+
"version": "8.0.4",
33+
"purl": "pkg:pypi/click@8.0.4",
34+
"type": "library",
35+
"bom-ref": "pkg:pypi/click@8.0.4"
36+
},
37+
{
38+
"name": "flask",
39+
"version": "2.0.3",
40+
"purl": "pkg:pypi/flask@2.0.3",
41+
"type": "library",
42+
"bom-ref": "pkg:pypi/flask@2.0.3"
43+
},
44+
{
45+
"name": "requests",
46+
"version": "2.25.1",
47+
"purl": "pkg:pypi/requests@2.25.1",
48+
"type": "library",
49+
"bom-ref": "pkg:pypi/requests@2.25.1"
50+
},
51+
{
52+
"name": "uvicorn",
53+
"version": "0.17.0",
54+
"purl": "pkg:pypi/uvicorn@0.17.0",
55+
"type": "library",
56+
"bom-ref": "pkg:pypi/uvicorn@0.17.0"
57+
},
58+
{
59+
"name": "soupsieve",
60+
"version": "2.8.3",
61+
"purl": "pkg:pypi/soupsieve@2.8.3",
62+
"type": "library",
63+
"bom-ref": "pkg:pypi/soupsieve@2.8.3"
64+
},
65+
{
66+
"name": "colorama",
67+
"version": "0.4.6",
68+
"purl": "pkg:pypi/colorama@0.4.6",
69+
"type": "library",
70+
"bom-ref": "pkg:pypi/colorama@0.4.6"
71+
},
72+
{
73+
"name": "itsdangerous",
74+
"version": "2.2.0",
75+
"purl": "pkg:pypi/itsdangerous@2.2.0",
76+
"type": "library",
77+
"bom-ref": "pkg:pypi/itsdangerous@2.2.0"
78+
},
79+
{
80+
"name": "jinja2",
81+
"version": "3.1.6",
82+
"purl": "pkg:pypi/jinja2@3.1.6",
83+
"type": "library",
84+
"bom-ref": "pkg:pypi/jinja2@3.1.6"
85+
},
86+
{
87+
"name": "werkzeug",
88+
"version": "3.1.8",
89+
"purl": "pkg:pypi/werkzeug@3.1.8",
90+
"type": "library",
91+
"bom-ref": "pkg:pypi/werkzeug@3.1.8"
92+
},
93+
{
94+
"name": "markupsafe",
95+
"version": "3.0.3",
96+
"purl": "pkg:pypi/markupsafe@3.0.3",
97+
"type": "library",
98+
"bom-ref": "pkg:pypi/markupsafe@3.0.3"
99+
},
100+
{
101+
"name": "chardet",
102+
"version": "4.0.0",
103+
"purl": "pkg:pypi/chardet@4.0.0",
104+
"type": "library",
105+
"bom-ref": "pkg:pypi/chardet@4.0.0"
106+
},
107+
{
108+
"name": "idna",
109+
"version": "2.10",
110+
"purl": "pkg:pypi/idna@2.10",
111+
"type": "library",
112+
"bom-ref": "pkg:pypi/idna@2.10"
113+
},
114+
{
115+
"name": "urllib3",
116+
"version": "1.26.20",
117+
"purl": "pkg:pypi/urllib3@1.26.20",
118+
"type": "library",
119+
"bom-ref": "pkg:pypi/urllib3@1.26.20"
120+
},
121+
{
122+
"name": "asgiref",
123+
"version": "3.11.1",
124+
"purl": "pkg:pypi/asgiref@3.11.1",
125+
"type": "library",
126+
"bom-ref": "pkg:pypi/asgiref@3.11.1"
127+
},
128+
{
129+
"name": "h11",
130+
"version": "0.16.0",
131+
"purl": "pkg:pypi/h11@0.16.0",
132+
"type": "library",
133+
"bom-ref": "pkg:pypi/h11@0.16.0"
134+
}
135+
],
136+
"dependencies": [
137+
{
138+
"ref": "pkg:pypi/test-project@0.1.0",
139+
"dependsOn": [
140+
"pkg:pypi/beautifulsoup4@4.12.2",
141+
"pkg:pypi/certifi@2023.7.22",
142+
"pkg:pypi/click@8.0.4",
143+
"pkg:pypi/flask@2.0.3",
144+
"pkg:pypi/requests@2.25.1",
145+
"pkg:pypi/uvicorn@0.17.0"
146+
]
147+
},
148+
{
149+
"ref": "pkg:pypi/beautifulsoup4@4.12.2",
150+
"dependsOn": [
151+
"pkg:pypi/soupsieve@2.8.3"
152+
]
153+
},
154+
{
155+
"ref": "pkg:pypi/certifi@2023.7.22",
156+
"dependsOn": []
157+
},
158+
{
159+
"ref": "pkg:pypi/click@8.0.4",
160+
"dependsOn": [
161+
"pkg:pypi/colorama@0.4.6"
162+
]
163+
},
164+
{
165+
"ref": "pkg:pypi/flask@2.0.3",
166+
"dependsOn": [
167+
"pkg:pypi/click@8.0.4",
168+
"pkg:pypi/itsdangerous@2.2.0",
169+
"pkg:pypi/jinja2@3.1.6",
170+
"pkg:pypi/werkzeug@3.1.8"
171+
]
172+
},
173+
{
174+
"ref": "pkg:pypi/requests@2.25.1",
175+
"dependsOn": [
176+
"pkg:pypi/certifi@2023.7.22",
177+
"pkg:pypi/chardet@4.0.0",
178+
"pkg:pypi/idna@2.10",
179+
"pkg:pypi/urllib3@1.26.20"
180+
]
181+
},
182+
{
183+
"ref": "pkg:pypi/uvicorn@0.17.0",
184+
"dependsOn": [
185+
"pkg:pypi/asgiref@3.11.1",
186+
"pkg:pypi/click@8.0.4",
187+
"pkg:pypi/h11@0.16.0"
188+
]
189+
},
190+
{
191+
"ref": "pkg:pypi/soupsieve@2.8.3",
192+
"dependsOn": []
193+
},
194+
{
195+
"ref": "pkg:pypi/colorama@0.4.6",
196+
"dependsOn": []
197+
},
198+
{
199+
"ref": "pkg:pypi/itsdangerous@2.2.0",
200+
"dependsOn": []
201+
},
202+
{
203+
"ref": "pkg:pypi/jinja2@3.1.6",
204+
"dependsOn": [
205+
"pkg:pypi/markupsafe@3.0.3"
206+
]
207+
},
208+
{
209+
"ref": "pkg:pypi/werkzeug@3.1.8",
210+
"dependsOn": [
211+
"pkg:pypi/markupsafe@3.0.3"
212+
]
213+
},
214+
{
215+
"ref": "pkg:pypi/markupsafe@3.0.3",
216+
"dependsOn": []
217+
},
218+
{
219+
"ref": "pkg:pypi/chardet@4.0.0",
220+
"dependsOn": []
221+
},
222+
{
223+
"ref": "pkg:pypi/idna@2.10",
224+
"dependsOn": []
225+
},
226+
{
227+
"ref": "pkg:pypi/urllib3@1.26.20",
228+
"dependsOn": []
229+
},
230+
{
231+
"ref": "pkg:pypi/asgiref@3.11.1",
232+
"dependsOn": []
233+
},
234+
{
235+
"ref": "pkg:pypi/h11@0.16.0",
236+
"dependsOn": []
237+
}
238+
]
239+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[project]
2+
name = "test-project"
3+
version = "0.1.0"
4+
description = "Test uv PEP 621 dependencies with no ignores"
5+
dependencies = [
6+
"flask==2.0.3",
7+
"requests==2.25.1",
8+
"uvicorn==0.17.0",
9+
"click==8.0.4",
10+
"beautifulsoup4==4.12.2",
11+
"certifi==2023.7.22",
12+
]
13+
14+
[build-system]
15+
requires = ["hatchling"]
16+
build-backend = "hatchling.build"

0 commit comments

Comments
 (0)