pyfig 0.4.0 → 0.5.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.
@@ -74,7 +74,7 @@ var import_node_os = __toESM(require("node:os"), 1);
74
74
  var import_node_path5 = __toESM(require("node:path"), 1);
75
75
  var import_node_stream = require("node:stream");
76
76
  var import_promises2 = require("node:stream/promises");
77
- var import_extract_zip = __toESM(require("extract-zip"), 1);
77
+ var import_fflate = require("fflate");
78
78
  var tar = __toESM(require("tar"), 1);
79
79
  var import_undici = require("undici");
80
80
 
@@ -105,8 +105,20 @@ function* ancestors(startDirectory) {
105
105
  }
106
106
  }
107
107
 
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
+
108
120
  // src/config.ts
109
- var DEFAULT_PYTHON_RANGE = ">=3.10.0 <4.0.0";
121
+ var DEFAULT_REQUIRES_PYTHON = ">=3.10,<4";
110
122
  var DEFAULT_MANAGED_PYTHON = "3.12";
111
123
  var DEFAULT_UV_VERSION = "0.13.6";
112
124
  var DEFAULT_UV_RANGE = ">=0.10.0 <1.0.0";
@@ -122,13 +134,13 @@ var envPathsFunction = (() => {
122
134
  })();
123
135
  function resolveOptions(options = {}) {
124
136
  const env = { ...options.env ?? process.env };
125
- const pythonRange = options.pythonRange ?? env["PYFIG_PYTHON_RANGE"] ?? DEFAULT_PYTHON_RANGE;
137
+ const requiresPython = options.requiresPython ?? env["PYFIG_REQUIRES_PYTHON"] ?? DEFAULT_REQUIRES_PYTHON;
126
138
  const uvRange = options.uvRange ?? env["PYFIG_UV_RANGE"] ?? DEFAULT_UV_RANGE;
127
- assertRange("Python", pythonRange);
139
+ assertPythonSpecifier(requiresPython);
128
140
  assertRange("uv", uvRange);
129
141
  const managedPython = options.managedPython ?? env["PYFIG_MANAGED_PYTHON"] ?? DEFAULT_MANAGED_PYTHON;
130
142
  const uvVersion = options.uvVersion ?? env["PYFIG_UV_VERSION"] ?? DEFAULT_UV_VERSION;
131
- assertManagedPythonRequest(managedPython, pythonRange);
143
+ assertManagedPythonRequest(managedPython, requiresPython);
132
144
  assertUvVersion(uvVersion, uvRange);
133
145
  const probeTimeoutMs = resolveDuration("probeTimeoutMs", options.probeTimeoutMs, env["PYFIG_PROBE_TIMEOUT_MS"], DEFAULT_PROBE_TIMEOUT_MS);
134
146
  const installTimeoutMs = resolveDuration("installTimeoutMs", options.installTimeoutMs, env["PYFIG_INSTALL_TIMEOUT_MS"], DEFAULT_INSTALL_TIMEOUT_MS);
@@ -141,7 +153,7 @@ function resolveOptions(options = {}) {
141
153
  const autoInstall = options.autoInstall ?? parseBoolean(env["PYFIG_AUTO_INSTALL"], true);
142
154
  return {
143
155
  python: options.python ?? env["PYFIG_PYTHON"] ?? "",
144
- pythonRange,
156
+ requiresPython,
145
157
  managedPython,
146
158
  uvVersion,
147
159
  uvRange,
@@ -182,14 +194,19 @@ function assertUvVersion(version, range) {
182
194
  throw new TypeError(`Pinned uv ${version} does not satisfy supported range ${range}.`);
183
195
  }
184
196
  }
185
- function assertManagedPythonRequest(request, pythonRange) {
197
+ function assertPythonSpecifier(specifier) {
198
+ if (!isValidPythonSpecifier(specifier)) {
199
+ throw new TypeError(`Invalid requires-python PEP 440 specifier: ${specifier}`);
200
+ }
201
+ }
202
+ function assertManagedPythonRequest(request, requiresPython) {
186
203
  if (!request || /[\\/\0\r\n]/u.test(request) || request.includes("..")) {
187
204
  throw new TypeError(`Invalid managed Python request: ${request}`);
188
205
  }
189
206
  if (/^\d+\.\d+(?:\.\d+)?$/u.test(request)) {
190
207
  const normalized = request.split(".").length === 2 ? `${request}.0` : request;
191
- if (!import_semver.default.satisfies(normalized, pythonRange)) {
192
- throw new TypeError(`Managed Python ${request} cannot satisfy configured range ${pythonRange}.`);
208
+ if (!pythonVersionSatisfies(normalized, requiresPython)) {
209
+ throw new TypeError(`Managed Python ${request} cannot satisfy requires-python ${requiresPython}.`);
193
210
  }
194
211
  }
195
212
  }
@@ -497,7 +514,15 @@ if os.path.isfile(header_path) and library_path:
497
514
  }
498
515
 
499
516
  release_level = sys.version_info.releaselevel
500
- release_suffix = "" if release_level == "final" else f"-{release_level}.{sys.version_info.serial}"
517
+ release_suffixes = {
518
+ "alpha": "a",
519
+ "beta": "b",
520
+ "candidate": "rc",
521
+ "final": "",
522
+ }
523
+ release_suffix = release_suffixes[release_level]
524
+ if release_suffix:
525
+ release_suffix += str(sys.version_info.serial)
501
526
 
502
527
  print(json.dumps({
503
528
  "executable": sys.executable,
@@ -517,8 +542,8 @@ print(json.dumps({
517
542
  "embedding": embedding,
518
543
  }))
519
544
  `;
520
- function pythonSatisfiesRequirements(python, options) {
521
- return import_semver2.default.satisfies(python.version, options.pythonRange) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
545
+ 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);
522
547
  }
523
548
  async function probePython(candidate, options) {
524
549
  const executable = await resolveExecutable(candidate.executable, options.env);
@@ -527,7 +552,7 @@ async function probePython(candidate, options) {
527
552
  try {
528
553
  const { stdout } = run(executable, [...candidate.args, "-c", PYTHON_PROBE], options.env, options.probeTimeoutMs);
529
554
  const info = JSON.parse(stdout.trim());
530
- return import_semver2.default.valid(info.version) ? info : null;
555
+ return isValidPythonVersion(info.version) ? info : null;
531
556
  } catch {
532
557
  return null;
533
558
  }
@@ -570,14 +595,14 @@ function pythonArchitectureMatchesRuntime(python) {
570
595
  async function ensureUv(options = {}) {
571
596
  return ensureUvResolved(resolveOptions(options));
572
597
  }
573
- async function ensureEnvironment(options = {}, requirements = [], dependencyOptions = {}) {
598
+ async function ensureEnvironment(options = {}, dependencies = [], dependencyOptions = {}) {
574
599
  const resolved = resolveOptions(options);
575
- const normalized = normalizeRequirements(requirements);
600
+ const normalized = normalizeDependencies(dependencies);
576
601
  const selections = normalizeDependencyOptions(dependencyOptions);
577
602
  const uv = await ensureUvResolved(resolved);
578
603
  const python = await ensureVirtualEnvironmentResolved(resolved, uv);
579
604
  if (normalized.length > 0 || selections.extras.length > 0 || selections.groups.length > 0 || selections.allExtras) {
580
- await installRuntimeRequirements(uv, python, normalized, selections, resolved);
605
+ await installRuntimeDependencies(uv, python, normalized, selections, resolved);
581
606
  }
582
607
  return {
583
608
  python,
@@ -592,7 +617,7 @@ async function ensureVirtualEnvironmentResolved(options, uv) {
592
617
  const directory = virtualEnvironmentDirectory(options.projectRoot);
593
618
  const check = async () => {
594
619
  const candidate = await inspectVirtualEnvironment(directory, options);
595
- return candidate && pythonSatisfiesRequirements(candidate, options) && await virtualEnvironmentMatchesConfiguredPython(candidate, options) ? candidate : null;
620
+ return candidate && pythonSatisfiesPolicy(candidate, options) && await virtualEnvironmentMatchesConfiguredPython(candidate, options) ? candidate : null;
596
621
  };
597
622
  if (!options.createVirtualEnvironment) {
598
623
  throw new Error(`No compatible environment is available and creation is disabled. ` + `Pyfig would create ${directory}.`);
@@ -625,7 +650,7 @@ async function ensureVirtualEnvironmentResolved(options, uv) {
625
650
  async function findReusableEnvironment(options) {
626
651
  if (options.python) {
627
652
  const configured = await probePython({ executable: options.python, args: [] }, options);
628
- if (!configured || !pythonSatisfiesRequirements(configured, options)) {
653
+ if (!configured || !pythonSatisfiesPolicy(configured, options)) {
629
654
  throw incompatibleConfiguredPython(options);
630
655
  }
631
656
  if (configured.virtualEnvironment)
@@ -636,7 +661,7 @@ async function findReusableEnvironment(options) {
636
661
  virtualEnvironmentDirectory(options.projectRoot)
637
662
  ]) {
638
663
  const info = await inspectVirtualEnvironment(directory, options);
639
- if (info && pythonSatisfiesRequirements(info, options) && await virtualEnvironmentMatchesConfiguredPython(info, options)) {
664
+ if (info && pythonSatisfiesPolicy(info, options) && await virtualEnvironmentMatchesConfiguredPython(info, options)) {
640
665
  return info;
641
666
  }
642
667
  }
@@ -645,13 +670,13 @@ async function findReusableEnvironment(options) {
645
670
  async function ensureBasePythonResolved(uv, options) {
646
671
  if (options.python) {
647
672
  const configured = await probePython({ executable: options.python, args: [] }, options);
648
- if (!configured || !pythonSatisfiesRequirements(configured, options)) {
673
+ if (!configured || !pythonSatisfiesPolicy(configured, options)) {
649
674
  throw incompatibleConfiguredPython(options);
650
675
  }
651
676
  if (!configured.virtualEnvironment)
652
677
  return configured;
653
678
  const base = await probePython({ executable: configured.baseExecutable, args: [] }, options);
654
- if (base && pythonSatisfiesRequirements(base, options))
679
+ if (base && pythonSatisfiesPolicy(base, options))
655
680
  return base;
656
681
  throw incompatibleConfiguredPython(options);
657
682
  }
@@ -693,7 +718,7 @@ async function findPythonWithUv(uv, options, source) {
693
718
  try {
694
719
  const { stdout } = await runAsync(uv.executable, args, explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
695
720
  const info = await probePython({ executable: stdout.trim(), args: [] }, options);
696
- if (info && pythonSatisfiesRequirements(info, options))
721
+ if (info && pythonSatisfiesPolicy(info, options))
697
722
  return info;
698
723
  } catch {}
699
724
  }
@@ -711,7 +736,7 @@ async function findPythonWithUv(uv, options, source) {
711
736
  const candidates = installedPythonPaths(stdout);
712
737
  for (const executable of candidates) {
713
738
  const info = await probePython({ executable, args: [] }, options);
714
- if (info && !info.virtualEnvironment && pythonSatisfiesRequirements(info, options)) {
739
+ if (info && !info.virtualEnvironment && pythonSatisfiesPolicy(info, options)) {
715
740
  return info;
716
741
  }
717
742
  }
@@ -735,7 +760,7 @@ async function inspectVirtualEnvironment(directory, options) {
735
760
  const info = await probePython({ executable, args: [] }, options);
736
761
  return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
737
762
  }
738
- async function installRuntimeRequirements(uv, python, requirements, dependencyOptions, options) {
763
+ async function installRuntimeDependencies(uv, python, dependencies, dependencyOptions, options) {
739
764
  if (!python.virtualEnvironment) {
740
765
  throw new Error("Runtime dependency installation requires a virtual environment.");
741
766
  }
@@ -744,18 +769,18 @@ async function installRuntimeRequirements(uv, python, requirements, dependencyOp
744
769
  timeoutMs: options.coordinationTimeoutMs,
745
770
  check: async () => null,
746
771
  mutate: async () => {
747
- const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-requirements-"));
772
+ const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-dependencies-"));
748
773
  try {
749
- const additions = import_node_path5.default.join(temporary, "runtime-requirements.txt");
750
- const resolved = import_node_path5.default.join(temporary, "resolved-requirements.txt");
774
+ const additions = import_node_path5.default.join(temporary, "runtime-dependencies.txt");
775
+ const resolved = import_node_path5.default.join(temporary, "resolved-dependencies.txt");
751
776
  const pyproject = import_node_path5.default.join(options.projectRoot, "pyproject.toml");
752
777
  const hasPyproject = import_node_fs4.default.existsSync(pyproject);
753
778
  if (!hasPyproject && (dependencyOptions.extras.length > 0 || dependencyOptions.groups.length > 0 || dependencyOptions.allExtras)) {
754
779
  throw new Error("Runtime extras and groups require an existing pyproject.toml.");
755
780
  }
756
781
  const sources = hasPyproject ? [pyproject] : [];
757
- if (requirements.length > 0) {
758
- await import_promises.writeFile(additions, `${requirements.join(`
782
+ if (dependencies.length > 0) {
783
+ await import_promises.writeFile(additions, `${dependencies.join(`
759
784
  `)}
760
785
  `, "utf8");
761
786
  sources.push(additions);
@@ -814,11 +839,11 @@ async function findAvailableUv(options) {
814
839
  const managed = probeUv(uvExecutable(options), options);
815
840
  return managed?.version === options.uvVersion ? managed : null;
816
841
  }
817
- function normalizeRequirements(requirements) {
818
- const normalized = requirements.map((requirement) => requirement.trim());
819
- for (const requirement of normalized) {
820
- if (!requirement || /[\0\r\n]/u.test(requirement)) {
821
- throw new TypeError(`Invalid runtime requirement: ${JSON.stringify(requirement)}`);
842
+ function normalizeDependencies(dependencies) {
843
+ const normalized = dependencies.map((dependency) => dependency.trim());
844
+ for (const dependency of normalized) {
845
+ if (!dependency || /[\0\r\n]/u.test(dependency)) {
846
+ throw new TypeError(`Invalid runtime dependency: ${JSON.stringify(dependency)}`);
822
847
  }
823
848
  }
824
849
  return [...new Set(normalized)];
@@ -855,17 +880,17 @@ function targetUvEnvironment(options, python) {
855
880
  return uvCommandEnvironment(uvEnvironment(options), python);
856
881
  }
857
882
  function incompatibleConfiguredPython(options) {
858
- return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` + `${options.pythonRange}${options.requireEmbedding ? " with CPython embedding files" : ""}.`);
883
+ return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` + `requires-python ${options.requiresPython}` + `${options.requireEmbedding ? " with CPython embedding files" : ""}.`);
859
884
  }
860
885
  function missingPythonMessage(options, reason) {
861
886
  const embedding = options.requireEmbedding ? " with compatible CPython headers, library, architecture, and GIL ABI" : "";
862
- return `No Python satisfies ${options.pythonRange}${embedding}; ${reason}.`;
887
+ return `No Python satisfies requires-python ${options.requiresPython}${embedding}; ${reason}.`;
863
888
  }
864
889
  async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
865
890
  if (!options.python)
866
891
  return true;
867
892
  const configured = await probePython({ executable: options.python, args: [] }, options);
868
- if (!configured || !pythonSatisfiesRequirements(configured, options))
893
+ if (!configured || !pythonSatisfiesPolicy(configured, options))
869
894
  return false;
870
895
  if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
871
896
  return true;
@@ -894,11 +919,7 @@ async function installUv(installDir, options) {
894
919
  }
895
920
  const extracted = import_node_path5.default.join(temporary, "extracted");
896
921
  await import_promises.mkdir(extracted, { recursive: true });
897
- if (asset.endsWith(".zip"))
898
- await import_extract_zip.default(archive, { dir: extracted });
899
- else
900
- await tar.x({ file: archive, cwd: extracted, strict: true });
901
- const source = await findFile(extracted, process.platform === "win32" ? "uv.exe" : "uv");
922
+ const source = await extractUvExecutable(archive, asset, extracted);
902
923
  if (!source)
903
924
  throw new Error(`uv archive did not contain the expected executable: ${asset}`);
904
925
  await publishUvAtomically(source, installDir, options);
@@ -907,6 +928,22 @@ async function installUv(installDir, options) {
907
928
  await import_promises.rm(temporary, { recursive: true, force: true });
908
929
  }
909
930
  }
931
+ async function extractUvExecutable(archive, asset, destination) {
932
+ const executableName = process.platform === "win32" ? "uv.exe" : "uv";
933
+ if (!asset.endsWith(".zip")) {
934
+ await tar.x({ file: archive, cwd: destination, strict: true });
935
+ return findFile(destination, executableName);
936
+ }
937
+ const entries = import_fflate.unzipSync(await import_promises.readFile(archive), {
938
+ filter: ({ name }) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName
939
+ });
940
+ const matches = Object.entries(entries).filter(([name]) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName);
941
+ if (matches.length !== 1 || !matches[0])
942
+ return null;
943
+ const source = import_node_path5.default.join(destination, executableName);
944
+ await import_promises.writeFile(source, matches[0][1], { flag: "wx" });
945
+ return source;
946
+ }
910
947
  async function publishUvAtomically(source, installDir, options) {
911
948
  await import_promises.mkdir(installDir, { recursive: true });
912
949
  const executable = uvExecutable(options);
@@ -1008,4 +1045,4 @@ main().catch((error) => {
1008
1045
  process.exitCode = 1;
1009
1046
  });
1010
1047
 
1011
- //# debugId=450BD20294AA111B64756E2164756E21
1048
+ //# debugId=8D6E1E9C1BFA227064756E2164756E21