pyfig 0.6.3 → 0.7.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.
Files changed (44) hide show
  1. package/README.md +46 -33
  2. package/SECURITY.md +1 -1
  3. package/dist/cjs/index.js +396 -256
  4. package/dist/cjs/index.js.map +10 -9
  5. package/dist/cjs/sync-resolver.js +356 -231
  6. package/dist/cjs/sync-resolver.js.map +9 -8
  7. package/dist/cli.js +12 -3
  8. package/dist/cli.js.map +1 -1
  9. package/dist/client.d.ts +6 -6
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +31 -21
  12. package/dist/client.js.map +1 -1
  13. package/dist/config.d.ts +9 -2
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +48 -1
  16. package/dist/config.js.map +1 -1
  17. package/dist/index.d.ts +3 -4
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/package-target.d.ts +3 -3
  22. package/dist/package-target.d.ts.map +1 -1
  23. package/dist/package-target.js +6 -6
  24. package/dist/package-target.js.map +1 -1
  25. package/dist/provision.d.ts +6 -12
  26. package/dist/provision.d.ts.map +1 -1
  27. package/dist/provision.js +141 -153
  28. package/dist/provision.js.map +1 -1
  29. package/dist/python-venv.d.ts +12 -0
  30. package/dist/python-venv.d.ts.map +1 -0
  31. package/dist/{virtual-environment.js → python-venv.js} +12 -9
  32. package/dist/python-venv.js.map +1 -0
  33. package/dist/runners.js +12 -12
  34. package/dist/runners.js.map +1 -1
  35. package/dist/sync-resolver.js +2 -2
  36. package/dist/sync-resolver.js.map +1 -1
  37. package/dist/venv-identity.d.ts +36 -0
  38. package/dist/venv-identity.d.ts.map +1 -0
  39. package/dist/venv-identity.js +80 -0
  40. package/dist/venv-identity.js.map +1 -0
  41. package/package.json +2 -2
  42. package/dist/virtual-environment.d.ts +0 -11
  43. package/dist/virtual-environment.d.ts.map +0 -1
  44. package/dist/virtual-environment.js.map +0 -1
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # pyfig
2
2
 
3
- `pyfig` prepares uv and a compatible Python virtual environment from Bun or Node.js. It prefers a valid uv already on the system, reuses a compatible venv, and searches system Python with downloads disabled before considering any managed Python download.
3
+ `pyfig` prepares uv and compatible Python venvs from Bun or Node.js. It prefers a valid uv already on the system and searches system Python with downloads disabled before considering a managed Python download.
4
4
 
5
- Pyfig never invokes standalone `pip`, never writes `pyproject.toml` or requirements files, and never creates a project-root `.venv`. When no compatible venv exists, its private environment is `<project-root>/node_modules/.pyfig/venv`.
5
+ Pyfig never invokes standalone `pip`, discovers Python project metadata, edits caller-owned requirements files, or creates a project-root `.venv`. Dependency-free callers can reuse a compatible active or project-root venv. A caller that passes dependencies or requirements files gets a content-addressed private venv under `<project-root>/node_modules/.pyfig/venvs/`, so independent libraries cannot replace or downgrade each other's environments.
6
6
 
7
7
  ## Install
8
8
 
@@ -13,31 +13,31 @@ bun add pyfig
13
13
  ## Use
14
14
 
15
15
  ```ts
16
- import { createPyfig } from 'pyfig';
16
+ import { Python } from 'pyfig';
17
17
 
18
- const pyfig = createPyfig({
18
+ const pyenv = new Python({
19
+ dependencies: ['requests>=2.32'],
20
+ requirements: ['requirements/runtime.txt'],
19
21
  requiresPython: '>=3.11,<4',
20
22
  requiresUv: '>=0.10,<1',
21
23
  requireEmbedding: true,
22
24
  requireGil: true,
23
25
  });
24
26
 
25
- const environment = await pyfig.prepare(
26
- ['requests>=2.32'],
27
- { extras: ['database'] },
28
- );
27
+ const environment = await pyenv.prepare();
29
28
 
30
29
  console.log(environment.uv);
31
30
  console.log(environment.python);
32
31
  console.log(environment.managed);
32
+ console.log(environment.venvId);
33
33
 
34
- await pyfig.uv(
34
+ await pyenv.uv(
35
35
  ['run', '--', 'python', 'script.py'],
36
36
  { stdio: 'inherit' },
37
37
  );
38
38
  ```
