@vercel/python-analysis 0.8.0 → 0.8.2

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.
package/dist/index.cjs CHANGED
@@ -66,13 +66,15 @@ __export(src_exports, {
66
66
  containsAppOrHandler: () => containsAppOrHandler,
67
67
  createMinimalManifest: () => createMinimalManifest,
68
68
  discoverPythonPackage: () => discoverPythonPackage,
69
+ extendDistRecord: () => extendDistRecord,
69
70
  getStringConstant: () => getStringConstant,
70
71
  isPrivatePackageSource: () => isPrivatePackageSource,
71
- normalizePackageName: () => normalizePackageName,
72
+ normalizePackageName: () => normalizePackageName2,
72
73
  parseDjangoSettingsModule: () => parseDjangoSettingsModule,
73
74
  parseUvLock: () => parseUvLock,
74
75
  scanDistributions: () => scanDistributions,
75
76
  selectPython: () => selectPython,
77
+ selectPythonVersion: () => selectPythonVersion,
76
78
  stringifyManifest: () => stringifyManifest
77
79
  });
78
80
  module.exports = __toCommonJS(src_exports);
@@ -139,8 +141,51 @@ async function parseDjangoSettingsModule(content) {
139
141
  }
140
142
 
141
143
  // src/manifest/dist-metadata.ts
144
+ var import_node_crypto = require("crypto");
145
+ var import_node_fs = require("fs");
142
146
  var import_promises2 = require("fs/promises");
143
147
  var import_node_path2 = require("path");
148
+
149
+ // src/manifest/pep508.ts
150
+ var EXTRAS_REGEX = /^(.+)\[([^\]]+)\]$/;
151
+ function splitExtras(spec) {
152
+ const match = EXTRAS_REGEX.exec(spec);
153
+ if (!match) {
154
+ return [spec, void 0];
155
+ }
156
+ const extras = match[2].split(",").map((e) => e.trim());
157
+ return [match[1], extras];
158
+ }
159
+ function normalizePackageName(name) {
160
+ return name.toLowerCase().replace(/[-_.]+/g, "-");
161
+ }
162
+ function formatPep508(req) {
163
+ let result = req.name;
164
+ if (req.extras && req.extras.length > 0) {
165
+ result += `[${req.extras.join(",")}]`;
166
+ }
167
+ if (req.url) {
168
+ result += ` @ ${req.url}`;
169
+ } else if (req.version && req.version !== "*") {
170
+ result += req.version;
171
+ }
172
+ if (req.markers) {
173
+ result += ` ; ${req.markers}`;
174
+ }
175
+ return result;
176
+ }
177
+ function mergeExtras(existing, additional) {
178
+ const result = new Set(existing || []);
179
+ if (additional) {
180
+ const additionalArray = Array.isArray(additional) ? additional : [additional];
181
+ for (const extra of additionalArray) {
182
+ result.add(extra);
183
+ }
184
+ }
185
+ return result.size > 0 ? Array.from(result) : void 0;
186
+ }
187
+
188
+ // src/manifest/dist-metadata.ts
144
189
  async function readDistInfoFile(distInfoDir, filename) {
145
190
  try {
146
191
  return await (0, import_promises2.readFile)((0, import_node_path2.join)(distInfoDir, filename), "utf-8");
@@ -227,6 +272,62 @@ async function scanDistributions(sitePackagesDir) {
227
272
  }
228
273
  return index;
229
274
  }
275
+ function hashFile(filePath) {
276
+ return new Promise((resolve, reject) => {
277
+ const h = (0, import_node_crypto.createHash)("sha256");
278
+ let size = 0;
279
+ const stream = (0, import_node_fs.createReadStream)(filePath);
280
+ stream.on("data", (chunk) => {
281
+ size += chunk.length;
282
+ h.update(chunk);
283
+ });
284
+ stream.on("error", reject);
285
+ stream.on("end", () => {
286
+ resolve({ hash: h.digest("base64url"), size });
287
+ });
288
+ });
289
+ }
290
+ async function extendDistRecord(sitePackagesDir, packageName, paths) {
291
+ const normalizedTarget = normalizePackageName(packageName);
292
+ const entries = await (0, import_promises2.readdir)(sitePackagesDir);
293
+ const distInfoDirName = entries.find((e) => {
294
+ if (!e.endsWith(".dist-info"))
295
+ return false;
296
+ const withoutSuffix = e.slice(0, -".dist-info".length);
297
+ const lastHyphen = withoutSuffix.lastIndexOf("-");
298
+ if (lastHyphen === -1)
299
+ return false;
300
+ const dirName = withoutSuffix.slice(0, lastHyphen);
301
+ return normalizePackageName(dirName) === normalizedTarget;
302
+ });
303
+ if (!distInfoDirName) {
304
+ throw new Error(
305
+ `No .dist-info directory found for package "${packageName}" in ${sitePackagesDir}`
306
+ );
307
+ }
308
+ const recordPath = (0, import_node_path2.join)(sitePackagesDir, distInfoDirName, "RECORD");
309
+ let existingRecord;
310
+ try {
311
+ existingRecord = await (0, import_promises2.readFile)(recordPath, "utf-8");
312
+ } catch {
313
+ throw new Error(`RECORD file not found in ${distInfoDirName}`);
314
+ }
315
+ const existingPaths = new Set(
316
+ existingRecord.split("\n").filter((line) => line.length > 0).map((line) => line.split(",")[0])
317
+ );
318
+ const newEntries = paths.filter((p) => !existingPaths.has(p));
319
+ if (newEntries.length > 0) {
320
+ const prefix = existingRecord.length > 0 && !existingRecord.endsWith("\n") ? "\n" : "";
321
+ const lines = [];
322
+ for (const p of newEntries) {
323
+ const fullPath = (0, import_node_path2.join)(sitePackagesDir, p);
324
+ const { hash, size } = await hashFile(fullPath);
325
+ lines.push(`${p},sha256=${hash},${size}`);
326
+ }
327
+ await (0, import_promises2.appendFile)(recordPath, prefix + lines.join("\n") + "\n");
328
+ }
329
+ return newEntries.length;
330
+ }
230
331
 
231
332
  // src/manifest/package.ts
232
333
  var import_node_path6 = __toESM(require("path"), 1);
@@ -447,42 +548,6 @@ var PipfileLikeSchema = pipfileLikeSchema;
447
548
  var PipfileLockMetaSchema = pipfileLockMetaSchema.passthrough();
448
549
  var PipfileLockLikeSchema = pipfileLockLikeSchema;
449
550
 
450
- // src/manifest/pep508.ts
451
- var EXTRAS_REGEX = /^(.+)\[([^\]]+)\]$/;
452
- function splitExtras(spec) {
453
- const match = EXTRAS_REGEX.exec(spec);
454
- if (!match) {
455
- return [spec, void 0];
456
- }
457
- const extras = match[2].split(",").map((e) => e.trim());
458
- return [match[1], extras];
459
- }
460
- function formatPep508(req) {
461
- let result = req.name;
462
- if (req.extras && req.extras.length > 0) {
463
- result += `[${req.extras.join(",")}]`;
464
- }
465
- if (req.url) {
466
- result += ` @ ${req.url}`;
467
- } else if (req.version && req.version !== "*") {
468
- result += req.version;
469
- }
470
- if (req.markers) {
471
- result += ` ; ${req.markers}`;
472
- }
473
- return result;
474
- }
475
- function mergeExtras(existing, additional) {
476
- const result = new Set(existing || []);
477
- if (additional) {
478
- const additionalArray = Array.isArray(additional) ? additional : [additional];
479
- for (const extra of additionalArray) {
480
- result.add(extra);
481
- }
482
- }
483
- return result.size > 0 ? Array.from(result) : void 0;
484
- }
485
-
486
551
  // src/util/type.ts
