Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions lib/modules/manager/pep621/dep-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export const knownDepTypes = [
depType: 'tool.uv.sources',
description: 'Listed under `[tool.uv.sources]`',
},
{
depType: 'uv.lock',
description:
'Transitive dependency resolved in `uv.lock`. Only surfaced for `osvVulnerabilityAlerts`; never updated routinely.',
},
] as const satisfies readonly DepTypeMetadata[];

export const supportsDynamicDepTypesNote =
Expand Down
85 changes: 85 additions & 0 deletions lib/modules/manager/pep621/extract.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,91 @@ describe('modules/manager/pep621/extract', () => {
});
});

it('should surface transitive deps from uv.lock as disabled deps', async () => {
fs.readLocalFile.mockResolvedValue(
codeBlock`
version = 1
requires-python = ">=3.11"

[[package]]
name = "attrs"
version = "24.2.0"
source = { registry = "https://pypi.org/simple" }

[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }

[[package]]
name = "internal-lib"
version = "1.2.3"
source = { registry = "https://example.com/simple" }

[[package]]
name = "local-pkg"
version = "0.1.0"
source = { editable = "../local-pkg" }

[[package]]
name = "pep621-uv"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "attrs" },
]
`,
);

fs.findLocalSiblingOrParent.mockResolvedValueOnce(null);
fs.findLocalSiblingOrParent.mockResolvedValueOnce('uv.lock');
fs.findLocalSiblingOrParent.mockResolvedValueOnce('uv.lock');

const res = await extractPackageFile(
codeBlock`
[project]
name = "pep621-uv"
version = "0.1.0"
dependencies = ["attrs>=24.1.0"]
requires-python = ">=3.11"
`,
'pyproject.toml',
);

// attrs (direct) keeps its lockedVersion; idna and internal-lib are
// surfaced as disabled transitive deps; the editable local-pkg and the
// virtual workspace root are skipped.
expect(res).toMatchObject({
deps: [
{ packageName: 'python', depType: 'requires-python' },
{
packageName: 'attrs',
depType: 'project.dependencies',
currentValue: '>=24.1.0',
lockedVersion: '24.2.0',
},
{
packageName: 'idna',
depName: 'idna',
depType: 'uv.lock',
datasource: 'pypi',
lockedVersion: '3.10',
enabled: false,
},
{
packageName: 'internal-lib',
depType: 'uv.lock',
lockedVersion: '1.2.3',
enabled: false,
registryUrls: ['https://example.com/simple'],
},
],
lockFiles: ['uv.lock'],
});
// default-PyPI transitive deps don't carry registryUrls
expect(res?.deps[2].registryUrls).toBeUndefined();
});

it('should resolve dependencies without locked versions on invalid uv.lock', async () => {
fs.readLocalFile.mockResolvedValue(codeBlock`invalid_toml`);

Expand Down
43 changes: 42 additions & 1 deletion lib/modules/manager/pep621/processors/uv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ import { BasePyProjectProcessor } from './abstract.ts';

const uvUpdateCMD = 'uv lock';

// The public PyPI index, as recorded in `uv.lock` package sources. Transitive
// deps resolved from it use the datasource default, so no `registryUrls` needed.
const defaultPypiRegistries = new Set([
'https://pypi.org/simple',
'https://pypi.org/simple/',
]);

function isDefaultPypiRegistry(registryUrl: string): boolean {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only used once, inline this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return defaultPypiRegistries.has(registryUrl);
}

export class UvProcessor extends BasePyProjectProcessor {
override lockfileName = 'uv.lock';

Expand Down Expand Up @@ -150,8 +161,38 @@ export class UvProcessor extends BasePyProjectProcessor {
for (const dep of deps) {
const packageName = dep.packageName;
if (packageName && packageName in lockFileMapping) {
dep.lockedVersion = lockFileMapping[packageName];
dep.lockedVersion = lockFileMapping[packageName].version;
}
}
const knownPackageNames = new Set(
deps.map((dep) => dep.packageName).filter(isString),
);

// Surface transitive (lockfile-only) packages as disabled
// dependencies. They produce no routine updates, but
// `osvVulnerabilityAlerts` can still match them and re-enable a
// targeted `uv lock --upgrade-package` when a fixed version exists.
for (const [packageName, locked] of Object.entries(lockFileMapping)) {
if (knownPackageNames.has(packageName)) {
continue;
}
// Only registry-sourced packages can be remediated by name. Skip
// the workspace root and any virtual/editable/path/git packages.
if (!locked.registryUrl) {
continue;
}
const indirectDep: PackageDependency = {
packageName,
depName: packageName,
depType: depTypes.uvIndirectDependencies,
datasource: PypiDatasource.id,
lockedVersion: locked.version,
enabled: false,
};
if (!isDefaultPypiRegistry(locked.registryUrl)) {
indirectDep.registryUrls = [locked.registryUrl];
}
deps.push(indirectDep);
}
}
}
Expand Down
14 changes: 13 additions & 1 deletion lib/modules/manager/pep621/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,17 +219,29 @@ export const PdmLockfile = Toml.pipe(
)
.transform((lock) => ({ lock }));

export interface UvLockedPackage {
version: string;
// The index a package was resolved from, if any. Absent for the workspace
// root and for virtual/editable/path/git packages, which cannot be
// remediated by `uv lock --upgrade-package`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use jsdoc comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

registryUrl?: string;
}

export const UvLockfile = Toml.pipe(
z.object({
package: LooseArray(
z.object({
name: z.string(),
version: z.string(),
source: z.object({ registry: z.string() }).partial().optional(),
}),
),
}),
).transform(({ package: pkg }) =>
Object.fromEntries(
pkg.map(({ name, version }): [string, string] => [name, version]),
pkg.map(({ name, version, source }): [string, UvLockedPackage] => [
name,
{ version, registryUrl: source?.registry },
]),
),
);
1 change: 1 addition & 0 deletions lib/modules/manager/pep621/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const depTypes = {
pdmDevDependencies: 'tool.pdm.dev-dependencies',
uvDevDependencies: 'tool.uv.dev-dependencies',
uvSources: 'tool.uv.sources',
uvIndirectDependencies: 'uv.lock',
buildSystemRequires: 'build-system.requires',
};

Expand Down
3 changes: 3 additions & 0 deletions lib/workers/repository/process/vulnerabilities.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,9 @@ describe('workers/repository/process/vulnerabilities', () => {
matchPackageNames: ['stdlib'],
matchCurrentVersion: '1.7.5',
allowedVersions: '>= 1.7.6',
// remediation must override a disabled dependency, e.g. a transitive
// dep surfaced from a lockfile

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsdoc comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enabled: true,
isVulnerabilityAlert: true,
},
]);
Expand Down
4 changes: 4 additions & 0 deletions lib/workers/repository/process/vulnerabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,10 @@ export class Vulnerabilities {
matchCurrentVersion: depVersion,
versioning,
allowedVersions: fixedVersion,
// Remediate even when updates are otherwise disabled for the dependency,
// e.g. transitive deps surfaced from a lockfile. Consistent with how
// vulnerability alerts already override schedule and minimumReleaseAge.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsdoc comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enabled: true,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enabled: true is a global behavior change, not scoped to uv.

needs maintainers input on this @viceice @secustor

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I can see how this can be regarded as too broad.

One way to make this cleaner could be to add a dedicated lockfile-only skipReason and have the OSV processor clear it when we need to push a fix bump.

isVulnerabilityAlert: true,
vulnerabilitySeverity: severityDetails.severityLevel,
prBodyNotes: this.generatePrBodyNotes(vulnerability, affected),
Expand Down
Loading