39
39
 
40
- `prepare()` accepts PEP 508 dependency specifiers plus optional `extras`, `groups`, or `allExtras`. If the project has a `pyproject.toml`, uv resolves that read-only input together with a temporary dependency file. Pyfig installs the resolved result with `uv pip install -r`, which preserves unrelated packages. Temporary inputs are created outside the repository and removed afterward.
40
+ `dependencies` accepts the requirement syntax uv supports, including PEP 508 specifiers, Git references, URLs, and local file references. `requirements` accepts explicit requirements-file paths, with relative paths resolved from `projectRoot`. Pyfig validates each value as one line and otherwise passes it to uv without interpreting package or source syntax. It does not discover or read `pyproject.toml`, Python lockfiles, or dependency groups. Configuration is bound by the `Python` constructor, so `prepare()`, `uv()`, and `uvSync()` always target the same venv. Temporary resolution inputs are created outside the repository and removed afterward.
41
41
 
42
42
  The same uv-only surface handles direct commands:
43
43
 
@@ -62,12 +62,13 @@ Pyfig uses this order:
62
62
 
63
63
  1. A compatible configured or `PATH` uv.
64
64
  2. Pyfig's cached, checksum-verified uv.
65
- 3. A compatible explicitly configured venv, active `VIRTUAL_ENV`, root `.venv`, or Pyfig-owned venv.
66
- 4. Every installed system Python reported by uv, with downloads disabled, until one passes all capability checks.
65
+ 3. For callers without dependencies or requirements files, a compatible explicitly configured venv, active `VIRTUAL_ENV`, or root `.venv`.
66
+ 4. A compatible base interpreter from those venvs, then every installed system Python reported by uv with downloads disabled.
67
67
  5. An already-cached uv-managed Python that passes the same checks.
68
68
  6. A managed Python download, only when installation policy allows it.
69
+ 7. An existing hashed venv for the exact interpreter and resolved dependencies, or a new venv below `node_modules/.pyfig/venvs`.
69
70
 
70
- An explicit `python` option is strict: an unavailable or incompatible interpreter is an error. Incompatible external venvs are preserved and skipped. Only an incompatible environment under `node_modules/.pyfig/venv` may be rebuilt.
71
+ An explicit `python` option is strict: an unavailable or incompatible interpreter is an error. Without runtime requirements, an explicitly configured compatible venv can be reused. With dependencies or requirements files, its base interpreter is used for a private venv. Incompatible external venvs are always preserved. Only an incomplete or damaged Pyfig-managed venv may be rebuilt.
71
72
 
72
73
  ![Pyfig prefers existing uv, compatible venvs, and system Python before managed downloads](docs/diagrams/python-selection.png)
73
74
 
@@ -78,29 +79,38 @@ Native consumers can set `requireEmbedding: true` to require CPython, `Python.h`
78
79
  ## Runtime dependency resolution
79
80
 
80
81
  ```ts
81
- await pyfig.prepare(
82
- ['orjson>=3.10', 'httpx[http2]'],
83
- {
84
- extras: ['postgres'],
85
- groups: ['runtime'],
86
- },
87
- );
82
+ const pyenv = new Python({
83
+ dependencies: ['orjson>=3.10', 'httpx[http2]'],
84
+ requirements: ['requirements/runtime.txt'],
85
+ });
86
+
87
+ await pyenv.prepare();
88
88
  ```
89
89
 
90
90
  Resolution follows the equivalent of:
91
91
 
92
92
  ```sh
93
- uv pip compile pyproject.toml /temporary/runtime-dependencies.txt \
94
- --extra postgres --group runtime \
95
- --python /selected/venv/python \
93
+ uv pip compile /project/requirements/runtime.txt \
94
+ /temporary/runtime-dependencies.txt \
95
+ --python /selected/base/python \
96
+ --no-header --no-annotate --generate-hashes \
96
97
  --output-file /temporary/resolved-dependencies.txt
97
98
 
99
+ uv venv --python /selected/base/python \
100
+ node_modules/.pyfig/venvs/cpython-3.12.7-0123456789abcdef0123
101
+
98
102
  uv pip install \
99
- --python /selected/venv/python \
103
+ --python /hashed/venv/python \
100
104
  -r /temporary/resolved-dependencies.txt
101
105
  ```
