@vercel/python-analysis 0.8.1 → 0.9.0

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
@@ -64,16 +64,17 @@ __export(src_exports, {
64
64
  UvIndexEntrySchema: () => UvIndexEntrySchema,
65
65
  classifyPackages: () => classifyPackages,
66
66
  containsAppOrHandler: () => containsAppOrHandler,
67
+ containsTopLevelCallable: () => containsTopLevelCallable,
67
68
  createMinimalManifest: () => createMinimalManifest,
68
69
  discoverPythonPackage: () => discoverPythonPackage,
69
70
  extendDistRecord: () => extendDistRecord,
70
71
  getStringConstant: () => getStringConstant,
71
72
  isPrivatePackageSource: () => isPrivatePackageSource,
72
73
  normalizePackageName: () => normalizePackageName2,
73
- parseDjangoSettingsModule: () => parseDjangoSettingsModule,
74
74
  parseUvLock: () => parseUvLock,
75
75
  scanDistributions: () => scanDistributions,
76
76
  selectPython: () => selectPython,
77
+ selectPythonVersion: () => selectPythonVersion,
77
78
  stringifyManifest: () => stringifyManifest
78
79
  });
79
80
  module.exports = __toCommonJS(src_exports);
@@ -126,17 +127,16 @@ async function containsAppOrHandler(source) {
126
127
  const mod = await importWasmModule();
127
128
  return mod.containsAppOrHandler(source);
128
129
  }
