pyfig 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -81,7 +81,21 @@ var import_undici = require("undici");
81
81
  // src/config.ts
82
82
  var import_node_path2 = __toESM(require("node:path"), 1);
83
83
  var envPaths = __toESM(require("env-paths"), 1);
84
- var import_semver = __toESM(require("semver"), 1);
84
+
85
+ // src/pep440.ts
86
+ var import_pep440 = require("@renovatebot/pep440");
87
+ function isValidPep440Specifier(specifier) {
88
+ return import_pep440.validRange(specifier);
89
+ }
90
+ function normalizePep440Version(version) {
91
+ return import_pep440.valid(version);
92
+ }
93
+ function pep440VersionSatisfies(version, specifier) {
94
+ return import_pep440.filter([version], specifier).length === 1;
95
+ }
96
+ function maxPep440Version(versions, specifier) {
97
+ return import_pep440.maxSatisfying([...versions], specifier);
98
+ }
85
99
 
86
100
  // src/project-root.ts
87
101
  var import_node_fs = __toESM(require("node:fs"), 1);
@@ -105,23 +119,10 @@ function* ancestors(startDirectory) {
105
119
  }
106
120
  }
107
121
 
108
- // src/python-version.ts
109
- var import_pep440 = require("@renovatebot/pep440");
110
- function isValidPythonSpecifier(specifier) {
111
- return import_pep440.validRange(specifier);
112
- }
113
- function isValidPythonVersion(version) {
114
- return import_pep440.valid(version) !== null;
115
- }
116
- function pythonVersionSatisfies(version, specifier) {
117
- return import_pep440.filter([version], specifier).length === 1;
118
- }
119
-
120
122
  // src/config.ts
121
123
  var DEFAULT_REQUIRES_PYTHON = ">=3.10,<4";
122
124
  var DEFAULT_MANAGED_PYTHON = "3.12";
123
- var DEFAULT_UV_VERSION = "0.13.6";
124
- var DEFAULT_UV_RANGE = ">=0.10.0 <1.0.0";
125
+ var DEFAULT_REQUIRES_UV = ">=0.10,<1";
125
126
  var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
126
127
  var DEFAULT_INSTALL_TIMEOUT_MS = 10 * 60000;
127
128
  var DEFAULT_DOWNLOAD_TIMEOUT_MS = 5 * 60000;
