Skip to content

Commit 87aa770

Browse files
committed
fix(python-uv): exclude project self-reference from dependency graph
When uv export includes the project itself as a requirement (e.g. test-project==1.0.0), it was being counted as a dependency, causing an off-by-one error in the total dependency count. Three fixes applied: - Add --no-emit-project flag to uv export command - Defensive filter in requirement parser to skip packages matching the project name - Defensive filter in editable install handler (-e .) for the same Implements TC-4097 Assisted-by: Claude Code
1 parent 6ca858a commit 87aa770

3 files changed

Lines changed: 70 additions & 1 deletion

File tree

src/providers/python_uv.js

Lines changed: 3 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
@@ -109,6 +110,7 @@ export default class Python_uv extends Base_pyproject {
109110
}
110111

111112
let key = this._canonicalize(name)
113+
if (key === canonProjectName) { continue }
112114
currentPkg = { name, version, parents: new Set() }
113115
packages.set(key, currentPkg)
114116
collectingVia = false

test/providers/python_pyproject.test.js

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

124+
suite('uv projects - self-reference filtering (TC-4097)', () => {
125+
const fixtureDir = `${MANIFESTS}/uv_lock`
126+
127+
function setUvExportFromFixture() {
128+
let output = fs.readFileSync(path.join(fixtureDir, 'uv_export_with_self_ref.txt')).toString()
129+
process.env['TRUSTIFY_DA_UV_EXPORT'] = Buffer.from(output).toString('base64')
130+
}
131+
132+
teardown(() => { delete process.env['TRUSTIFY_DA_UV_EXPORT'] })
133+
134+
/** Verifies that the project itself is excluded from the component SBOM when uv export includes it as a requirement. */
135+
test('project self-reference excluded from component analysis', async () => {
136+
setUvExportFromFixture()
137+
let result = await uvProvider.provideComponent(path.join(fixtureDir, 'pyproject.toml'))
138+
let sbom = JSON.parse(result.content)
139+
let names = sbom.components.map(c => c.name)
140+
expect(names).to.not.include('test-project')
141+
expect(names).to.include('flask')
142+
expect(names).to.include('requests')
143+
}).timeout(TIMEOUT)
144+
145+
/** Verifies that the project itself is excluded from the stack SBOM components and not listed as its own dependency. */
146+
test('project self-reference excluded from stack analysis', async () => {
147+
setUvExportFromFixture()
148+
let result = await uvProvider.provideStack(path.join(fixtureDir, 'pyproject.toml'))
149+
let sbom = JSON.parse(result.content)
150+
let names = sbom.components.map(c => c.name)
151+
expect(names).to.not.include('test-project')
152+
expect(names).to.include('flask')
153+
expect(names).to.include('requests')
154+
let rootDep = sbom.dependencies.find(d => d.ref === 'pkg:pypi/test-project@1.0.0')
155+
expect(rootDep).to.exist
156+
expect(rootDep.dependsOn).to.not.include('pkg:pypi/test-project@1.0.0')
157+
}).timeout(TIMEOUT)
158+
})
159+
124160
suite('uv projects - uv_lock manifest', () => {
125161
const fixtureDir = `${MANIFESTS}/uv_lock`
126162

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Test fixture for TC-4097: simulates uv export output that includes the project
2+
# itself as a requirement (line "test-project==1.0.0"). This cannot be reproduced
3+
# by running uv export directly because --no-emit-project prevents it. The file is
4+
# injected via the TRUSTIFY_DA_UV_EXPORT env var to test the defensive filter that
5+
# skips packages matching the project name.
6+
certifi==2023.7.22
7+
# via requests
8+
chardet==4.0.0
9+
# via requests
10+
click==8.3.1
11+
# via flask
12+
colorama==0.4.6 ; sys_platform == 'win32'
13+
# via click
14+
flask==2.0.3
15+
# via test-project
16+
idna==2.10
17+
# via requests
18+
itsdangerous==2.0.1
19+
# via flask
20+
jinja2==3.0.3
21+
# via flask
22+
markupsafe==2.0.1
23+
# via jinja2
24+
requests==2.25.1
25+
# via test-project
26+
test-project==1.0.0
27+
# via test-project
28+
urllib3==1.26.16
29+
# via requests
30+
werkzeug==2.0.3
31+
# via flask

0 commit comments

Comments
 (0)