102
106
 
103
- When no `pyproject.toml` exists, only the temporary dependency input is compiled. Extras and dependency groups require an existing `pyproject.toml`. Pyfig does not use `uv add` or `uv pip sync`: the former would edit project metadata, while the latter could remove unrelated packages from the venv.
107
+ The venv identity includes the exact base interpreter, platform, architecture, GIL/ABI properties, and SHA-256 of uv's complete compiled resolution. This includes every artifact hash emitted by `uv pip compile --generate-hashes`. uv also resolves Git references to commit hashes. Pyfig does not duplicate uv's dependency parser or invent hashes for source types uv cannot hash. Equivalent resolutions can share one venv; a changed artifact hash, Git commit, or other resolved output creates another. The metadata file inside each venv is written last and acts as its publication marker.
108
+
109
+ Only the explicit `dependencies` values and `requirements` files participate in resolution. Known Python project metadata and lockfile paths are rejected as requirements inputs. Pyfig does not use `uv add` or `uv pip sync`, so it neither edits project metadata nor removes packages from an externally owned venv.
110
+
111
+ Raw `pyenv.uv(['pip', ...])` remains an intentional low-level escape hatch. It can mutate the selected venv after publication and bypass the content-addressing guarantee. Use constructor-level `dependencies` or `requirements` when isolation and repeatability matter.
112
+
113
+ Separate venvs work for subprocesses and isolated workers. Native libraries that initialize CPython in the same JavaScript process must use one combined compatible dependency set or move each interpreter into a separate worker process; two venv directories cannot provide two independent process-global CPython runtimes.
104
114
 
105
115
  ![Runtime dependencies are resolved in temporary files before installation](docs/diagrams/runtime-dependencies.png)
106
116
 
@@ -108,7 +118,7 @@ When no `pyproject.toml` exists, only the temporary dependency input is compiled
108
118
 
109
119
  ## Project root
110
120
 
111
- Pyfig does not interpret `package.json`, workspace metadata, `pyproject.toml`, or lockfiles to choose ownership. Root precedence is:
121
+ Pyfig does not interpret `package.json`, workspace metadata, `pyproject.toml`, or lockfiles. The root only locates private venv storage and resolves relative requirements-file paths. Root precedence is:
112
122
 
113
123
  1. `projectRoot`
114
124
  2. `PYFIG_PROJECT_ROOT`
@@ -121,12 +131,12 @@ Set `projectRoot` explicitly in reusable libraries and unusual workspace layouts
121
131
 
122
132
  | Export | Responsibility |
123
133
  | --- | --- |
124
- | `createPyfig(options)` | Create a scoped client with memoized base preparation. |
125
- | `client.prepare(dependencies?, options?)` | Select a venv and optionally resolve/install runtime dependencies. |
126
- | `client.pythonInfo()` | Return the selected interpreter and embedding metadata. |
127
- | `client.uvInfo()` | Return the selected uv executable and version. |
128
- | `client.uv()` / `client.uvSync()` | Spawn uv and pin pip/run commands to the selected venv. |
129
- | `ensureEnvironment(options?, dependencies?, dependencyOptions?)` | Standalone preparation API. |
134
+ | `new Python(options)` | Bind one immutable venv configuration and memoize its preparation. |
135
+ | `pyenv.prepare()` | Resolve and prepare that configuration's exact venv. |
136
+ | `pyenv.pythonInfo()` | Return the selected interpreter and embedding metadata. |
137
+ | `pyenv.uvInfo()` | Return the selected uv executable and version. |
138
+ | `pyenv.uv()` / `pyenv.uvSync()` | Spawn uv and pin pip/run commands to the same venv. |
139
+ | `ensurePython(options?)` | Standalone Python preparation API using the same options. |
130
140
  | `ensureUv(options?)` | Prefer existing uv, otherwise bootstrap verified uv when allowed. |
131
141
  | `uvSpawn()` / `uvSpawnSync()` | Standalone uv spawn API. |
132
142
  | `resolveProjectRoot(start?)` | Resolve the nearest Git root without reading manifests. |
@@ -143,6 +153,8 @@ When embedding files are available, `PythonInfo.embedding.stableAbiLibraryPath`
143
153
 
144
154
  | API option | Environment variable | Default |
145
155
  | --- | --- | --- |