@@ -135,13 +136,11 @@ var envPathsFunction = (() => {
135
136
  function resolveOptions(options = {}) {
136
137
  const env = { ...options.env ?? process.env };
137
138
  const requiresPython = options.requiresPython ?? env["PYFIG_REQUIRES_PYTHON"] ?? DEFAULT_REQUIRES_PYTHON;
138
- const uvRange = options.uvRange ?? env["PYFIG_UV_RANGE"] ?? DEFAULT_UV_RANGE;
139
- assertPythonSpecifier(requiresPython);
140
- assertRange("uv", uvRange);
139
+ const requiresUv = options.requiresUv ?? env["PYFIG_REQUIRES_UV"] ?? DEFAULT_REQUIRES_UV;
140
+ assertPep440Specifier("requires-python", requiresPython);
141
+ assertPep440Specifier("requires-uv", requiresUv);
141
142
  const managedPython = options.managedPython ?? env["PYFIG_MANAGED_PYTHON"] ?? DEFAULT_MANAGED_PYTHON;
142
- const uvVersion = options.uvVersion ?? env["PYFIG_UV_VERSION"] ?? DEFAULT_UV_VERSION;
143
143
  assertManagedPythonRequest(managedPython, requiresPython);
144
- assertUvVersion(uvVersion, uvRange);
145
144
  const probeTimeoutMs = resolveDuration("probeTimeoutMs", options.probeTimeoutMs, env["PYFIG_PROBE_TIMEOUT_MS"], DEFAULT_PROBE_TIMEOUT_MS);
146
145
  const installTimeoutMs = resolveDuration("installTimeoutMs", options.installTimeoutMs, env["PYFIG_INSTALL_TIMEOUT_MS"], DEFAULT_INSTALL_TIMEOUT_MS);
147
146
  const downloadTimeoutMs = resolveDuration("downloadTimeoutMs", options.downloadTimeoutMs, env["PYFIG_DOWNLOAD_TIMEOUT_MS"], DEFAULT_DOWNLOAD_TIMEOUT_MS);
@@ -155,8 +154,7 @@ function resolveOptions(options = {}) {
155
154
  python: options.python ?? env["PYFIG_PYTHON"] ?? "",
156
155
  requiresPython,
157
156
  managedPython,
158
- uvVersion,
159
- uvRange,
157
+ requiresUv,
160
158
  requireEmbedding: options.requireEmbedding ?? parseBoolean(env["PYFIG_REQUIRE_EMBEDDING"], false),
161
159
  requireGil: options.requireGil ?? parseBoolean(env["PYFIG_REQUIRE_GIL"], false),
162
160
  autoInstall,
@@ -181,22 +179,9 @@ function uvEnvironment(options) {
181
179
  UV_NO_CONFIG: "1"
182
180
  };
183
181
  }
184
- function assertRange(tool, range) {
185
- if (!import_semver.default.validRange(range)) {
186
- throw new TypeError(`Invalid ${tool} SemVer range: ${range}`);
187
- }
188
- }
189
- function assertUvVersion(version, range) {
190
- if (!import_semver.default.valid(version)) {
191
- throw new TypeError(`Invalid exact uv version: ${version}`);
192
- }
193
- if (!import_semver.default.satisfies(version, range)) {
194
- throw new TypeError(`Pinned uv ${version} does not satisfy supported range ${range}.`);
195
- }
196
- }
197
- function assertPythonSpecifier(specifier) {
198
- if (!isValidPythonSpecifier(specifier)) {
199
- throw new TypeError(`Invalid requires-python PEP 440 specifier: ${specifier}`);
182
+ function assertPep440Specifier(name, specifier) {
183
+ if (!isValidPep440Specifier(specifier)) {
184
+ throw new TypeError(`Invalid ${name} PEP 440 specifier: ${specifier}`);
200
185
  }
201
186
  }
202
187
  function assertManagedPythonRequest(request, requiresPython) {
@@ -205,7 +190,7 @@ function assertManagedPythonRequest(request, requiresPython) {
205
190
  }
206
191
  if (/^\d+\.\d+(?:\.\d+)?$/u.test(request)) {
207
192
  const normalized = request.split(".").length === 2 ? `${request}.0` : request;
208
- if (!pythonVersionSatisfies(normalized, requiresPython)) {
193
+ if (!pep440VersionSatisfies(normalized, requiresPython)) {
209
194
  throw new TypeError(`Managed Python ${request} cannot satisfy requires-python ${requiresPython}.`);
210
195
  }
211
196
  }
@@ -371,9 +356,6 @@ function assertNoTargetOverride(args) {
371
356
  }
372
357
  }
373
358
 
374
- // src/probes.ts
375
- var import_semver2 = __toESM(require("semver"), 1);
376
-
377
359
  // src/process.ts
378
360
  var import_node_child_process = require("node:child_process");
379
361
  var import_node_path4 = __toESM(require("node:path"), 1);
@@ -543,7 +525,7 @@ print(json.dumps({
543
525
  }))
544
526
  `;
545
527
  function pythonSatisfiesPolicy(python, options) {
546
- return pythonVersionSatisfies(python.version, options.requiresPython) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
528
+ return pep440VersionSatisfies(python.version, options.requiresPython) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
547
529
  }
548
530
  async function probePython(candidate, options) {
549
531
  const executable = await resolveExecutable(candidate.executable, options.env);
@@ -552,7 +534,7 @@ async function probePython(candidate, options) {
552
534
  try {
553
535
  const { stdout } = run(executable, [...candidate.args, "-c", PYTHON_PROBE], options.env, options.probeTimeoutMs);
554
536
  const info = JSON.parse(stdout.trim());
555
- return isValidPythonVersion(info.version) ? info : null;
537
+ return normalizePep440Version(info.version) ? info : null;
556
538
  } catch {
557
539
  return null;
558
540
  }
@@ -561,7 +543,7 @@ async function findUv(options) {
561
543
  const configured = options.env["PYFIG_UV"] ?? "uv";
562
544
  const executable = await resolveExecutable(configured, options.env);
563
545
  const uv = executable ? probeUv(executable, options) : null;
564
- return uv && import_semver2.default.satisfies(uv.version, options.uvRange) ? uv : null;
546
+ return uv && pep440VersionSatisfies(uv.version, options.requiresUv) ? uv : null;
565
547
  }
566
548
  function probeUv(executable, options) {
567
549
  try {
@@ -575,7 +557,7 @@ function probeUv(executable, options) {
575
557
  function normalizeVersion(value) {
576
558
  if (!value)
577
559
  return null;
578
- return import_semver2.default.valid(value) ?? import_semver2.default.coerce(value)?.version ?? null;
560
+ return normalizePep440Version(value);
579
561
  }
580
562
  function pythonArchitectureMatchesRuntime(python) {
581
563
  const architecture = python.architecture.toLowerCase();
@@ -814,21 +796,30 @@ async function ensureUvResolved(options) {
814
796
  if (existing)
815
797
  return existing;
816
798
  if (!options.installUv) {
817
- throw new Error(`Compatible uv is unavailable (required ${options.uvRange}), and uv installation is disabled.`);
799
+ throw new Error(`Compatible uv is unavailable (requires-uv ${options.requiresUv}), and uv installation is disabled.`);
818
800
  }
819
- const installDir = uvInstallDirectory(options);
820
- const executable = uvExecutable(options);
801
+ const policyKey = import_node_crypto.createHash("sha256").update(options.requiresUv).digest("hex");
821
802
  return withProvisioningLock({
822
- key: `uv:${canonicalPath(installDir)}`,
803
+ key: `uv-policy:${canonicalPath(uvInstallRoot(options))}:${policyKey}`,
823
804
  timeoutMs: options.coordinationTimeoutMs,
824
805
  check: () => findAvailableUv(options),
825
806
  mutate: async () => {
826
- await installUv(installDir, options);
827
- const result = probeUv(executable, options);
828
- if (!result || result.version !== options.uvVersion) {
829
- throw new Error(`Expected managed uv ${options.uvVersion}, but the installed executable did not match.`);
830
- }
831
- return result;
807
+ const release = await resolveUvRelease(options);
808
+ const installDir = uvInstallDirectory(options, release.version);
809
+ const executable = uvExecutable(options, release.version);
810
+ return withProvisioningLock({
811
+ key: `uv-release:${canonicalPath(installDir)}`,
812
+ timeoutMs: options.coordinationTimeoutMs,
813
+ check: () => findAvailableUv(options),
814
+ mutate: async () => {
815
+ await installUv(installDir, release, options);
816
+ const result = probeUv(executable, options);
817
+ if (!result || result.version !== release.version) {
818
+ throw new Error(`Expected managed uv ${release.version}, but the installed executable did not match.`);
819
+ }
820
+ return result;
821
+ }
822
+ });
832
823
  }
833
824
  });
834
825
  }
@@ -836,8 +827,21 @@ async function findAvailableUv(options) {
836
827
  const external = await findUv(options);
837
828
  if (external)
838
829
  return external;
839
- const managed = probeUv(uvExecutable(options), options);
840
- return managed?.version === options.uvVersion ? managed : null;
830
+ return findManagedUv(options);
831
+ }
832
+ async function findManagedUv(options) {
833
+ const entries = await import_promises.readdir(uvInstallRoot(options), { withFileTypes: true }).catch(() => []);
834
+ let versions = entries.filter((entry) => entry.isDirectory() && normalizePep440Version(entry.name) === entry.name).map((entry) => entry.name);
835
+ while (versions.length > 0) {
836
+ const selected = maxPep440Version(versions, options.requiresUv);
837
+ if (!selected)
838
+ return null;
839
+ const candidate = probeUv(uvExecutable(options, selected), options);
840
+ if (candidate?.version === selected)
841
+ return candidate;
842
+ versions = versions.filter((version) => version !== selected);
843
+ }
844
+ return null;
841
845
  }
842
846
  function normalizeDependencies(dependencies) {
843
847
  const normalized = dependencies.map((dependency) => dependency.trim());
@@ -898,15 +902,159 @@ async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, opt
898
902
  const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
899
903
  return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
900
904
  }
901
- function uvInstallDirectory(options) {
902
- return import_node_path5.default.join(options.cacheDir, "tools", "uv", options.uvVersion);
905
+ var UV_RELEASE_PAGE_SIZE = 100;
906
+ var UV_RELEASE_MAX_PAGES = 20;
907
+ var UV_RELEASE_METADATA_TTL_MS = 24 * 60 * 60000;
908
+ async function resolveUvRelease(options) {
909
+ const catalog = await ensureUvReleaseCatalog(options);
910
+ const asset = uvAsset();
911
+ const releases = catalog.releases.filter((release) => release.assets.includes(asset) && release.assets.includes(`${asset}.sha256`));
912
+ const version = maxPep440Version(releases.map((release) => release.version), options.requiresUv);
913
+ const release = releases.find((candidate) => candidate.version === version);
914
+ if (!release) {
915
+ throw new Error(`No published uv release satisfies requires-uv ${options.requiresUv}.`);
916
+ }
917
+ return release;
918
+ }
919
+ async function ensureUvReleaseCatalog(options) {
920
+ const file = uvReleaseMetadataFile(options);
921
+ const stale = await readUvReleaseCatalog(file);
922
+ try {
923
+ return await withProvisioningLock({
924
+ key: `uv-metadata:${canonicalPath(file)}`,
925
+ timeoutMs: options.coordinationTimeoutMs,
926
+ check: () => readFreshUvReleaseCatalog(file),
927
+ mutate: async () => {
928
+ const catalog = await fetchUvReleaseCatalog(options);
929
+ await writeUvReleaseCatalog(file, catalog);
930
+ return catalog;
931
+ }
932
+ });
933
+ } catch (error) {
934
+ const fallback = await readUvReleaseCatalog(file) ?? stale;
935
+ if (fallback)
936
+ return fallback;
937
+ throw error;
938
+ }
939
+ }
940
+ async function fetchUvReleaseCatalog(options) {
941
+ const dispatcher = proxyDispatcher(options.env);
942
+ const releases = new Map;
943
+ let exhausted = false;
944
+ try {
945
+ for (let page = 1;page <= UV_RELEASE_MAX_PAGES; page += 1) {
946
+ const text = await downloadText(`https://api.github.com/repos/astral-sh/uv/releases?per_page=${UV_RELEASE_PAGE_SIZE}&page=${page}`, options.downloadTimeoutMs, dispatcher, {
947
+ Accept: "application/vnd.github+json",
948
+ "User-Agent": "pyfig",
949
+ "X-GitHub-Api-Version": "2022-11-28"
950
+ });
951
+ const payload = JSON.parse(text);
952
+ if (!Array.isArray(payload)) {
953
+ throw new Error("GitHub returned invalid uv release metadata.");
954
+ }
955
+ for (const candidate of payload) {
956
+ if (!candidate || typeof candidate !== "object")
957
+ continue;
958
+ const release = candidate;
959
+ if (release.draft === true || typeof release.tag_name !== "string")
960
+ continue;
961
+ const assetNames = releaseAssetNames(release.assets);
962
+ const version = normalizePep440Version(release.tag_name);
963
+ if (version) {
964
+ releases.set(version, { assets: assetNames, tag: release.tag_name, version });
965
+ }
966
+ }
967
+ if (payload.length < UV_RELEASE_PAGE_SIZE) {
968
+ exhausted = true;
969
+ break;
970
+ }
971
+ }
972
+ } finally {
973
+ await dispatcher?.close();
974
+ }
975
+ if (!exhausted) {
976
+ throw new Error(`uv release catalog exceeded ${UV_RELEASE_MAX_PAGES} pages.`);
977
+ }
978
+ if (releases.size === 0) {
979
+ throw new Error("GitHub returned no usable uv releases.");
980
+ }
981
+ return { fetchedAt: Date.now(), releases: [...releases.values()] };
982
+ }
983
+ async function readFreshUvReleaseCatalog(file) {
984
+ const catalog = await readUvReleaseCatalog(file);
985
+ if (!catalog)
986
+ return null;
987
+ const age = Date.now() - catalog.fetchedAt;
988
+ return age >= 0 && age < UV_RELEASE_METADATA_TTL_MS ? catalog : null;
989
+ }
990
+ async function readUvReleaseCatalog(file) {
991
+ try {
992
+ return parseUvReleaseCatalog(JSON.parse(await import_promises.readFile(file, "utf8")));
993
+ } catch {
994
+ return null;
995
+ }
996
+ }
997
+ function parseUvReleaseCatalog(value) {
998
+ if (!value || typeof value !== "object")
999
+ return null;
1000
+ const catalog = value;
1001
+ if (!Number.isSafeInteger(catalog.fetchedAt) || catalog.fetchedAt < 0 || !Array.isArray(catalog.releases)) {
1002
+ return null;
1003
+ }
1004
+ const releases = [];
1005
+ for (const value of catalog.releases) {
1006
+ if (!value || typeof value !== "object")
1007
+ return null;
1008
+ const release = value;
1009
+ if (!Array.isArray(release.assets) || !release.assets.every((asset) => typeof asset === "string") || typeof release.tag !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/u.test(release.tag) || typeof release.version !== "string" || normalizePep440Version(release.version) !== release.version) {
1010
+ return null;
1011
+ }
1012
+ releases.push({
1013
+ assets: release.assets,
1014
+ tag: release.tag,
1015
+ version: release.version
1016
+ });
1017
+ }
1018
+ return { fetchedAt: catalog.fetchedAt, releases };
1019
+ }
1020
+ async function writeUvReleaseCatalog(file, catalog) {
1021
+ await import_promises.mkdir(import_node_path5.default.dirname(file), { recursive: true });
1022
+ const staged = import_node_path5.default.join(import_node_path5.default.dirname(file), `.${import_node_path5.default.basename(file)}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`);
1023
+ try {
1024
+ await import_promises.writeFile(staged, `${JSON.stringify(catalog)}
1025
+ `, { encoding: "utf8", flag: "wx" });
1026
+ if (process.platform === "win32")
1027
+ await import_promises.rm(file, { force: true });
1028
+ await import_promises.rename(staged, file);
1029
+ } finally {
1030
+ await import_promises.rm(staged, { force: true });
1031
+ }
1032
+ }
1033
+ function uvReleaseMetadataFile(options) {
1034
+ return import_node_path5.default.join(options.cacheDir, "metadata", "uv-releases.json");
1035
+ }
1036
+ function releaseAssetNames(value) {
1037
+ if (!Array.isArray(value))
1038
+ return [];
1039
+ return value.flatMap((entry) => {
1040
+ if (!entry || typeof entry !== "object")
1041
+ return [];
1042
+ const name = entry.name;
1043
+ return typeof name === "string" ? [name] : [];
1044
+ });
1045
+ }
1046
+ function uvInstallRoot(options) {
1047
+ return import_node_path5.default.join(options.cacheDir, "tools", "uv");
1048
+ }
1049
+ function uvInstallDirectory(options, version) {
1050
+ return import_node_path5.default.join(uvInstallRoot(options), version);
903
1051
  }
904
- function uvExecutable(options) {
905
- return import_node_path5.default.join(uvInstallDirectory(options), process.platform === "win32" ? "uv.exe" : "uv");
1052
+ function uvExecutable(options, version) {
1053
+ return import_node_path5.default.join(uvInstallDirectory(options, version), process.platform === "win32" ? "uv.exe" : "uv");
906
1054
  }
907
- async function installUv(installDir, options) {
1055
+ async function installUv(installDir, release, options) {
908
1056
  const asset = uvAsset();
909
- const baseUrl = `https://github.com/astral-sh/uv/releases/download/${options.uvVersion}`;
1057
+ const baseUrl = `https://github.com/astral-sh/uv/releases/download/${release.tag}`;
910
1058
  const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
911
1059
  const dispatcher = proxyDispatcher(options.env);
912
1060
  try {
@@ -922,7 +1070,7 @@ async function installUv(installDir, options) {
922
1070
  const source = await extractUvExecutable(archive, asset, extracted);
923
1071
  if (!source)
924
1072
  throw new Error(`uv archive did not contain the expected executable: ${asset}`);
925
- await publishUvAtomically(source, installDir, options);
1073
+ await publishUvAtomically(source, installDir, release.version, options);
926
1074
  } finally {
927
1075
  await dispatcher?.close();
928
1076
  await import_promises.rm(temporary, { recursive: true, force: true });
@@ -944,17 +1092,17 @@ async function extractUvExecutable(archive, asset, destination) {
944
1092
  await import_promises.writeFile(source, matches[0][1], { flag: "wx" });
945
1093
  return source;
946
1094
  }
947
- async function publishUvAtomically(source, installDir, options) {
1095
+ async function publishUvAtomically(source, installDir, version, options) {
948
1096
  await import_promises.mkdir(installDir, { recursive: true });
949
- const executable = uvExecutable(options);
1097
+ const executable = uvExecutable(options, version);
950
1098
  const staged = import_node_path5.default.join(installDir, `.${import_node_path5.default.basename(executable)}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`);
951
1099
  try {
952
1100
  await import_promises.copyFile(source, staged);
953
1101
  if (process.platform !== "win32")
954
1102
  await import_promises.chmod(staged, 493);
955
1103
  const stagedUv = probeUv(staged, options);
956
- if (!stagedUv || stagedUv.version !== options.uvVersion) {
957
- throw new Error(`Downloaded uv executable is not version ${options.uvVersion}.`);
1104
+ if (!stagedUv || stagedUv.version !== version) {
1105
+ throw new Error(`Downloaded uv executable is not version ${version}.`);
958
1106
  }
959
1107
  if (process.platform === "win32")
960
1108
  await import_promises.rm(executable, { force: true });
@@ -987,9 +1135,10 @@ async function download(url, destination, timeoutMs, dispatcher) {
987
1135
  }
988
1136
  await import_promises2.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs3.createWriteStream(destination, { flags: "wx" }));
989
1137
  }
990
- async function downloadText(url, timeoutMs, dispatcher) {
1138
+ async function downloadText(url, timeoutMs, dispatcher, headers) {
991
1139
  const response = await import_undici.fetch(url, {
992
1140
  dispatcher,
1141
+ headers,
993
1142
  redirect: "follow",
994
1143
  signal: AbortSignal.timeout(timeoutMs)
995
1144
  });
@@ -1045,4 +1194,4 @@ main().catch((error) => {
1045
1194
  process.exitCode = 1;
1046
1195
  });
1047
1196
 
1048
- //# debugId=8D6E1E9C1BFA227064756E2164756E21
1197
+ //# debugId=D02BE720BF896D6864756E2164756E21