Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(manager/uv): support uv.toml configuration #31189

Closed
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion lib/modules/manager/pep621/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export async function extractPackageFile(
// process specific tool sets
let processedDeps = deps;
for (const processor of processors) {
processedDeps = processor.process(def, processedDeps);
processedDeps = await processor.process(def, processedDeps);
processedDeps = await processor.extractLockedVersions(
def,
processedDeps,
Expand Down
6 changes: 3 additions & 3 deletions lib/modules/manager/pep621/processors/hatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ export class HatchProcessor implements PyProjectProcessor {
process(
pyproject: PyProject,
deps: PackageDependency[],
): PackageDependency[] {
): Promise<PackageDependency[]> {
const hatch_envs = pyproject.tool?.hatch?.envs;
if (is.nullOrUndefined(hatch_envs)) {
return deps;
return Promise.resolve(deps);
}

for (const [envName, env] of Object.entries(hatch_envs)) {
Expand All @@ -29,7 +29,7 @@ export class HatchProcessor implements PyProjectProcessor {
deps.push(...extraDeps);
}

return deps;
return Promise.resolve(deps);
}

extractLockedVersions(
Expand Down
11 changes: 7 additions & 4 deletions lib/modules/manager/pep621/processors/pdm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ import type { PyProjectProcessor } from './types';
const pdmUpdateCMD = 'pdm update --no-sync --update-eager';

export class PdmProcessor implements PyProjectProcessor {
process(project: PyProject, deps: PackageDependency[]): PackageDependency[] {
process(
project: PyProject,
deps: PackageDependency[],
): Promise<PackageDependency[]> {
const pdm = project.tool?.pdm;
if (is.nullOrUndefined(pdm)) {
return deps;
return Promise.resolve(deps);
}

deps.push(
Expand All @@ -35,7 +38,7 @@ export class PdmProcessor implements PyProjectProcessor {

const pdmSource = pdm.source;
if (is.nullOrUndefined(pdmSource)) {
return deps;
return Promise.resolve(deps);
}

// add pypi default url, if there is no source declared with the name `pypi`. https://daobook.github.io/pdm/pyproject/tool-pdm/#specify-other-sources-for-finding-packages
Expand All @@ -51,7 +54,7 @@ export class PdmProcessor implements PyProjectProcessor {
dep.registryUrls = [...registryUrls];
}

return deps;
return Promise.resolve(deps);
}

async extractLockedVersions(
Expand Down
5 changes: 4 additions & 1 deletion lib/modules/manager/pep621/processors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export interface PyProjectProcessor {
* @param project PyProject object
* @param deps List of already extracted/processed dependencies
*/
process(project: PyProject, deps: PackageDependency[]): PackageDependency[];
process(
project: PyProject,
deps: PackageDependency[],
): Promise<PackageDependency[]>;

extractLockedVersions(
project: PyProject,
Expand Down
124 changes: 119 additions & 5 deletions lib/modules/manager/pep621/processors/uv.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { codeBlock } from 'common-tags';
import { join } from 'upath';
import { mockExecAll } from '../../../../../test/exec-util';
import { fs, mockedFunction } from '../../../../../test/util';
Expand All @@ -24,32 +25,89 @@ const processor = new UvProcessor();

describe('modules/manager/pep621/processors/uv', () => {
describe('process()', () => {
it('returns initial dependencies if there is no tool.uv section', () => {
it('reads configuration solely from uv.toml if valid', async () => {
const pyproject = {
tool: { uv: { 'index-url': 'https://example.com' } },
};
const dependencies = [{ packageName: 'dep1' }];

fs.readLocalFile.mockResolvedValueOnce(
codeBlock`
dev-dependencies = ["dep2==1.2.3"]
`,
);

const result = await processor.process(pyproject, dependencies);

expect(result).toEqual([
{
packageName: 'dep1',
registryUrls: ['https://pypi.org/pypi/'],
},
{
currentValue: '==1.2.3',
currentVersion: '1.2.3',
datasource: 'pypi',
depName: 'dep2',
depType: 'tool.uv.dev-dependencies',
packageName: 'dep2',
registryUrls: ['https://pypi.org/pypi/'],
},
]);
});

it('reads configuration from pyproject.toml on invalid uv.toml', async () => {
const pyproject = {
tool: { uv: { 'index-url': 'https://example.com' } },
};
const dependencies = [{ packageName: 'dep1' }];

fs.readLocalFile.mockResolvedValueOnce(
codeBlock`
dev-dependencies = invalid_toml
`,
);

const result = await processor.process(pyproject, dependencies);

expect(result).toEqual([
{
packageName: 'dep1',
registryUrls: ['https://example.com'],
},
]);
});

it('returns initial dependencies if there is no tool.uv section', async () => {
const pyproject = { tool: {} };
const dependencies = [{ packageName: 'dep1' }];

const result = processor.process(pyproject, dependencies);
const result = await processor.process(pyproject, dependencies);

expect(result).toEqual(dependencies);
});

it('includes uv dev dependencies if there is a tool.uv section', () => {
it('includes uv dev dependencies if there is a tool.uv section', async () => {
const pyproject = {
tool: { uv: { 'dev-dependencies': ['dep2==1.2.3', 'dep3==2.3.4'] } },
};
const dependencies = [{ packageName: 'dep1' }];

const result = processor.process(pyproject, dependencies);
const result = await processor.process(pyproject, dependencies);

expect(result).toEqual([
{ packageName: 'dep1' },
{
packageName: 'dep1',
registryUrls: ['https://pypi.org/pypi/'],
},
{
currentValue: '==1.2.3',
currentVersion: '1.2.3',
datasource: 'pypi',
depName: 'dep2',
depType: 'tool.uv.dev-dependencies',
packageName: 'dep2',
registryUrls: ['https://pypi.org/pypi/'],
},
{
currentValue: '==2.3.4',
Expand All @@ -58,6 +116,62 @@ describe('modules/manager/pep621/processors/uv', () => {
depName: 'dep3',
depType: 'tool.uv.dev-dependencies',
packageName: 'dep3',
registryUrls: ['https://pypi.org/pypi/'],
},
]);
});

it('uses default PyPI and extra URLs when setting extra-index-url', async () => {
const pyproject = {
tool: {
uv: {
'extra-index-url': [
'https://foo.example.com',
'https://bar.example.com',
],
},
},
};
const dependencies = [{ packageName: 'dep1' }];

const result = await processor.process(pyproject, dependencies);

expect(result).toEqual([
{
packageName: 'dep1',
registryUrls: [
'https://foo.example.com',
'https://bar.example.com',
'https://pypi.org/pypi/',
],
},
]);
});

it('uses index and extra URLs when setting index-url and extra-index-url', async () => {
const pyproject = {
tool: {
uv: {
'index-url': 'https://foobar.example.com',
'extra-index-url': [
'https://foo.example.com',
'https://bar.example.com',
],
},
},
};
const dependencies = [{ packageName: 'dep1' }];

const result = await processor.process(pyproject, dependencies);

expect(result).toEqual([
{
packageName: 'dep1',
registryUrls: [
'https://foo.example.com',
'https://bar.example.com',
'https://foobar.example.com',
],
},
]);
});
Expand Down
44 changes: 41 additions & 3 deletions lib/modules/manager/pep621/processors/uv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,28 @@ import { exec } from '../../../../util/exec';
import type { ExecOptions, ToolConstraint } from '../../../../util/exec/types';
import { getSiblingFileName, readLocalFile } from '../../../../util/fs';
import { Result } from '../../../../util/result';
import { Toml } from '../../../../util/schema-utils';
import { PypiDatasource } from '../../../datasource/pypi';
import type {
PackageDependency,
UpdateArtifact,
UpdateArtifactsResult,
Upgrade,
} from '../../types';
import { type PyProject, UvLockfileSchema } from '../schema';
import { type PyProject, type Uv, UvLockfileSchema } from '../schema';
import { depTypes, parseDependencyList } from '../utils';
import type { PyProjectProcessor } from './types';

const UV_CONFIG_FILE = 'uv.toml';

const uvUpdateCMD = 'uv lock';

export class UvProcessor implements PyProjectProcessor {
process(project: PyProject, deps: PackageDependency[]): PackageDependency[] {
const uv = project.tool?.uv;
async process(
project: PyProject,
deps: PackageDependency[],
): Promise<PackageDependency[]> {
const uv = await getUvConfig(project.tool?.uv);
if (is.nullOrUndefined(uv)) {
return deps;
}
Expand All @@ -32,6 +39,11 @@ export class UvProcessor implements PyProjectProcessor {
),
);

const registryUrls = extractRegistryUrls(uv);
for (const dep of deps) {
dep.registryUrls = [...registryUrls];
}

return deps;
}

Expand Down Expand Up @@ -142,6 +154,32 @@ export class UvProcessor implements PyProjectProcessor {
}
}

function extractRegistryUrls(uvManifest: Uv): string[] {
// Extra indexes have priority over default index: https://docs.astral.sh/uv/reference/settings/#extra-index-url
const registryUrls = uvManifest['extra-index-url'] ?? [];

// If default index URL is not overridden, we need to use default PyPI URL additionally to potential extra indexes.
registryUrls.push(uvManifest['index-url'] ?? PypiDatasource.defaultURL);

return registryUrls;
}

async function getUvConfig(
uvPyprojectSection: Uv | undefined,
): Promise<Uv | undefined> {
const uvTomlContent = await readLocalFile(UV_CONFIG_FILE, 'utf8');

if (uvTomlContent) {
try {
return Toml.parse(uvTomlContent) as Uv;
} catch (err) {
logger.debug({ UV_CONFIG_FILE, err }, 'Error parsing uv config file');
}
}

return uvPyprojectSection;
}

function generateCMD(updatedDeps: Upgrade[]): string {
const deps: string[] = [];

Expand Down
Loading