129
- async function getStringConstant(source, name) {
130
+ async function containsTopLevelCallable(source, name) {
131
+ if (!source.includes(name)) {
132
+ return false;
133
+ }
130
134
  const mod = await importWasmModule();
131
- return mod.getStringConstant(source, name) ?? null;
135
+ return mod.containsTopLevelCallable(source, name);
132
136
  }
133
- var DJANGO_SETTINGS_MODULE_PATTERN_RE = /DJANGO_SETTINGS_MODULE/;
134
- async function parseDjangoSettingsModule(content) {
135
- if (!DJANGO_SETTINGS_MODULE_PATTERN_RE.test(content)) {
136
- return null;
137
- }
137
+ async function getStringConstant(source, name) {
138
138
  const mod = await importWasmModule();
139
- return mod.parseDjangoSettingsModule(content) ?? null;
139
+ return mod.getStringConstant(source, name) ?? null;
140
140
  }
141
141
 
142
142
  // src/manifest/dist-metadata.ts
@@ -739,17 +739,24 @@ function convertPipfileToPyprojectToml(pipfile) {
739
739
  function convertPipfileLockToPyprojectToml(pipfileLock) {
740
740
  const sources = {};
741
741
  const pyproject = {};
742
+ const pythonVersion = pipfileLock._meta?.requires?.python_version;
742
743
  const deps = [];
743
744
  for (const dep of pipfileLockDepsToRequirements(pipfileLock.default || {})) {
744
745
  deps.push(formatPep508(dep));
745
746
  addDepSource(sources, dep);
746
747
  }
747
- if (deps.length > 0) {
748
- pyproject.project = {
748
+ if (deps.length > 0 || pythonVersion) {
749
+ const project = {
749
750
  name: "app",
750
- version: "0.1.0",
751
- dependencies: deps
751
+ version: "0.1.0"
752
752
  };
753
+ if (pythonVersion) {
754
+ project["requires-python"] = `==${pythonVersion}.*`;
755
+ }
756
+ if (deps.length > 0) {
757
+ project.dependencies = deps;
758
+ }
759
+ pyproject.project = project;
753
760
  }
754
761
  const devDeps = [];
755
762
  for (const dep of pipfileLockDepsToRequirements(pipfileLock.develop || {})) {
@@ -1930,8 +1937,10 @@ async function discoverPythonPackage({
1930
1937
  let workspaceManifest;
1931
1938
  let workspaceLockFile;
1932
1939
  if (manifests.length === 0) {
1940
+ const requiresPython2 = computeRequiresPython(void 0, void 0, configs);
1933
1941
  return {
1934
- configs
1942
+ configs,
1943
+ requiresPython: requiresPython2
1935
1944
  };
1936
1945
  } else {
1937
1946
  entrypointManifest = manifests[0];
@@ -1965,37 +1974,44 @@ async function discoverPythonPackage({
1965
1974
  }
1966
1975
  function computeRequiresPython(manifest, workspaceManifest, configs) {
1967
1976
  const constraints = [];
1977
+ let hasPythonVersionFile = false;
1968
1978
  for (const configSet of configs) {
1969
1979
  const pythonVersionConfig = configSet[".python-version" /* PythonVersion */];
1970
1980
  if (pythonVersionConfig !== void 0) {
1971
1981
  constraints.push({
1972
1982
  request: pythonVersionConfig.data,
1973
- source: `${pythonVersionConfig.path}`
1983
+ source: pythonVersionConfig.path,
1984
+ prettySource: pythonVersionConfig.path
1974
1985
  });
1986
+ hasPythonVersionFile = true;
1975
1987
  break;
1976
1988
  }
1977
1989
  }
1978
- const manifestRequiresPython = manifest?.data.project?.["requires-python"];
1979
- if (manifestRequiresPython) {
1980
- const parsed = (0, import_specifier.parse)(manifestRequiresPython);
1981
- if (parsed?.length) {
1982
- const request = pythonRequestFromConstraint(parsed);
1983
- constraints.push({
1984
- request: [request],
1985
- source: `"requires-python" key in ${manifest.path}`
1986
- });
1987
- }
1988
- } else {
1989
- const workspaceRequiresPython = workspaceManifest?.data.project?.["requires-python"];
1990
- if (workspaceRequiresPython) {
1991
- const parsed = (0, import_specifier.parse)(workspaceRequiresPython);
1990
+ if (!hasPythonVersionFile) {
1991
+ const manifestRequiresPython = manifest?.data.project?.["requires-python"];
1992
+ if (manifestRequiresPython) {
1993
+ const parsed = (0, import_specifier.parse)(manifestRequiresPython);
1992
1994
  if (parsed?.length) {
1993
1995
  const request = pythonRequestFromConstraint(parsed);
1994
1996
  constraints.push({
1995
1997
  request: [request],
1996
- source: `"requires-python" key in ${workspaceManifest.path}`
1998
+ source: manifest.path,
1999
+ prettySource: `"requires-python" key in ${manifest.path}`
1997
2000
  });
1998
2001
  }
2002
+ } else {
2003
+ const workspaceRequiresPython = workspaceManifest?.data.project?.["requires-python"];
2004
+ if (workspaceRequiresPython) {
2005
+ const parsed = (0, import_specifier.parse)(workspaceRequiresPython);
2006
+ if (parsed?.length) {
2007
+ const request = pythonRequestFromConstraint(parsed);
2008
+ constraints.push({
2009
+ request: [request],
2010
+ source: workspaceManifest.path,
2011
+ prettySource: `"requires-python" key in ${workspaceManifest.path}`
2012
+ });
2013
+ }
2014
+ }
1999
2015
  }
2000
2016
  }
2001
2017
  return constraints;
@@ -2407,13 +2423,15 @@ function selectPython(constraints, available) {
2407
2423
  }
2408
2424
  }
2409
2425
  if (constraintsWithMatches.length > 1) {
2410
- const sources = constraintsWithMatches.map((i) => constraints[i].source);
2426
+ const sources = constraintsWithMatches.map(
2427
+ (i) => constraints[i].prettySource
2428
+ );
2411
2429
  warnings.push(
2412
2430
  `Python version constraints may not overlap: ${sources.join(", ")}`
2413
2431
  );
2414
2432
  }
2415
2433
  }
2416
- const constraintDescriptions = constraints.map((c) => c.source).join(", ");
2434
+ const constraintDescriptions = constraints.map((c) => c.prettySource).join(", ");
2417
2435
  errors.push(
2418
2436
  `No Python build satisfies all constraints: ${constraintDescriptions}`
2419
2437
  );
@@ -2423,6 +2441,126 @@ function selectPython(constraints, available) {
2423
2441
  warnings: warnings.length > 0 ? warnings : void 0
2424
2442
  };
2425
2443
  }
2444
+ function selectPythonVersion({
2445
+ constraints,
2446
+ availableBuilds,
2447
+ allBuilds,
2448
+ defaultBuild,
2449
+ majorMinorOnly,
2450
+ legacyTildeEquals
2451
+ }) {
2452
+ const source = constraints?.[0]?.source;
2453
+ if (!constraints || constraints.length === 0) {
2454
+ return { build: defaultBuild };
2455
+ }
2456
+ let effectiveConstraints = majorMinorOnly ? constraints.map((c) => ({
2457
+ ...c,
2458
+ request: c.request.map(truncatePatchVersionsInRequest)
2459
+ })) : constraints;
2460
+ if (legacyTildeEquals) {
2461
+ effectiveConstraints = effectiveConstraints.map((c) => ({
2462
+ ...c,
2463
+ request: c.request.map(legacyTildeEqualsTransform)
2464
+ }));
2465
+ }
2466
+ const result = selectPython(effectiveConstraints, availableBuilds);
2467
+ if (result.build) {
2468
+ return { build: result.build, source };
2469
+ }
2470
+ const allResult = selectPython(effectiveConstraints, allBuilds);
2471
+ if (allResult.build) {
2472
+ const version = pythonVersionToString(allResult.build.version);
2473
+ return {
2474
+ build: defaultBuild,
2475
+ source,
2476
+ notAvailable: { build: allResult.build, version }
2477
+ };
2478
+ }
2479
+ const versionString = extractConstraintVersionString(constraints);
2480
+ return {
2481
+ build: defaultBuild,
2482
+ source,
2483
+ invalidConstraint: { versionString }
2484
+ };
2485
+ }
2486
+ function extractConstraintVersionString(constraints) {
2487
+ for (const c of constraints) {
2488
+ for (const req of c.request) {
2489
+ if (req.version?.constraint && req.version.constraint.length > 0) {
2490
+ const specs = req.version.constraint;
2491
+ if (specs.length === 1 && specs[0].operator === "==" && (!specs[0].prefix || specs[0].prefix === ".*")) {
2492
+ return specs[0].version;
2493
+ }
2494
+ return pep440ConstraintsToString(specs);
2495
+ }
2496
+ }
2497
+ }
2498
+ return "unknown";
2499
+ }
2500
+ function truncatePatchVersionsInRequest(req) {
2501
+ if (!req.version?.constraint || req.version.constraint.length === 0) {
2502
+ return req;
2503
+ }
2504
+ const newConstraints = [];
2505
+ for (const c of req.version.constraint) {
2506
+ const result = truncatePatchConstraint(c);
2507
+ if (result !== null) {
2508
+ newConstraints.push(result);
2509
+ }
2510
+ }
2511
+ const newVersion = {
2512
+ ...req.version,
2513
+ constraint: newConstraints
2514
+ };
2515
+ return { ...req, version: newVersion };
2516
+ }
2517
+ function truncatePatchConstraint(c) {
2518
+ if (c.operator === "===") {
2519
+ return c;
2520
+ }
2521
+ const parts = c.version.split(".");
2522
+ if (parts.length < 3) {
2523
+ return c;
2524
+ }
2525
+ const majorMinor = parts.slice(0, 2).join(".");
2526
+ const patch = parseInt(parts[2], 10);
2527
+ switch (c.operator) {
2528
+ case "==":
2529
+ return { operator: "==", version: majorMinor, prefix: ".*" };
2530
+ case "!=":
2531
+ return null;
2532
+ case "~=":
2533
+ return { operator: "==", version: majorMinor, prefix: ".*" };
2534
+ case ">=":
2535
+ return { operator: ">=", version: majorMinor, prefix: "" };
2536
+ case "<=":
2537
+ return { operator: "<=", version: majorMinor, prefix: "" };
2538
+ case ">":
2539
+ return patch === 0 ? { operator: ">", version: majorMinor, prefix: "" } : { operator: ">=", version: majorMinor, prefix: "" };
2540
+ case "<":
2541
+ return patch === 0 ? { operator: "<", version: majorMinor, prefix: "" } : { operator: "<=", version: majorMinor, prefix: "" };
2542
+ default:
2543
+ return c;
2544
+ }
2545
+ }
2546
+ function legacyTildeEqualsTransform(req) {
2547
+ if (!req.version?.constraint || req.version.constraint.length === 0) {
2548
+ return req;
2549
+ }
2550
+ const newConstraints = req.version.constraint.map((c) => {
2551
+ if (c.operator !== "~=")
2552
+ return c;
2553
+ const parts = c.version.split(".");
2554
+ if (parts.length === 2) {
2555
+ return { operator: "==", version: c.version, prefix: ".*" };
2556
+ }
2557
+ return c;
2558
+ });
2559
+ return {
2560
+ ...req,
2561
+ version: { ...req.version, constraint: newConstraints }
2562
+ };
2563
+ }
2426
2564
  function pythonVersionToString(version) {
2427
2565
  let str = `${version.major}.${version.minor}`;
2428
2566
  if (version.patch !== void 0) {
@@ -2434,7 +2572,7 @@ function pythonVersionToString(version) {
2434
2572
  return str;
2435
2573
  }
2436
2574
  function pep440ConstraintsToString(constraints) {
2437
- return constraints.map((c) => `${c.operator}${c.prefix}${c.version}`).join(",");
2575
+ return constraints.map((c) => `${c.operator}${c.version}${c.prefix ?? ""}`).join(",");
2438
2576
  }
2439
2577
  function implementationsMatch(buildImpl, requestImpl) {
2440
2578
  if (PythonImplementation.isUnknown(buildImpl)) {
@@ -2552,15 +2690,16 @@ var HashDigestSchema = hashDigestSchema;
2552
2690
  UvIndexEntrySchema,
2553
2691
  classifyPackages,
2554
2692
  containsAppOrHandler,
2693
+ containsTopLevelCallable,
2555
2694
  createMinimalManifest,
2556
2695
  discoverPythonPackage,
2557
2696
  extendDistRecord,
2558
2697
  getStringConstant,
2559
2698
  isPrivatePackageSource,
2560
2699
  normalizePackageName,
2561
- parseDjangoSettingsModule,
2562
2700
  parseUvLock,
2563
2701
  scanDistributions,
2564
2702
  selectPython,
2703
+ selectPythonVersion,
2565
2704
  stringifyManifest
2566
2705
  });
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module @vercel/python-analysis
8
8
  */
9
- export { containsAppOrHandler, getStringConstant, parseDjangoSettingsModule, } from './semantic/entrypoints';
9
+ export { containsAppOrHandler, containsTopLevelCallable, getStringConstant, } from './semantic/entrypoints';
10
10
  export type { Distribution, DistributionIndex, PackagePath, DirectUrlInfo, } from './manifest/dist-metadata';
11
11
  export { extendDistRecord, scanDistributions } from './manifest/dist-metadata';
12
12
  export type { PythonConfig, PythonConfigs, PythonLockFile, PythonManifest, PythonManifestOrigin, PythonPackage, PythonVersionConfig, } from './manifest/package';
@@ -14,8 +14,8 @@ export { discoverPythonPackage, PythonConfigKind, PythonLockFileKind, PythonMani
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
@@ -46,17 +46,16 @@ async function containsAppOrHandler(source) {
46
46
  const mod = await importWasmModule();
47
47
  return mod.containsAppOrHandler(source);
48
48
  }
49
- async function getStringConstant(source, name) {
49
+ async function containsTopLevelCallable(source, name) {
50
+ if (!source.includes(name)) {
51
+ return false;
52
+ }
50
53
  const mod = await importWasmModule();
51
- return mod.getStringConstant(source, name) ?? null;
54
+ return mod.containsTopLevelCallable(source, name);
52
55
  }
53
- var DJANGO_SETTINGS_MODULE_PATTERN_RE = /DJANGO_SETTINGS_MODULE/;
54
- async function parseDjangoSettingsModule(content) {
55
- if (!DJANGO_SETTINGS_MODULE_PATTERN_RE.test(content)) {
56
- return null;
57
- }
56
+ async function getStringConstant(source, name) {
58
57
  const mod = await importWasmModule();
59
- return mod.parseDjangoSettingsModule(content) ?? null;
58
+ return mod.getStringConstant(source, name) ?? null;
60
59
  }
61
60
 
62
61
  // src/manifest/dist-metadata.ts
@@ -662,17 +661,24 @@ function convertPipfileToPyprojectToml(pipfile) {
662
661
  function convertPipfileLockToPyprojectToml(pipfileLock) {
663
662
  const sources = {};
664
663
  const pyproject = {};
664
+ const pythonVersion = pipfileLock._meta?.requires?.python_version;
665
665
  const deps = [];
666
666
  for (const dep of pipfileLockDepsToRequirements(pipfileLock.default || {})) {
667
667
  deps.push(formatPep508(dep));
668
668
  addDepSource(sources, dep);
669
669
  }
670
- if (deps.length > 0) {
671
- pyproject.project = {
670
+ if (deps.length > 0 || pythonVersion) {
671
+ const project = {
672
672
  name: "app",
673
- version: "0.1.0",
674
- dependencies: deps
673
+ version: "0.1.0"
675
674
  };
675
+ if (pythonVersion) {
676
+ project["requires-python"] = `==${pythonVersion}.*`;
677
+ }
678
+ if (deps.length > 0) {
679
+ project.dependencies = deps;
680
+ }
681
+ pyproject.project = project;
676
682
  }
677
683
  const devDeps = [];
678
684
  for (const dep of pipfileLockDepsToRequirements(pipfileLock.develop || {})) {
@@ -1853,8 +1859,10 @@ async function discoverPythonPackage({
1853
1859
  let workspaceManifest;
1854
1860
  let workspaceLockFile;
1855
1861
  if (manifests.length === 0) {
1862
+ const requiresPython2 = computeRequiresPython(void 0, void 0, configs);
1856
1863
  return {
1857
- configs
1864
+ configs,
1865
+ requiresPython: requiresPython2
1858
1866
  };
1859
1867
  } else {
1860
1868
  entrypointManifest = manifests[0];
@@ -1888,37 +1896,44 @@ async function discoverPythonPackage({
1888
1896
  }
1889
1897
  function computeRequiresPython(manifest, workspaceManifest, configs) {
1890
1898
  const constraints = [];
1899
+ let hasPythonVersionFile = false;
1891
1900
  for (const configSet of configs) {
1892
1901
  const pythonVersionConfig = configSet[".python-version" /* PythonVersion */];
1893
1902
  if (pythonVersionConfig !== void 0) {
1894
1903
  constraints.push({
1895
1904
  request: pythonVersionConfig.data,
1896
- source: `${pythonVersionConfig.path}`
1905
+ source: pythonVersionConfig.path,
1906
+ prettySource: pythonVersionConfig.path
1897
1907
  });
1908
+ hasPythonVersionFile = true;
1898
1909
  break;
1899
1910
  }
1900
1911
  }
1901
- const manifestRequiresPython = manifest?.data.project?.["requires-python"];
1902
- if (manifestRequiresPython) {
1903
- const parsed = parse2(manifestRequiresPython);
1904
- if (parsed?.length) {
1905
- const request = pythonRequestFromConstraint(parsed);
1906
- constraints.push({
1907
- request: [request],
1908
- source: `"requires-python" key in ${manifest.path}`
1909
- });
1910
- }
1911
- } else {
1912
- const workspaceRequiresPython = workspaceManifest?.data.project?.["requires-python"];
1913
- if (workspaceRequiresPython) {
1914
- const parsed = parse2(workspaceRequiresPython);
1912
+ if (!hasPythonVersionFile) {
1913
+ const manifestRequiresPython = manifest?.data.project?.["requires-python"];
1914
+ if (manifestRequiresPython) {
1915
+ const parsed = parse2(manifestRequiresPython);
1915
1916
  if (parsed?.length) {
1916
1917
  const request = pythonRequestFromConstraint(parsed);
1917
1918
  constraints.push({
1918
1919
  request: [request],
1919
- source: `"requires-python" key in ${workspaceManifest.path}`
1920
+ source: manifest.path,
1921
+ prettySource: `"requires-python" key in ${manifest.path}`
1920
1922
  });
1921
1923
  }
1924
+ } else {
1925
+ const workspaceRequiresPython = workspaceManifest?.data.project?.["requires-python"];
1926
+ if (workspaceRequiresPython) {
1927
+ const parsed = parse2(workspaceRequiresPython);
1928
+ if (parsed?.length) {
1929
+ const request = pythonRequestFromConstraint(parsed);
1930
+ constraints.push({
1931
+ request: [request],
1932
+ source: workspaceManifest.path,
1933
+ prettySource: `"requires-python" key in ${workspaceManifest.path}`
1934
+ });
1935
+ }
1936
+ }
1922
1937
  }
1923
1938
  }
1924
1939
  return constraints;
@@ -2330,13 +2345,15 @@ function selectPython(constraints, available) {
2330
2345
  }
2331
2346
  }
2332
2347
  if (constraintsWithMatches.length > 1) {
2333
- const sources = constraintsWithMatches.map((i) => constraints[i].source);
2348
+ const sources = constraintsWithMatches.map(
2349
+ (i) => constraints[i].prettySource
2350
+ );
2334
2351
  warnings.push(
2335
2352
  `Python version constraints may not overlap: ${sources.join(", ")}`
2336
2353
  );
2337
2354
  }
2338
2355
  }
2339
- const constraintDescriptions = constraints.map((c) => c.source).join(", ");
2356
+ const constraintDescriptions = constraints.map((c) => c.prettySource).join(", ");
2340
2357
  errors.push(
2341
2358
  `No Python build satisfies all constraints: ${constraintDescriptions}`
2342
2359
  );
@@ -2346,6 +2363,126 @@ function selectPython(constraints, available) {
2346
2363
  warnings: warnings.length > 0 ? warnings : void 0
2347
2364
  };
2348
2365
  }
2366
+ function selectPythonVersion({
2367
+ constraints,
2368
+ availableBuilds,
2369
+ allBuilds,
2370
+ defaultBuild,
2371
+ majorMinorOnly,
2372
+ legacyTildeEquals
2373
+ }) {
2374
+ const source = constraints?.[0]?.source;
2375
+ if (!constraints || constraints.length === 0) {
2376
+ return { build: defaultBuild };
2377
+ }
2378
+ let effectiveConstraints = majorMinorOnly ? constraints.map((c) => ({
2379
+ ...c,
2380
+ request: c.request.map(truncatePatchVersionsInRequest)
2381
+ })) : constraints;
2382
+ if (legacyTildeEquals) {
2383
+ effectiveConstraints = effectiveConstraints.map((c) => ({
2384
+ ...c,
2385
+ request: c.request.map(legacyTildeEqualsTransform)
2386
+ }));
2387
+ }
2388
+ const result = selectPython(effectiveConstraints, availableBuilds);
2389
+ if (result.build) {
2390
+ return { build: result.build, source };
2391
+ }
2392
+ const allResult = selectPython(effectiveConstraints, allBuilds);
2393
+ if (allResult.build) {
2394
+ const version = pythonVersionToString(allResult.build.version);
2395
+ return {
2396
+ build: defaultBuild,
2397
+ source,
2398
+ notAvailable: { build: allResult.build, version }
2399
+ };
2400
+ }
2401
+ const versionString = extractConstraintVersionString(constraints);
2402
+ return {
2403
+ build: defaultBuild,
2404
+ source,
2405
+ invalidConstraint: { versionString }
2406
+ };
2407
+ }
2408
+ function extractConstraintVersionString(constraints) {
2409
+ for (const c of constraints) {
2410
+ for (const req of c.request) {
2411
+ if (req.version?.constraint && req.version.constraint.length > 0) {
2412
+ const specs = req.version.constraint;
2413
+ if (specs.length === 1 && specs[0].operator === "==" && (!specs[0].prefix || specs[0].prefix === ".*")) {
2414
+ return specs[0].version;
2415
+ }
2416
+ return pep440ConstraintsToString(specs);
2417
+ }
2418
+ }
2419
+ }
2420
+ return "unknown";
2421
+ }
2422
+ function truncatePatchVersionsInRequest(req) {
2423
+ if (!req.version?.constraint || req.version.constraint.length === 0) {
2424
+ return req;
2425
+ }
2426
+ const newConstraints = [];
2427
+ for (const c of req.version.constraint) {
2428
+ const result = truncatePatchConstraint(c);
2429
+ if (result !== null) {
2430
+ newConstraints.push(result);
2431
+ }
2432
+ }
2433
+ const newVersion = {
2434
+ ...req.version,
2435
+ constraint: newConstraints
2436
+ };
2437
+ return { ...req, version: newVersion };
2438
+ }
2439
+ function truncatePatchConstraint(c) {
2440
+ if (c.operator === "===") {
2441
+ return c;
2442
+ }
2443
+ const parts = c.version.split(".");
2444
+ if (parts.length < 3) {
2445
+ return c;
2446
+ }
2447
+ const majorMinor = parts.slice(0, 2).join(".");
2448
+ const patch = parseInt(parts[2], 10);
2449
+ switch (c.operator) {
2450
+ case "==":
2451
+ return { operator: "==", version: majorMinor, prefix: ".*" };
2452
+ case "!=":
2453
+ return null;
2454
+ case "~=":
2455
+ return { operator: "==", version: majorMinor, prefix: ".*" };
2456
+ case ">=":
2457
+ return { operator: ">=", version: majorMinor, prefix: "" };
2458
+ case "<=":
2459
+ return { operator: "<=", version: majorMinor, prefix: "" };
2460
+ case ">":
2461
+ return patch === 0 ? { operator: ">", version: majorMinor, prefix: "" } : { operator: ">=", version: majorMinor, prefix: "" };
2462
+ case "<":
2463
+ return patch === 0 ? { operator: "<", version: majorMinor, prefix: "" } : { operator: "<=", version: majorMinor, prefix: "" };
2464
+ default:
2465
+ return c;
2466
+ }
2467
+ }
2468
+ function legacyTildeEqualsTransform(req) {
2469
+ if (!req.version?.constraint || req.version.constraint.length === 0) {
2470
+ return req;
2471
+ }
2472
+ const newConstraints = req.version.constraint.map((c) => {
2473
+ if (c.operator !== "~=")
2474
+ return c;
2475
+ const parts = c.version.split(".");
2476
+ if (parts.length === 2) {
2477
+ return { operator: "==", version: c.version, prefix: ".*" };
2478
+ }
2479
+ return c;
2480
+ });
2481
+ return {
2482
+ ...req,
2483
+ version: { ...req.version, constraint: newConstraints }
2484
+ };
2485
+ }
2349
2486
  function pythonVersionToString(version) {
2350
2487
  let str = `${version.major}.${version.minor}`;
2351
2488
  if (version.patch !== void 0) {
@@ -2357,7 +2494,7 @@ function pythonVersionToString(version) {
2357
2494
  return str;
2358
2495
  }
2359
2496
  function pep440ConstraintsToString(constraints) {
2360
- return constraints.map((c) => `${c.operator}${c.prefix}${c.version}`).join(",");
2497
+ return constraints.map((c) => `${c.operator}${c.version}${c.prefix ?? ""}`).join(",");
2361
2498
  }
2362
2499
  function implementationsMatch(buildImpl, requestImpl) {
2363
2500
  if (PythonImplementation.isUnknown(buildImpl)) {
@@ -2474,15 +2611,16 @@ export {
2474
2611
  UvIndexEntrySchema,
2475
2612
  classifyPackages,
2476
2613
  containsAppOrHandler,
2614
+ containsTopLevelCallable,
2477
2615
  createMinimalManifest,
2478
2616
  discoverPythonPackage,
2479
2617
  extendDistRecord,
2480
2618
  getStringConstant,
2481
2619
  isPrivatePackageSource,
2482
2620
  normalizePackageName2 as normalizePackageName,
2483
- parseDjangoSettingsModule,
2484
2621
  parseUvLock,
2485
2622
  scanDistributions,
2486
2623
  selectPython,
2624
+ selectPythonVersion,
2487
2625
  stringifyManifest
2488
2626
  };
@@ -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;