@adhd/apigen-python-env 0.2.3 → 0.2.4

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/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 0.2.4 (2026-08-08)
2
+
3
+
4
+ ### 🩹 Fixes
5
+
6
+ - **apigen-python-env:** probe monorepo-live python source before co-located dist copy (DEBT-APIGEN-010)
7
+
8
+
9
+ ### ❤️ Thank You
10
+
11
+ - pseudosky
12
+
1
13
  ## 0.2.3 (2026-08-07)
2
14
 
3
15
 
package/index.js CHANGED
@@ -1,10 +1,10 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const D=require("node:fs"),I=require("node:os"),q=require("node:path"),F=require("node:crypto"),h=require("node:child_process");function P(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t){for(const r in t)if(r!=="default"){const i=Object.getOwnPropertyDescriptor(t,r);Object.defineProperty(e,r,i.get?i:{enumerable:!0,get:()=>t[r]})}}return e.default=t,Object.freeze(e)}const n=P(D),b=P(I),o=P(q);function k(t){const e=t??__dirname,r=o.join(e,"python");if(n.existsSync(o.join(r,"apigen_python")))return r;const i=o.resolve(e,"..","..","..","python");if(n.existsSync(o.join(i,"apigen_python")))return i;let a=e;for(let c=0;c<20;c++){const p=o.join(a,"packages","apigen","python");if(n.existsSync(o.join(p,"apigen_python")))return p;const u=o.dirname(a);if(u===a)break;a=u}return i}const H=["python3.13","python3.12","python3.11","python3"],m=[3,11];function J(t){const e=h.spawnSync(t,["-c",'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")'],{encoding:"utf8",timeout:15e3});if(e.status!==0||!e.stdout)return;const[r,i]=e.stdout.trim().split(".").map(Number);if(!(!Number.isFinite(r)||!Number.isFinite(i)))return[r,i]}function M(){for(const t of H){const e=J(t);if(e&&(e[0]>m[0]||e[0]===m[0]&&e[1]>=m[1]))return t}throw new Error(`apigen-python-env: no Python >= ${m.join(".")} found on PATH (tried: ${H.join(", ")}). Install Python ${m.join(".")}+ or set APIGEN_PYTHON to a provisioned interpreter.`)}function Y(t){return process.platform==="win32"?o.join(t,"Scripts","python.exe"):o.join(t,"bin","python3")}function G(t){const e=n.readFileSync(o.join(t,"pyproject.toml"));return F.createHash("sha256").update(e).digest("hex")}function O(t){try{const e=JSON.parse(n.readFileSync(t,"utf8"));if(typeof e.pyprojectHash=="string"&&Array.isArray(e.extras))return e}catch{}}function B(t,e,r){const i=Date.now()+e;for(;;)try{n.mkdirSync(t,{recursive:!1});break}catch{if(Date.now()>i){try{n.rmdirSync(t)}catch{}continue}h.spawnSync(process.execPath,["-e","setTimeout(()=>{}, 250)"],{timeout:5e3})}try{return r()}finally{try{n.rmdirSync(t)}catch{}}}function V(t={}){const e=k(),r=process.env.APIGEN_PYTHON;if(r){if(!n.existsSync(r))throw new Error(`apigen-python-env: APIGEN_PYTHON points to a missing file: ${r}`);return{python:r,venvDir:"",pythonPkgDir:e}}const i=[...new Set(t.extras??[])].sort(),a=t.envHome??process.env.APIGEN_PYENV_HOME??o.join(b.homedir(),".adhd","apigen"),c=o.join(a,"pyvenv"),p=o.join(c,".apigen-stamp"),u=G(e),y=Y(c),l=()=>{if(!n.existsSync(y))return!1;const s=O(p);return!s||s.pyprojectHash!==u?!1:i.every(d=>s.extras.includes(d))};if(l())return{python:y,venvDir:c,pythonPkgDir:e};n.mkdirSync(a,{recursive:!0});const v=o.join(a,"pyvenv.lock"),w=t.lockTimeoutMs??3e5;return B(v,w,()=>{if(l())return{python:y,venvDir:c,pythonPkgDir:e};const s=O(p),d=[...new Set([...(s==null?void 0:s.extras)??[],...i])].sort();n.rmSync(c,{recursive:!0,force:!0});const _=M(),j=h.spawnSync(_,["-m","venv",c],{encoding:"utf8",timeout:12e4});if(j.status!==0)throw new Error(`apigen-python-env: "${_} -m venv" failed (exit ${j.status}):
2
- ${j.stderr}`);const x=o.join(e,"build");n.existsSync(x)&&n.rmSync(x,{recursive:!0,force:!0});for(const f of n.readdirSync(e))f.endsWith(".egg-info")&&n.rmSync(o.join(e,f),{recursive:!0,force:!0});const S=n.mkdtempSync(o.join(b.tmpdir(),"apigen-python-wheel-"));try{const f=h.spawnSync(y,["-m","pip","wheel","--no-deps","--wheel-dir",S,e],{encoding:"utf8",timeout:6e5});if(f.status!==0)throw new Error(`apigen-python-env: pip wheel failed (exit ${f.status}):
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const D=require("node:fs"),I=require("node:os"),q=require("node:path"),F=require("node:crypto"),h=require("node:child_process");function P(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t){for(const n in t)if(n!=="default"){const i=Object.getOwnPropertyDescriptor(t,n);Object.defineProperty(e,n,i.get?i:{enumerable:!0,get:()=>t[n]})}}return e.default=t,Object.freeze(e)}const r=P(D),b=P(I),o=P(q);function k(t){const e=t??__dirname,n=o.resolve(e,"..","..","..","python");if(r.existsSync(o.join(n,"apigen_python")))return n;const i=o.join(e,"python");if(r.existsSync(o.join(i,"apigen_python")))return i;let a=e;for(let c=0;c<20;c++){const p=o.join(a,"packages","apigen","python");if(r.existsSync(o.join(p,"apigen_python")))return p;const u=o.dirname(a);if(u===a)break;a=u}return n}const H=["python3.13","python3.12","python3.11","python3"],m=[3,11];function J(t){const e=h.spawnSync(t,["-c",'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")'],{encoding:"utf8",timeout:15e3});if(e.status!==0||!e.stdout)return;const[n,i]=e.stdout.trim().split(".").map(Number);if(!(!Number.isFinite(n)||!Number.isFinite(i)))return[n,i]}function M(){for(const t of H){const e=J(t);if(e&&(e[0]>m[0]||e[0]===m[0]&&e[1]>=m[1]))return t}throw new Error(`apigen-python-env: no Python >= ${m.join(".")} found on PATH (tried: ${H.join(", ")}). Install Python ${m.join(".")}+ or set APIGEN_PYTHON to a provisioned interpreter.`)}function Y(t){return process.platform==="win32"?o.join(t,"Scripts","python.exe"):o.join(t,"bin","python3")}function G(t){const e=r.readFileSync(o.join(t,"pyproject.toml"));return F.createHash("sha256").update(e).digest("hex")}function O(t){try{const e=JSON.parse(r.readFileSync(t,"utf8"));if(typeof e.pyprojectHash=="string"&&Array.isArray(e.extras))return e}catch{}}function B(t,e,n){const i=Date.now()+e;for(;;)try{r.mkdirSync(t,{recursive:!1});break}catch{if(Date.now()>i){try{r.rmdirSync(t)}catch{}continue}h.spawnSync(process.execPath,["-e","setTimeout(()=>{}, 250)"],{timeout:5e3})}try{return n()}finally{try{r.rmdirSync(t)}catch{}}}function V(t={}){const e=k(),n=process.env.APIGEN_PYTHON;if(n){if(!r.existsSync(n))throw new Error(`apigen-python-env: APIGEN_PYTHON points to a missing file: ${n}`);return{python:n,venvDir:"",pythonPkgDir:e}}const i=[...new Set(t.extras??[])].sort(),a=t.envHome??process.env.APIGEN_PYENV_HOME??o.join(b.homedir(),".adhd","apigen"),c=o.join(a,"pyvenv"),p=o.join(c,".apigen-stamp"),u=G(e),y=Y(c),l=()=>{if(!r.existsSync(y))return!1;const s=O(p);return!s||s.pyprojectHash!==u?!1:i.every(d=>s.extras.includes(d))};if(l())return{python:y,venvDir:c,pythonPkgDir:e};r.mkdirSync(a,{recursive:!0});const v=o.join(a,"pyvenv.lock"),w=t.lockTimeoutMs??3e5;return B(v,w,()=>{if(l())return{python:y,venvDir:c,pythonPkgDir:e};const s=O(p),d=[...new Set([...(s==null?void 0:s.extras)??[],...i])].sort();r.rmSync(c,{recursive:!0,force:!0});const _=M(),j=h.spawnSync(_,["-m","venv",c],{encoding:"utf8",timeout:12e4});if(j.status!==0)throw new Error(`apigen-python-env: "${_} -m venv" failed (exit ${j.status}):
2
+ ${j.stderr}`);const x=o.join(e,"build");r.existsSync(x)&&r.rmSync(x,{recursive:!0,force:!0});for(const f of r.readdirSync(e))f.endsWith(".egg-info")&&r.rmSync(o.join(e,f),{recursive:!0,force:!0});const S=r.mkdtempSync(o.join(b.tmpdir(),"apigen-python-wheel-"));try{const f=h.spawnSync(y,["-m","pip","wheel","--no-deps","--wheel-dir",S,e],{encoding:"utf8",timeout:6e5});if(f.status!==0)throw new Error(`apigen-python-env: pip wheel failed (exit ${f.status}):
3
3
  ${f.stderr}
4
4
  stdout:
5
- ${f.stdout}`);const E=n.readdirSync(S).filter(A=>A.endsWith(".whl"));if(E.length===0)throw new Error("apigen-python-env: pip wheel produced no .whl file");const $=o.join(S,E[0]),N=d.length?`${$}[${d.join(",")}]`:$,g=h.spawnSync(y,["-m","pip","install","--quiet","--disable-pip-version-check",N],{encoding:"utf8",timeout:6e5});if(g.status!==0)throw new Error(`apigen-python-env: pip install "${N}" failed (exit ${g.status}):
5
+ ${f.stdout}`);const E=r.readdirSync(S).filter(A=>A.endsWith(".whl"));if(E.length===0)throw new Error("apigen-python-env: pip wheel produced no .whl file");const $=o.join(S,E[0]),N=d.length?`${$}[${d.join(",")}]`:$,g=h.spawnSync(y,["-m","pip","install","--quiet","--disable-pip-version-check",N],{encoding:"utf8",timeout:6e5});if(g.status!==0)throw new Error(`apigen-python-env: pip install "${N}" failed (exit ${g.status}):
6
6
  ${g.stderr}
7
7
  stdout:
8
- ${g.stdout}`)}finally{n.rmSync(S,{recursive:!0,force:!0})}const T={pyprojectHash:u,extras:d};return n.writeFileSync(p,JSON.stringify(T)),{python:y,venvDir:c,pythonPkgDir:e}})}function L(t,e,r,i,a){return new Promise((c,p)=>{var v,w;const u=h.spawn(t,["-m","apigen_python.extractor",r,"--namespace",i,"--emit-json"],{cwd:e,env:{...process.env,PYTHONPATH:e},stdio:["ignore","pipe","pipe"]});let y="",l="";(v=u.stdout)==null||v.on("data",s=>{y+=s.toString("utf8")}),(w=u.stderr)==null||w.on("data",s=>{l+=s.toString("utf8")}),u.on("error",p),u.on("exit",s=>{if(s!==0){p(new Error(`${a}: extractor --emit-json exited with code ${s}:
8
+ ${g.stdout}`)}finally{r.rmSync(S,{recursive:!0,force:!0})}const T={pyprojectHash:u,extras:d};return r.writeFileSync(p,JSON.stringify(T)),{python:y,venvDir:c,pythonPkgDir:e}})}function L(t,e,n,i,a){return new Promise((c,p)=>{var v,w;const u=h.spawn(t,["-m","apigen_python.extractor",n,"--namespace",i,"--emit-json"],{cwd:e,env:{...process.env,PYTHONPATH:e},stdio:["ignore","pipe","pipe"]});let y="",l="";(v=u.stdout)==null||v.on("data",s=>{y+=s.toString("utf8")}),(w=u.stderr)==null||w.on("data",s=>{l+=s.toString("utf8")}),u.on("error",p),u.on("exit",s=>{if(s!==0){p(new Error(`${a}: extractor --emit-json exited with code ${s}:
9
9
  ${l}`));return}try{c(JSON.parse(y))}catch(d){p(new Error(`${a}: extractor --emit-json produced invalid JSON (${d.message}):
10
10
  ${y}`))}})})}exports.ensurePythonEnv=V;exports.resolvePythonPkgDir=k;exports.runExtractorEmitJson=L;
package/index.mjs CHANGED
@@ -4,10 +4,10 @@ import * as r from "node:path";
4
4
  import { createHash as T } from "node:crypto";
5
5
  import { spawnSync as l, spawn as I } from "node:child_process";
6
6
  function O(e) {
7
- const t = e ?? __dirname, o = r.join(t, "python");
7
+ const t = e ?? __dirname, o = r.resolve(t, "..", "..", "..", "python");
8
8
  if (n.existsSync(r.join(o, "apigen_python")))
9
9
  return o;
10
- const s = r.resolve(t, "..", "..", "..", "python");
10
+ const s = r.join(t, "python");
11
11
  if (n.existsSync(r.join(s, "apigen_python")))
12
12
  return s;
13
13
  let a = t;
@@ -20,7 +20,7 @@ function O(e) {
20
20
  break;
21
21
  a = u;
22
22
  }
23
- return s;
23
+ return o;
24
24
  }
25
25
  const _ = ["python3.13", "python3.12", "python3.11", "python3"], m = [3, 11];
26
26
  function D(e) {
@@ -27,17 +27,27 @@ export interface IEnsurePythonEnvOptions {
27
27
  * Locate the directory containing the `apigen_python` package sources.
28
28
  *
29
29
  * Probed in order:
30
- * 1. Co-located (SHIPPED) copy: `<this file's dir>/python`. This package's
31
- * build copies `packages/apigen/python/apigen_python/**` into its own
32
- * `dist/python/apigen_python/`, so for a consumer installed from npm
33
- * OUTSIDE the monorepo (where __dirname is `node_modules/@adhd/
34
- * apigen-python-env/dist`), the sources are bundled right next to the
35
- * compiled code. This MUST be probed first so published consumers never
36
- * fall through to a monorepo-only path that doesn't exist for them.
37
- * 2. Monorepo source (dev-time fallback): `<this file>/../../../python`
38
- * (source layout: packages/apigen/python-env/src/lib → packages/apigen/
39
- * python). Keeps vitest/ts-node runs resolving the LIVE source instead
40
- * of the copied snapshot.
30
+ * 1. Monorepo source (LIVE, preferred when present): `<this file>/../../
31
+ * ../python` (source layout: packages/apigen/python-env/src/lib →
32
+ * packages/apigen/python; from a build, `dist/lib` → `packages/apigen/
33
+ * python` by the same three-levels-up relationship). When both this
34
+ * AND the co-located dist copy below exist — i.e. running inside the
35
+ * monorepo, built or not — this MUST win. `dist/python` is a
36
+ * point-in-time copy made by this package's build (vite.config.ts
37
+ * `copyPythonPackagePlugin`); it goes stale the moment someone edits
38
+ * `packages/apigen/python` without rebuilding, and code run outside
39
+ * Nx's task graph (`npx vitest run` directly, bypassing the `test`
40
+ * target's `dependsOn: ["build"]`) has no mechanism to refresh it. Prior
41
+ * ordering probed the dist copy FIRST, so any monorepo dev/test
42
+ * invocation that happened to have a `dist/python` lying around from a
43
+ * previous build silently exercised stale Python source with no
44
+ * indication the live edit was never observed — see
45
+ * DEBT-APIGEN-010. Source must win whenever it's reachable.
46
+ * 2. Co-located (SHIPPED) copy: `<this file's dir>/python`. For a
47
+ * consumer installed from npm OUTSIDE the monorepo (where __dirname is
48
+ * `node_modules/@adhd/apigen-python-env/dist`), step 1's monorepo-
49
+ * relative path doesn't exist, so this bundled-alongside-the-compiled-
50
+ * code copy is the only candidate and is used.
41
51
  * 3. Walk up from __dirname looking for `packages/apigen/python` (covers
42
52
  * the vite-bundled CLI, where __dirname is the bundle output dir).
43
53
  * (Shared by py-grpc / py-flask — previously duplicated in each plugin.)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhd/apigen-python-env",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "main": "./index.js",
5
5
  "module": "./index.mjs",
6
6
  "types": "./index.d.ts",