156
+ | `dependencies` | — | `[]` |
157
+ | `requirements` | — | `[]` |
146
158
  | `python` | `PYFIG_PYTHON` | uv discovery |
147
159
  | `requiresPython` | `PYFIG_REQUIRES_PYTHON` | `>=3.10,<4` |
148
160
  | `managedPython` | `PYFIG_MANAGED_PYTHON` | `3.12` |
@@ -169,6 +181,7 @@ bunx --bun pyfig
169
181
  bunx --bun pyfig --check
170
182
  bunx --bun pyfig --requires-python '>=3.12,<3.14'
171
183
  bunx --bun pyfig --requires-uv '>=0.12,<1'
184
+ bunx --bun pyfig --dependency 'requests>=2.32' --requirement requirements/runtime.txt
172
185
  bunx --bun pyfig --require-embedding --require-gil
173
186
  bunx --bun pyfig --project-root /path/to/repository
174
187
  bunx --bun pyfig --json
@@ -180,7 +193,7 @@ bunx --bun pyfig --json
180
193
 
181
194
  Pyfig resolves a compatible uv release from Astral's GitHub releases, downloads that exact archive over HTTPS, verifies its adjacent SHA-256 checksum, validates the staged executable version, and atomically publishes it in Pyfig's cache. A compatible cached executable is probed locally without a network request. Release metadata is cached on disk for 24 hours; stale metadata is refreshed under a Loqkit lock and remains available as a fallback when GitHub's API is temporarily unavailable. Set `GITHUB_TOKEN` or `GH_TOKEN` to authenticate release discovery on CI and other rate-limited hosts. Pyfig does not execute installer scripts or modify shell profiles.
182
195
 
183
- Provisioning mutations use Loqkit's layered in-memory, worker, native-file-lock-when-available, and portable-file coordination with capability checks before and after acquisition. Compatible managed uv executables are cached by resolved version; Loqkit coordinates both the `requiresUv` policy lookup and exact release publication. uv supplies its own target-environment lock for installs. Base client preparation is memoized; dependency preparation is intentionally rerun so uv can cheaply verify current inputs through its shared cache.
196
+ Provisioning mutations use Loqkit's layered in-memory, worker, native-file-lock-when-available, and portable-file coordination with capability checks before and after acquisition. Compatible managed uv executables are cached by resolved version; Loqkit coordinates both the `requiresUv` policy lookup and exact release publication. Each hashed venv has its own provisioning key. Creation, installation, and validation finish before the venv metadata marker is published. Preparation is memoized within one `pyenv`; separate processes converge through the same resolved identity and Loqkit lock.
184
197
 
185
198
  ## Platform support
186
199
 
package/SECURITY.md CHANGED
@@ -4,6 +4,6 @@ Pyfig can download Astral uv release archives and can ask uv to download Python
4
4
 
5
5
  Pyfig resolves a uv release allowed by `requiresUv`, then fetches that exact release URL over HTTPS with bounded redirects and timeouts. GitHub release metadata is cached for 24 hours under Loqkit coordination, with validated stale metadata available if refresh fails. Pyfig verifies the adjacent SHA-256 checksum, validates the staged executable's reported version, and atomically publishes it into its cache. It never executes an installer script or modifies shell profiles or the host `PATH`. Proxy variables are honored through a per-request dispatcher without logging proxy credentials.
6
6
 
7
- Provisioning uses in-process, worker, optional native advisory, and portable filesystem coordination followed by a post-lock capability check. Runtime dependency mutation uses uv's target-environment lock. Pyfig pins uv commands to the selected venv and removes configuration values that could redirect them. It reads an existing `pyproject.toml` only as a resolver input and never writes project manifests or requirements files.
7
+ Provisioning uses in-process, worker, optional native advisory, and portable filesystem coordination followed by a post-lock capability check. Runtime dependency mutation uses uv's target-environment lock. Pyfig pins uv commands to the selected venv and removes configuration values that could redirect them. It resolves only caller-provided dependency specifiers and requirements files; it does not discover or read Python project metadata. Caller-owned requirements files are read-only.
8
8
 
9
9
  Executable paths, project roots, version specifiers, package specifications, indexes, and proxy settings remain trusted configuration. Do not let an untrusted user choose `python`, `PYFIG_UV`, `cacheDir`, package arguments, or related environment variables in a privileged process. A checksum fetched from the same release protects transfer integrity, not a compromised upstream publisher.