487
552
  function isPlainObject(value) {
488
553
  return value != null && typeof value === "object" && !Array.isArray(value);
@@ -675,17 +740,24 @@ function convertPipfileToPyprojectToml(pipfile) {
675
740
  function convertPipfileLockToPyprojectToml(pipfileLock) {
676
741
  const sources = {};
677
742
  const pyproject = {};
743
+ const pythonVersion = pipfileLock._meta?.requires?.python_version;
678
744
  const deps = [];
679
745
  for (const dep of pipfileLockDepsToRequirements(pipfileLock.default || {})) {
680
746
  deps.push(formatPep508(dep));
681
747
  addDepSource(sources, dep);
682
748
  }
683
- if (deps.length > 0) {
684
- pyproject.project = {
749
+ if (deps.length > 0 || pythonVersion) {
750
+ const project = {
685
751
  name: "app",
686
- version: "0.1.0",
687
- dependencies: deps
752
+ version: "0.1.0"
688
753
  };
754
+ if (pythonVersion) {
755
+ project["requires-python"] = `==${pythonVersion}.*`;
756
+ }
757
+ if (deps.length > 0) {
758
+ project.dependencies = deps;
759
+ }
760
+ pyproject.project = project;
689
761
  }
690
762
  const devDeps = [];
691
763
  for (const dep of pipfileLockDepsToRequirements(pipfileLock.develop || {})) {
@@ -1866,8 +1938,10 @@ async function discoverPythonPackage({
1866
1938
  let workspaceManifest;
1867
1939
  let workspaceLockFile;
1868
1940
  if (manifests.length === 0) {
1941
+ const requiresPython2 = computeRequiresPython(void 0, void 0, configs);
1869
1942
  return {
1870
- configs
1943
+ configs,
1944
+ requiresPython: requiresPython2
1871
1945
  };
1872
1946
  } else {
1873
1947
  entrypointManifest = manifests[0];
@@ -1901,37 +1975,44 @@ async function discoverPythonPackage({
1901
1975
  }
1902
1976
  function computeRequiresPython(manifest, workspaceManifest, configs) {
1903
1977
  const constraints = [];
1978
+ let hasPythonVersionFile = false;
1904
1979
  for (const configSet of configs) {
1905
1980
  const pythonVersionConfig = configSet[".python-version" /* PythonVersion */];
1906
1981
  if (pythonVersionConfig !== void 0) {
1907
1982
  constraints.push({
1908
1983
  request: pythonVersionConfig.data,
1909
- source: `${pythonVersionConfig.path}`
1984
+ source: pythonVersionConfig.path,
1985
+ prettySource: pythonVersionConfig.path
1910
1986
  });
1987
+ hasPythonVersionFile = true;
1911
1988
  break;
1912
1989
  }
1913
1990
  }
1914
- const manifestRequiresPython = manifest?.data.project?.["requires-python"];
1915
- if (manifestRequiresPython) {
1916
- const parsed = (0, import_specifier.parse)(manifestRequiresPython);
1917
- if (parsed?.length) {
1918
- const request = pythonRequestFromConstraint(parsed);
1919
- constraints.push({
1920
- request: [request],
1921
- source: `"requires-python" key in ${manifest.path}`
1922
- });
1923
- }
1924
- } else {
1925
- const workspaceRequiresPython = workspaceManifest?.data.project?.["requires-python"];
1926
- if (workspaceRequiresPython) {
1927
- const parsed = (0, import_specifier.parse)(workspaceRequiresPython);
1991
+ if (!hasPythonVersionFile) {
1992
+ const manifestRequiresPython = manifest?.data.project?.["requires-python"];
1993
+ if (manifestRequiresPython) {
1994
+ const parsed = (0, import_specifier.parse)(manifestRequiresPython);
1928
1995
  if (parsed?.length) {
1929
1996
  const request = pythonRequestFromConstraint(parsed);
1930
1997
  constraints.push({
1931
1998
  request: [request],
1932
- source: `"requires-python" key in ${workspaceManifest.path}`
1999
+ source: manifest.path,
2000
+ prettySource: `"requires-python" key in ${manifest.path}`
1933
2001
  });
1934
2002
  }
2003
+ } else {
2004
+ const workspaceRequiresPython = workspaceManifest?.data.project?.["requires-python"];
2005
+ if (workspaceRequiresPython) {
2006
+ const parsed = (0, import_specifier.parse)(workspaceRequiresPython);
2007
+ if (parsed?.length) {
2008
+ const request = pythonRequestFromConstraint(parsed);
2009
+ constraints.push({
2010
+ request: [request],
2011
+ source: workspaceManifest.path,
2012
+ prettySource: `"requires-python" key in ${workspaceManifest.path}`
2013
+ });
2014
+ }
2015
+ }
1935
2016
  }
1936
2017
  }
1937
2018
  return constraints;
@@ -2280,7 +2361,7 @@ function isPrivatePackageSource(source) {
2280
2361
  }
2281
2362
  return false;
2282
2363
  }
2283
- function normalizePackageName(name) {
2364
+ function normalizePackageName2(name) {
2284
2365
  return name.toLowerCase().replace(/[-_.]+/g, "-");
2285
2366
  }
2286
2367
  function classifyPackages(options) {
@@ -2288,9 +2369,9 @@ function classifyPackages(options) {
2288
2369
  const privatePackages = [];
2289
2370
  const publicPackages = [];
2290
2371
  const packageVersions = {};
2291
- const excludeSet = new Set(excludePackages.map(normalizePackageName));
2372
+ const excludeSet = new Set(excludePackages.map(normalizePackageName2));
2292
2373
  for (const pkg of lockFile.packages) {
2293
- if (excludeSet.has(normalizePackageName(pkg.name))) {
2374
+ if (excludeSet.has(normalizePackageName2(pkg.name))) {
2294
2375
  continue;
2295
2376
  }
2296
2377
  packageVersions[pkg.name] = pkg.version;
@@ -2343,13 +2424,15 @@ function selectPython(constraints, available) {
2343
2424
  }
2344
2425
  }
2345
2426
  if (constraintsWithMatches.length > 1) {
2346
- const sources = constraintsWithMatches.map((i) => constraints[i].source);
2427
+ const sources = constraintsWithMatches.map(
2428
+ (i) => constraints[i].prettySource
2429
+ );
2347
2430
  warnings.push(
2348
2431
  `Python version constraints may not overlap: ${sources.join(", ")}`
2349
2432
  );
2350
2433
  }
2351
2434
  }
2352
- const constraintDescriptions = constraints.map((c) => c.source).join(", ");
2435
+ const constraintDescriptions = constraints.map((c) => c.prettySource).join(", ");
2353
2436
  errors.push(
2354
2437
  `No Python build satisfies all constraints: ${constraintDescriptions}`
2355
2438
  );
@@ -2359,6 +2442,126 @@ function selectPython(constraints, available) {
2359
2442
  warnings: warnings.length > 0 ? warnings : void 0
2360
2443
  };
2361
2444
  }
2445
+ function selectPythonVersion({
2446
+ constraints,
2447
+ availableBuilds,
2448
+ allBuilds,
2449
+ defaultBuild,
2450
+ majorMinorOnly,
2451
+ legacyTildeEquals
2452
+ }) {
2453
+ const source = constraints?.[0]?.source;
2454
+ if (!constraints || constraints.length === 0) {
2455
+ return { build: defaultBuild };
2456
+ }
2457
+ let effectiveConstraints = majorMinorOnly ? constraints.map((c) => ({
2458
+ ...c,
2459
+ request: c.request.map(truncatePatchVersionsInRequest)
2460
+ })) : constraints;
2461
+ if (legacyTildeEquals) {
2462
+ effectiveConstraints = effectiveConstraints.map((c) => ({
2463
+ ...c,
2464
+ request: c.request.map(legacyTildeEqualsTransform)
2465
+ }));
2466
+ }
2467
+ const result = selectPython(effectiveConstraints, availableBuilds);
2468
+ if (result.build) {
2469
+ return { build: result.build, source };
2470
+ }
2471
+ const allResult = selectPython(effectiveConstraints, allBuilds);
2472
+ if (allResult.build) {
2473
+ const version = pythonVersionToString(allResult.build.version);
2474
+ return {
2475
+ build: defaultBuild,
2476
+ source,
2477
+ notAvailable: { build: allResult.build, version }
2478
+ };
2479
+ }
2480
+ const versionString = extractConstraintVersionString(constraints);
2481
+ return {
2482
+ build: defaultBuild,
2483
+ source,
2484
+ invalidConstraint: { versionString }
2485
+ };
2486
+ }
2487
+ function extractConstraintVersionString(constraints) {
2488
+ for (const c of constraints) {
2489
+ for (const req of c.request) {
2490
+ if (req.version?.constraint && req.version.constraint.length > 0) {
2491
+ const specs = req.version.constraint;
2492
+ if (specs.length === 1 && specs[0].operator === "==" && (!specs[0].prefix || specs[0].prefix === ".*")) {
2493
+ return specs[0].version;
2494
+ }
2495
+ return pep440ConstraintsToString(specs);
2496
+ }
2497
+ }
2498
+ }
2499
+ return "unknown";
2500
+ }
2501
+ function truncatePatchVersionsInRequest(req) {
2502
+ if (!req.version?.constraint || req.version.constraint.length === 0) {
2503
+ return req;
2504
+ }
2505
+ const newConstraints = [];
2506
+ for (const c of req.version.constraint) {
2507
+ const result = truncatePatchConstraint(c);
2508
+ if (result !== null) {
2509
+ newConstraints.push(result);
2510
+ }
2511
+ }
2512
+ const newVersion = {
2513
+ ...req.version,
2514
+ constraint: newConstraints
2515
+ };
2516
+ return { ...req, version: newVersion };
2517
+ }
2518
+ function truncatePatchConstraint(c) {
2519
+ if (c.operator === "===") {
2520
+ return c;
2521
+ }
2522
+ const parts = c.version.split(".");
2523
+ if (parts.length < 3) {
2524
+ return c;
2525
+ }
2526
+ const majorMinor = parts.slice(0, 2).join(".");
2527
+ const patch = parseInt(parts[2], 10);
2528
+ switch (c.operator) {
2529
+ case "==":
2530
+ return { operator: "==", version: majorMinor, prefix: ".*" };
2531
+ case "!=":
2532
+ return null;
2533
+ case "~=":
2534
+ return { operator: "==", version: majorMinor, prefix: ".*" };
2535
+ case ">=":
2536
+ return { operator: ">=", version: majorMinor, prefix: "" };
2537
+ case "<=":
2538
+ return { operator: "<=", version: majorMinor, prefix: "" };
2539
+ case ">":
2540
+ return patch === 0 ? { operator: ">", version: majorMinor, prefix: "" } : { operator: ">=", version: majorMinor, prefix: "" };
2541
+ case "<":
2542
+ return patch === 0 ? { operator: "<", version: majorMinor, prefix: "" } : { operator: "<=", version: majorMinor, prefix: "" };
2543
+ default:
2544
+ return c;
2545
+ }
2546
+ }
2547
+ function legacyTildeEqualsTransform(req) {
2548
+ if (!req.version?.constraint || req.version.constraint.length === 0) {
2549
+ return req;
2550
+ }
2551
+ const newConstraints = req.version.constraint.map((c) => {
2552
+ if (c.operator !== "~=")
2553
+ return c;
2554
+ const parts = c.version.split(".");
2555
+ if (parts.length === 2) {
2556
+ return { operator: "==", version: c.version, prefix: ".*" };
2557
+ }
2558
+ return c;
2559
+ });
2560
+ return {
2561
+ ...req,
2562
+ version: { ...req.version, constraint: newConstraints }
2563
+ };
2564
+ }
2362
2565
  function pythonVersionToString(version) {
2363
2566
  let str = `${version.major}.${version.minor}`;
2364
2567
  if (version.patch !== void 0) {
@@ -2370,7 +2573,7 @@ function pythonVersionToString(version) {
2370
2573
  return str;
2371
2574
  }
2372
2575
  function pep440ConstraintsToString(constraints) {
2373
- return constraints.map((c) => `${c.operator}${c.prefix}${c.version}`).join(",");
2576
+ return constraints.map((c) => `${c.operator}${c.version}${c.prefix ?? ""}`).join(",");
2374
2577
  }
2375
2578
  function implementationsMatch(buildImpl, requestImpl) {
2376
2579
  if (PythonImplementation.isUnknown(buildImpl)) {
@@ -2490,6 +2693,7 @@ var HashDigestSchema = hashDigestSchema;
2490
2693
  containsAppOrHandler,
2491
2694
  createMinimalManifest,
2492
2695
  discoverPythonPackage,
2696
+ extendDistRecord,
2493
2697
  getStringConstant,
2494
2698
  isPrivatePackageSource,
2495
2699
  normalizePackageName,
@@ -2497,5 +2701,6 @@ var HashDigestSchema = hashDigestSchema;
2497
2701
  parseUvLock,
2498
2702
  scanDistributions,
2499
2703
  selectPython,
2704
+ selectPythonVersion,
2500
2705
  stringifyManifest
2501
2706
  });
package/dist/index.d.ts CHANGED
@@ -8,14 +8,14 @@
8
8
  */
9
9
  export { containsAppOrHandler, getStringConstant, parseDjangoSettingsModule, } from './semantic/entrypoints';
10
10
  export type { Distribution, DistributionIndex, PackagePath, DirectUrlInfo, } from './manifest/dist-metadata';
11
- export { scanDistributions } from './manifest/dist-metadata';
11
+ export { extendDistRecord, scanDistributions } from './manifest/dist-metadata';
12
12
  export type { PythonConfig, PythonConfigs, PythonLockFile, PythonManifest, PythonManifestOrigin, PythonPackage, PythonVersionConfig, } from './manifest/package';
13
13
  export { discoverPythonPackage, PythonConfigKind, PythonLockFileKind, PythonManifestConvertedKind, PythonManifestKind, } from './manifest/package';
14
14
  export { createMinimalManifest, stringifyManifest, type CreateMinimalManifestOptions, } from './manifest/serialize';
15
15
  export type { ClassifyPackagesOptions, PackageClassification, UvLockFile, UvLockPackage, UvLockPackageSource, } from './manifest/uv-lock-parser';
16
16
  export { classifyPackages, isPrivatePackageSource, normalizePackageName, parseUvLock, } from './manifest/uv-lock-parser';
17
- export type { PythonSelectionResult } from './manifest/python-selector';
18
- export { selectPython } from './manifest/python-selector';
17
+ export type { PythonSelectionResult, PythonVersionSelectionResult, } from './manifest/python-selector';
18
+ export { selectPython, selectPythonVersion } from './manifest/python-selector';
19
19
  export { PythonAnalysisError } from './util/error';
20
20
  export { LicenseObjectSchema, LicenseSchema, PersonSchema, PyProjectBuildSystemSchema, PyProjectDependencyGroupsSchema, PyProjectProjectSchema, PyProjectToolSectionSchema, PyProjectTomlSchema, ReadmeObjectSchema, ReadmeSchema, } from './manifest/pyproject/schema';
21
21
  export type { License, LicenseObject, Person, PyProjectBuildSystem, PyProjectDependencyGroups, PyProjectProject, PyProjectToml, PyProjectToolSection, Readme, ReadmeObject, } from './manifest/pyproject/types';
package/dist/index.js CHANGED
@@ -60,8 +60,51 @@ async function parseDjangoSettingsModule(content) {
60
60
  }
61
61
 
62
62
  // src/manifest/dist-metadata.ts
63
- import { readdir, readFile as readFile2 } from "fs/promises";
63
+ import { createHash } from "crypto";
64
+ import { createReadStream } from "fs";
65
+ import { appendFile, readdir, readFile as readFile2 } from "fs/promises";
64
66
  import { join as join2 } from "path";
67
+
68
+ // src/manifest/pep508.ts
69
+ var EXTRAS_REGEX = /^(.+)\[([^\]]+)\]$/;
70
+ function splitExtras(spec) {
71
+ const match = EXTRAS_REGEX.exec(spec);
72
+ if (!match) {
73
+ return [spec, void 0];
74
+ }
75
+ const extras = match[2].split(",").map((e) => e.trim());
76
+ return [match[1], extras];
77
+ }
78
+ function normalizePackageName(name) {
79
+ return name.toLowerCase().replace(/[-_.]+/g, "-");
80
+ }
81
+ function formatPep508(req) {
82
+ let result = req.name;
83
+ if (req.extras && req.extras.length > 0) {
84
+ result += `[${req.extras.join(",")}]`;
85
+ }
86
+ if (req.url) {
87
+ result += ` @ ${req.url}`;
88
+ } else if (req.version && req.version !== "*") {
89
+ result += req.version;
90
+ }
91
+ if (req.markers) {
92
+ result += ` ; ${req.markers}`;
93
+ }
94
+ return result;
95
+ }
96
+ function mergeExtras(existing, additional) {
97
+ const result = new Set(existing || []);
98
+ if (additional) {
99
+ const additionalArray = Array.isArray(additional) ? additional : [additional];
100
+ for (const extra of additionalArray) {
101
+ result.add(extra);
102
+ }
103
+ }
104
+ return result.size > 0 ? Array.from(result) : void 0;
105
+ }
106
+
107
+ // src/manifest/dist-metadata.ts
65
108
  async function readDistInfoFile(distInfoDir, filename) {
66
109
  try {
67
110
  return await readFile2(join2(distInfoDir, filename), "utf-8");
@@ -148,6 +191,62 @@ async function scanDistributions(sitePackagesDir) {
148
191
  }
149
192
  return index;
150
193
  }
194
+ function hashFile(filePath) {
195
+ return new Promise((resolve, reject) => {
196
+ const h = createHash("sha256");
197
+ let size = 0;
198
+ const stream = createReadStream(filePath);
199
+ stream.on("data", (chunk) => {
200
+ size += chunk.length;
201
+ h.update(chunk);
202
+ });
203
+ stream.on("error", reject);
204
+ stream.on("end", () => {
205
+ resolve({ hash: h.digest("base64url"), size });
206
+ });
207
+ });
208
+ }
209
+ async function extendDistRecord(sitePackagesDir, packageName, paths) {
210
+ const normalizedTarget = normalizePackageName(packageName);
211
+ const entries = await readdir(sitePackagesDir);
212
+ const distInfoDirName = entries.find((e) => {
213
+ if (!e.endsWith(".dist-info"))
214
+ return false;
215
+ const withoutSuffix = e.slice(0, -".dist-info".length);
216
+ const lastHyphen = withoutSuffix.lastIndexOf("-");
217
+ if (lastHyphen === -1)
218
+ return false;
219
+ const dirName = withoutSuffix.slice(0, lastHyphen);
220
+ return normalizePackageName(dirName) === normalizedTarget;
221
+ });
222
+ if (!distInfoDirName) {
223
+ throw new Error(
224
+ `No .dist-info directory found for package "${packageName}" in ${sitePackagesDir}`
225
+ );
226
+ }
227
+ const recordPath = join2(sitePackagesDir, distInfoDirName, "RECORD");
228
+ let existingRecord;
229
+ try {
230
+ existingRecord = await readFile2(recordPath, "utf-8");
231
+ } catch {
232
+ throw new Error(`RECORD file not found in ${distInfoDirName}`);
233
+ }
234
+ const existingPaths = new Set(
235
+ existingRecord.split("\n").filter((line) => line.length > 0).map((line) => line.split(",")[0])
236
+ );
237
+ const newEntries = paths.filter((p) => !existingPaths.has(p));
238
+ if (newEntries.length > 0) {
239
+ const prefix = existingRecord.length > 0 && !existingRecord.endsWith("\n") ? "\n" : "";
240
+ const lines = [];
241
+ for (const p of newEntries) {
242
+ const fullPath = join2(sitePackagesDir, p);
243
+ const { hash, size } = await hashFile(fullPath);
244
+ lines.push(`${p},sha256=${hash},${size}`);
245
+ }
246
+ await appendFile(recordPath, prefix + lines.join("\n") + "\n");
247
+ }
248
+ return newEntries.length;
249
+ }
151
250
 
152
251
  // src/manifest/package.ts
153
252
  import path3 from "path";
@@ -371,42 +470,6 @@ var PipfileLikeSchema = pipfileLikeSchema;
371
470
  var PipfileLockMetaSchema = pipfileLockMetaSchema.passthrough();
372
471
  var PipfileLockLikeSchema = pipfileLockLikeSchema;
373
472
 
374
- // src/manifest/pep508.ts
375
- var EXTRAS_REGEX = /^(.+)\[([^\]]+)\]$/;
376
- function splitExtras(spec) {
377
- const match = EXTRAS_REGEX.exec(spec);
378
- if (!match) {
379
- return [spec, void 0];
380
- }
381
- const extras = match[2].split(",").map((e) => e.trim());
382
- return [match[1], extras];
383
- }
384
- function formatPep508(req) {
385
- let result = req.name;
386
- if (req.extras && req.extras.length > 0) {
387
- result += `[${req.extras.join(",")}]`;
388
- }
389
- if (req.url) {
390
- result += ` @ ${req.url}`;
391
- } else if (req.version && req.version !== "*") {
392
- result += req.version;
393
- }
394
- if (req.markers) {
395
- result += ` ; ${req.markers}`;
396
- }
397
- return result;
398
- }
399
- function mergeExtras(existing, additional) {
400
- const result = new Set(existing || []);
401
- if (additional) {
402
- const additionalArray = Array.isArray(additional) ? additional : [additional];
403
- for (const extra of additionalArray) {
404
- result.add(extra);
405
- }
406
- }
407
- return result.size > 0 ? Array.from(result) : void 0;
408
- }
409
-
410
473
  // src/util/type.ts
411
474
  function isPlainObject(value) {
412
475
  return value != null && typeof value === "object" && !Array.isArray(value);
@@ -599,17 +662,24 @@ function convertPipfileToPyprojectToml(pipfile) {
599
662
  function convertPipfileLockToPyprojectToml(pipfileLock) {
600
663
  const sources = {};
601
664
  const pyproject = {};
665
+ const pythonVersion = pipfileLock._meta?.requires?.python_version;
602
666
  const deps = [];
603
667
  for (const dep of pipfileLockDepsToRequirements(pipfileLock.default || {})) {
604
668
  deps.push(formatPep508(dep));
605
669
  addDepSource(sources, dep);
606
670
  }
607
- if (deps.length > 0) {
608
- pyproject.project = {
671
+ if (deps.length > 0 || pythonVersion) {
672
+ const project = {
609
673
  name: "app",
610
- version: "0.1.0",
611
- dependencies: deps
674
+ version: "0.1.0"
612
675
  };
676
+ if (pythonVersion) {
677
+ project["requires-python"] = `==${pythonVersion}.*`;
678
+ }
679
+ if (deps.length > 0) {
680
+ project.dependencies = deps;
681
+ }
682
+ pyproject.project = project;
613
683
  }
614
684
  const devDeps = [];
615
685
  for (const dep of pipfileLockDepsToRequirements(pipfileLock.develop || {})) {
@@ -1790,8 +1860,10 @@ async function discoverPythonPackage({
1790
1860
  let workspaceManifest;
1791
1861
  let workspaceLockFile;
1792
1862
  if (manifests.length === 0) {
1863
+ const requiresPython2 = computeRequiresPython(void 0, void 0, configs);
1793
1864
  return {
1794
- configs
1865
+ configs,
1866
+ requiresPython: requiresPython2
1795
1867
  };
1796
1868
  } else {
1797
1869
  entrypointManifest = manifests[0];
@@ -1825,37 +1897,44 @@ async function discoverPythonPackage({
1825
1897
  }
1826
1898
  function computeRequiresPython(manifest, workspaceManifest, configs) {
1827
1899
  const constraints = [];
1900
+ let hasPythonVersionFile = false;
1828
1901
  for (const configSet of configs) {
1829
1902
  const pythonVersionConfig = configSet[".python-version" /* PythonVersion */];
1830
1903
  if (pythonVersionConfig !== void 0) {
1831
1904
  constraints.push({
1832
1905
  request: pythonVersionConfig.data,
1833
- source: `${pythonVersionConfig.path}`
1906
+ source: pythonVersionConfig.path,
1907
+ prettySource: pythonVersionConfig.path
1834
1908
  });
1909
+ hasPythonVersionFile = true;
1835
1910
  break;
1836
1911
  }
1837
1912
  }
1838
- const manifestRequiresPython = manifest?.data.project?.["requires-python"];
1839
- if (manifestRequiresPython) {
1840
- const parsed = parse2(manifestRequiresPython);
1841
- if (parsed?.length) {
1842
- const request = pythonRequestFromConstraint(parsed);
1843
- constraints.push({
1844
- request: [request],
1845
- source: `"requires-python" key in ${manifest.path}`
1846
- });
1847
- }
1848
- } else {
1849
- const workspaceRequiresPython = workspaceManifest?.data.project?.["requires-python"];
1850
- if (workspaceRequiresPython) {
1851
- const parsed = parse2(workspaceRequiresPython);
1913
+ if (!hasPythonVersionFile) {
1914
+ const manifestRequiresPython = manifest?.data.project?.["requires-python"];
1915
+ if (manifestRequiresPython) {
1916
+ const parsed = parse2(manifestRequiresPython);
1852
1917
  if (parsed?.length) {
1853
1918
  const request = pythonRequestFromConstraint(parsed);
1854
1919
  constraints.push({
1855
1920
  request: [request],
1856
- source: `"requires-python" key in ${workspaceManifest.path}`
1921
+ source: manifest.path,
1922
+ prettySource: `"requires-python" key in ${manifest.path}`
1857
1923
  });
1858
1924
  }
1925
+ } else {
1926
+ const workspaceRequiresPython = workspaceManifest?.data.project?.["requires-python"];
1927
+ if (workspaceRequiresPython) {
1928
+ const parsed = parse2(workspaceRequiresPython);
1929
+ if (parsed?.length) {
1930
+ const request = pythonRequestFromConstraint(parsed);
1931
+ constraints.push({
1932
+ request: [request],
1933
+ source: workspaceManifest.path,
1934
+ prettySource: `"requires-python" key in ${workspaceManifest.path}`
1935
+ });
1936
+ }
1937
+ }
1859
1938
  }
1860
1939
  }
1861
1940
  return constraints;
@@ -2204,7 +2283,7 @@ function isPrivatePackageSource(source) {
2204
2283
  }
2205
2284
  return false;
2206
2285
  }
2207
- function normalizePackageName(name) {
2286
+ function normalizePackageName2(name) {
2208
2287
  return name.toLowerCase().replace(/[-_.]+/g, "-");
2209
2288
  }
2210
2289
  function classifyPackages(options) {
@@ -2212,9 +2291,9 @@ function classifyPackages(options) {
2212
2291
  const privatePackages = [];
2213
2292
  const publicPackages = [];
2214
2293
  const packageVersions = {};
2215
- const excludeSet = new Set(excludePackages.map(normalizePackageName));
2294
+ const excludeSet = new Set(excludePackages.map(normalizePackageName2));
2216
2295
  for (const pkg of lockFile.packages) {
2217
- if (excludeSet.has(normalizePackageName(pkg.name))) {
2296
+ if (excludeSet.has(normalizePackageName2(pkg.name))) {
2218
2297
  continue;
2219
2298
  }
2220
2299
  packageVersions[pkg.name] = pkg.version;
@@ -2267,13 +2346,15 @@ function selectPython(constraints, available) {
2267
2346
  }
2268
2347
  }
2269
2348
  if (constraintsWithMatches.length > 1) {
2270
- const sources = constraintsWithMatches.map((i) => constraints[i].source);
2349
+ const sources = constraintsWithMatches.map(
2350
+ (i) => constraints[i].prettySource
2351
+ );
2271
2352
  warnings.push(
2272
2353
  `Python version constraints may not overlap: ${sources.join(", ")}`
2273
2354
  );
2274
2355
  }
2275
2356
  }
2276
- const constraintDescriptions = constraints.map((c) => c.source).join(", ");
2357
+ const constraintDescriptions = constraints.map((c) => c.prettySource).join(", ");
2277
2358
  errors.push(
2278
2359
  `No Python build satisfies all constraints: ${constraintDescriptions}`
2279
2360
  );
@@ -2283,6 +2364,126 @@ function selectPython(constraints, available) {
2283
2364
  warnings: warnings.length > 0 ? warnings : void 0
2284
2365
  };
2285
2366
  }
2367
+ function selectPythonVersion({
2368
+ constraints,
2369
+ availableBuilds,
2370
+ allBuilds,
2371
+ defaultBuild,
2372
+ majorMinorOnly,
2373
+ legacyTildeEquals
2374
+ }) {
2375
+ const source = constraints?.[0]?.source;
2376
+ if (!constraints || constraints.length === 0) {
2377
+ return { build: defaultBuild };
2378
+ }
2379
+ let effectiveConstraints = majorMinorOnly ? constraints.map((c) => ({
2380
+ ...c,
2381
+ request: c.request.map(truncatePatchVersionsInRequest)
2382
+ })) : constraints;
2383
+ if (legacyTildeEquals) {
2384
+ effectiveConstraints = effectiveConstraints.map((c) => ({
2385
+ ...c,
2386
+ request: c.request.map(legacyTildeEqualsTransform)
2387
+ }));
2388
+ }
2389
+ const result = selectPython(effectiveConstraints, availableBuilds);
2390
+ if (result.build) {
2391
+ return { build: result.build, source };
2392
+ }
2393
+ const allResult = selectPython(effectiveConstraints, allBuilds);
2394
+ if (allResult.build) {
2395
+ const version = pythonVersionToString(allResult.build.version);
2396
+ return {
2397
+ build: defaultBuild,
2398
+ source,
2399
+ notAvailable: { build: allResult.build, version }
2400
+ };
2401
+ }
2402
+ const versionString = extractConstraintVersionString(constraints);
2403
+ return {
2404
+ build: defaultBuild,
2405
+ source,
2406
+ invalidConstraint: { versionString }
2407
+ };
2408
+ }
2409
+ function extractConstraintVersionString(constraints) {
2410
+ for (const c of constraints) {
2411
+ for (const req of c.request) {
2412
+ if (req.version?.constraint && req.version.constraint.length > 0) {
2413
+ const specs = req.version.constraint;
2414
+ if (specs.length === 1 && specs[0].operator === "==" && (!specs[0].prefix || specs[0].prefix === ".*")) {
2415
+ return specs[0].version;
2416
+ }
2417
+ return pep440ConstraintsToString(specs);
2418
+ }
2419
+ }
2420
+ }
2421
+ return "unknown";
2422
+ }
2423
+ function truncatePatchVersionsInRequest(req) {
2424
+ if (!req.version?.constraint || req.version.constraint.length === 0) {
2425
+ return req;
2426
+ }
2427
+ const newConstraints = [];
2428
+ for (const c of req.version.constraint) {
2429
+ const result = truncatePatchConstraint(c);
2430
+ if (result !== null) {
2431
+ newConstraints.push(result);
2432
+ }
2433
+ }
2434
+ const newVersion = {
2435
+ ...req.version,
2436
+ constraint: newConstraints
2437
+ };
2438
+ return { ...req, version: newVersion };
2439
+ }
2440
+ function truncatePatchConstraint(c) {
2441
+ if (c.operator === "===") {
2442
+ return c;
2443
+ }
2444
+ const parts = c.version.split(".");
2445
+ if (parts.length < 3) {
2446
+ return c;
2447
+ }
2448
+ const majorMinor = parts.slice(0, 2).join(".");
2449
+ const patch = parseInt(parts[2], 10);
2450
+ switch (c.operator) {
2451
+ case "==":
2452
+ return { operator: "==", version: majorMinor, prefix: ".*" };
2453
+ case "!=":
2454
+ return null;
2455
+ case "~=":
2456
+ return { operator: "==", version: majorMinor, prefix: ".*" };
2457
+ case ">=":
2458
+ return { operator: ">=", version: majorMinor, prefix: "" };
2459
+ case "<=":
2460
+ return { operator: "<=", version: majorMinor, prefix: "" };
2461
+ case ">":
2462
+ return patch === 0 ? { operator: ">", version: majorMinor, prefix: "" } : { operator: ">=", version: majorMinor, prefix: "" };
2463
+ case "<":
2464
+ return patch === 0 ? { operator: "<", version: majorMinor, prefix: "" } : { operator: "<=", version: majorMinor, prefix: "" };
2465
+ default:
2466
+ return c;
2467
+ }
2468
+ }
2469
+ function legacyTildeEqualsTransform(req) {
2470
+ if (!req.version?.constraint || req.version.constraint.length === 0) {
2471
+ return req;
2472
+ }
2473
+ const newConstraints = req.version.constraint.map((c) => {
2474
+ if (c.operator !== "~=")
2475
+ return c;
2476
+ const parts = c.version.split(".");
2477
+ if (parts.length === 2) {
2478
+ return { operator: "==", version: c.version, prefix: ".*" };
2479
+ }
2480
+ return c;
2481
+ });
2482
+ return {
2483
+ ...req,
2484
+ version: { ...req.version, constraint: newConstraints }
2485
+ };
2486
+ }
2286
2487
  function pythonVersionToString(version) {
2287
2488
  let str = `${version.major}.${version.minor}`;
2288
2489
  if (version.patch !== void 0) {
@@ -2294,7 +2495,7 @@ function pythonVersionToString(version) {
2294
2495
  return str;
2295
2496
  }
2296
2497
  function pep440ConstraintsToString(constraints) {
2297
- return constraints.map((c) => `${c.operator}${c.prefix}${c.version}`).join(",");
2498
+ return constraints.map((c) => `${c.operator}${c.version}${c.prefix ?? ""}`).join(",");
2298
2499
  }
2299
2500
  function implementationsMatch(buildImpl, requestImpl) {
2300
2501
  if (PythonImplementation.isUnknown(buildImpl)) {
@@ -2413,12 +2614,14 @@ export {
2413
2614
  containsAppOrHandler,
2414
2615
  createMinimalManifest,
2415
2616
  discoverPythonPackage,
2617
+ extendDistRecord,
2416
2618
  getStringConstant,
2417
2619
  isPrivatePackageSource,
2418
- normalizePackageName,
2620
+ normalizePackageName2 as normalizePackageName,
2419
2621
  parseDjangoSettingsModule,
2420
2622
  parseUvLock,
2421
2623
  scanDistributions,
2422
2624
  selectPython,
2625
+ selectPythonVersion,
2423
2626
  stringifyManifest
2424
2627
  };
@@ -101,3 +101,17 @@ export type DistributionIndex = Map<string, Distribution>;
101
101
  * @returns Map of normalized package name to distribution info
102
102
  */
103
103
  export declare function scanDistributions(sitePackagesDir: string): Promise<DistributionIndex>;
104
+ /**
105
+ * Append new file entries to a distribution's RECORD file.
106
+ *
107
+ * Finds the `.dist-info` directory for the given package, reads its RECORD,
108
+ * and appends any paths not already tracked. The caller is responsible for
109
+ * scanning directories and building the path list.
110
+ *
111
+ * @param sitePackagesDir - Absolute path to a site-packages directory
112
+ * @param packageName - Distribution name (matched via PEP 503 normalization)
113
+ * @param paths - File paths relative to site-packages to add
114
+ * @returns Count of new entries appended (skips paths already in RECORD)
115
+ * @throws If the dist-info directory or RECORD file is not found
116
+ */
117
+ export declare function extendDistRecord(sitePackagesDir: string, packageName: string, paths: string[]): Promise<number>;
@@ -20,6 +20,47 @@ export interface PythonSelectionResult {
20
20
  * @returns The first matching build, or null with errors/warnings if none match
21
21
  */
22
22
  export declare function selectPython(constraints: PythonConstraint[], available: PythonBuild[]): PythonSelectionResult;
23
+ /**
24
+ * Result of the higher-level selectPythonVersion function.
25
+ */
26
+ export interface PythonVersionSelectionResult {
27
+ /** The selected build. Falls back to defaultBuild if no constraints match. */
28
+ build: PythonBuild;
29
+ /** Source file where the constraint originated (e.g. "pyproject.toml"). */
30
+ source?: string;
31
+ /** Diagnostic indicating the constraint was found but the build isn't in availableBuilds. */
32
+ notAvailable?: {
33
+ build: PythonBuild;
34
+ /** The version string of the unavailable build. */
35
+ version: string;
36
+ };
37
+ /** Diagnostic indicating no build matches the constraint at all. */
38
+ invalidConstraint?: {
39
+ /** Human-readable version string from the constraint. */
40
+ versionString: string;
41
+ };
42
+ }
43
+ /**
44
+ * Higher-level Python version selection with two-pass matching and diagnostics.
45
+ *
46
+ * First tries to match constraints against availableBuilds. If no match,
47
+ * tries against allBuilds to produce diagnostic information. Falls back
48
+ * to defaultBuild if no constraints are provided or no match is found.
49
+ */
50
+ export declare function selectPythonVersion({ constraints, availableBuilds, allBuilds, defaultBuild, majorMinorOnly, legacyTildeEquals, }: {
51
+ constraints?: PythonConstraint[];
52
+ availableBuilds: PythonBuild[];
53
+ allBuilds: PythonBuild[];
54
+ defaultBuild: PythonBuild;
55
+ majorMinorOnly?: boolean;
56
+ /**
57
+ * When true, treat 2-part compatible-release specifiers (`~=X.Y`) as
58
+ * pinning to exactly that minor version (`==X.Y.*`) rather than the
59
+ * PEP 440 correct `>=X.Y, <(X+1).0`. This preserves the historical
60
+ * behaviour of the Python builder prior to the python-analysis migration.
61
+ */
62
+ legacyTildeEquals?: boolean;
63
+ }): PythonVersionSelectionResult;
23
64
  /**
24
65
  * Convert a PythonVersion to a string suitable for PEP 440 comparison.
25
66
  *
@@ -51,8 +51,10 @@ export interface PythonVersionRequest {
51
51
  export interface PythonConstraint {
52
52
  /** The Python version request(s) that define this constraint. */
53
53
  request: PythonRequest[];
54
- /** Human-readable description of where this constraint came from. */
54
+ /** Config file name where this constraint originated (e.g. "pyproject.toml", ".python-version"). */
55
55
  source: string;
56
+ /** Human-readable description of where this constraint came from. */
57
+ prettySource: string;
56
58
  }
57
59
  export type PythonVersion = {
58
60
  major: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/python-analysis",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",