Files
setup-uv/src/version/requirements-file.ts
T
9c7332190c fix: allow whitespace in uv requirement specifiers (#1112)
## Summary

When the uv version is read from dependencies (`requirements.txt`, or
`project.dependencies` / `optional-dependencies` / `dependency-groups`
in `pyproject.toml` via `version-file`), the specifier was matched with
`/^uv([=<>~!]+\S*)/`. PEP 508 allows whitespace around operators and
after commas, and those valid forms were misread:

| dependency | before | after |
|---|---|---|
| `uv >= 0.5.0` | not found, latest uv installed | `>=0.5.0` |
| `uv == 0.11.20` | not found, latest uv installed | `==0.11.20` |
| `uv>=0.5.0, <0.6.0` | `>=0.5.0,`, fails with `No version found for
>=0.5.0,` | `>=0.5.0,<0.6.0` |

The regex now allows whitespace around operators and between
comma-separated clauses, and strips it from the result. Inputs that
worked before give the same result, including inline `--hash=` options,
trailing ` # comments`, the `\` continuation lines from `uv pip compile
--generate-hashes`, and names that only start with `uv` (`uvicorn`,
`uv-build`).

## Test plan

- Added cases for spaced operators, spaced clauses, inline `--hash` and
trailing comments. The four whitespace cases failed before the change.
- `npm run all` on Ubuntu (WSL) with Node 24.19.0: build, Biome (no
fixes), packaging, and 165 tests across 15 suites pass. The rebuilt
`dist/setup/index.cjs` only differs in the changed line.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 08:37:03 +01:00

76 lines
2.2 KiB
TypeScript

import fs from "node:fs";
import * as toml from "smol-toml";
export function getUvVersionFromRequirementsFile(
filePath: string,
): string | undefined {
const fileContent = fs.readFileSync(filePath, "utf-8");
if (filePath.endsWith(".txt")) {
return getUvVersionFromRequirementsText(fileContent);
}
return getUvVersionFromPyprojectContent(fileContent);
}
export function getUvVersionFromRequirementsText(
fileContent: string,
): string | undefined {
return getUvVersionFromAllDependencies(fileContent.split("\n"));
}
export function getUvVersionFromParsedPyproject(
pyproject: Pyproject,
): string | undefined {
const dependencies: string[] = pyproject?.project?.dependencies || [];
const optionalDependencies: string[] = Object.values(
pyproject?.project?.["optional-dependencies"] || {},
).flat();
const devDependencies: string[] = Object.values(
pyproject?.["dependency-groups"] || {},
)
.flat()
.filter((item: string | object) => typeof item === "string");
return getUvVersionFromAllDependencies(
dependencies.concat(optionalDependencies, devDependencies),
);
}
export function getUvVersionFromPyprojectContent(
pyprojectContent: string,
): string | undefined {
const pyproject = parsePyprojectContent(pyprojectContent);
return getUvVersionFromParsedPyproject(pyproject);
}
export interface Pyproject {
project?: {
dependencies?: string[];
"optional-dependencies"?: Record<string, string[]>;
};
"dependency-groups"?: Record<string, Array<string | object>>;
tool?: {
uv?: Record<string, string | undefined>;
};
}
export function parsePyprojectContent(pyprojectContent: string): Pyproject {
return toml.parse(pyprojectContent) as Pyproject;
}
function getUvVersionFromAllDependencies(
allDependencies: string[],
): string | undefined {
return allDependencies
.map(getUvVersionFromDependency)
.find((version): version is string => version !== undefined);
}
function getUvVersionFromDependency(dependency: string): string | undefined {
const dependencyWithoutMarker = dependency.split(";", 1)[0]?.trim();
return dependencyWithoutMarker
?.match(/^uv\s*((?:[=<>~!]+\s*[^\s,]+\s*,?\s*)+)/)?.[1]
.replace(/\s/g, "");
}