@styx-api/core 0.5.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@styx-api/core",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Styx compiler core: parses CLI tool descriptors (e.g. Boutiques), optimizes an IR, solves typed parameter bindings, and generates type-safe wrappers (TypeScript, Python, JSON Schema). Part of the Styx/NiWrap ecosystem.",
5
5
  "keywords": [
6
6
  "styx",
@@ -72,17 +72,23 @@ function licenseField(proj: ProjectMeta): string {
72
72
  }
73
73
 
74
74
  /**
75
- * Per-suite `pyproject.toml`. The flat layout (`python/<pkg>/bet.py`) makes the
76
- * directory itself the importable package, so setuptools' `package-dir` maps the
77
- * import name (`<pkg>`) onto the distribution's root directory. The styxdefs
75
+ * Per-suite `pyproject.toml`. The suite source stays in a flat directory
76
+ * (`python/<pkg>/bet.py`), but setuptools' `package-dir` maps that directory onto
77
+ * the dotted import package `<project>.<pkg>`, so every suite nests under the
78
+ * metapackage's `<project>/` namespace. That restores `from <project> import
79
+ * <pkg>` while keeping each suite a separately-installable distribution (and
80
+ * leaves no top-level `<pkg>` polluting the global namespace). With no project
81
+ * name the import name is the bare `<pkg>` (top-level fallback). The styxdefs
78
82
  * floor is the only runtime dependency.
79
83
  *
80
- * Precondition: `pkg.name` must be a valid Python identifier - the flat layout's
81
- * relative imports (`from .bet import *`) already require this, and the CLI uses
82
- * it verbatim as the directory name, so this stays consistent with that.
84
+ * Precondition: `importName` is a valid (possibly dotted) Python package path -
85
+ * the caller scrubs the project + package names into identifiers accordingly.
83
86
  */
84
- export function generateSubPyproject(proj: ProjectMeta, pkg: PackageMeta): string {
85
- const importName = pkgDir(pkg);
87
+ export function generateSubPyproject(
88
+ proj: ProjectMeta,
89
+ pkg: PackageMeta,
90
+ importName: string,
91
+ ): string {
86
92
  const cb = new CodeBuilder(" ");
87
93
  cb.line("[project]");
88
94
  cb.line(`name = "${tomlStr(pyDistName(proj, pkg))}"`);
@@ -133,14 +139,28 @@ export function generateRootInitPy(): string {
133
139
 
134
140
  /**
135
141
  * Root `pyproject.toml`: a metapackage depending on `styxkit[all]` (the runner
136
- * stack it re-exports) plus each per-suite distribution. `packages` lists only
137
- * the metapackage's own module so setuptools ships the styxkit re-export without
138
- * sweeping the sibling suite directories into this distribution.
142
+ * stack it re-exports) plus each per-suite distribution, pinned to this exact
143
+ * project version (`niwrap_fsl==1.2.3`). The suites all ride the project version
144
+ * and ship in lockstep, so an exact pin keeps `pip install niwrap==X` consistent:
145
+ * during the post-release index-propagation window (or any momentary registry
146
+ * inconsistency) pip errors cleanly instead of silently grafting an older suite
147
+ * whose layout no longer matches the metapackage. Mirrors the way the CLI pins
148
+ * `@styx-api/core` exactly.
149
+ *
150
+ * `packages` lists only the metapackage's own module so setuptools ships the
151
+ * styxkit re-export without sweeping the sibling suite directories into this
152
+ * distribution. The module is the `<project>/` namespace package the suites nest
153
+ * into, so installing the metapackage makes `<project>.use_docker()` reachable
154
+ * alongside the suites' `from <project> import <pkg>`. `moduleDir` is the on-disk
155
+ * source directory; it differs from the import `moduleName` only when a suite is
156
+ * named after the project, in which case a `package-dir` remap keeps the import
157
+ * name intact.
139
158
  */
140
159
  export function generateRootPyproject(
141
160
  proj: ProjectMeta,
142
161
  distNames: string[],
143
162
  moduleName: string,
163
+ moduleDir: string,
144
164
  ): string {
145
165
  const cb = new CodeBuilder(" ");
146
166
  cb.line("[project]");
@@ -153,11 +173,17 @@ export function generateRootPyproject(
153
173
  cb.line(`requires-python = "${REQUIRES_PYTHON}"`);
154
174
  cb.line("dependencies = [");
155
175
  for (const dep of PYTHON_RUNNER_DEPS) cb.line(` "${dep}",`);
156
- for (const dist of distNames) cb.line(` "${tomlStr(dist)}",`);
176
+ // Pin each suite to the exact project version (they ship in lockstep) so a
177
+ // mid-propagation install can't mix a new metapackage with an old suite.
178
+ const suitePin = tomlStr(proj.version ?? "0.0.0");
179
+ for (const dist of distNames) cb.line(` "${tomlStr(dist)}==${suitePin}",`);
157
180
  cb.line("]");
158
181
  cb.blank();
159
182
  cb.line("[tool.setuptools]");
160
183
  cb.line(`packages = ["${moduleName}"]`);
184
+ if (moduleDir !== moduleName) {
185
+ cb.line(`package-dir = { "${moduleName}" = "${moduleDir}" }`);
186
+ }
161
187
  cb.blank();
162
188
  cb.line("[tool.setuptools.package-data]");
163
189
  cb.line(`"${moduleName}" = ["py.typed"]`);
@@ -547,6 +547,14 @@ export class PythonBackend implements Backend {
547
547
  const pkgDirs: string[] = [];
548
548
  const warnings: EmitWarning[] = [];
549
549
 
550
+ // The metapackage's importable module doubles as the namespace every suite
551
+ // nests under (`<project>.<suite>`), so `from <project> import <suite>` and
552
+ // `<project>.use_docker()` both resolve from one shared `<project>/` package.
553
+ // Scrub the project name to a valid, non-keyword identifier; with no project
554
+ // name there is no namespace and suites stay top-level (matching the bare
555
+ // distribution-name fallback in `pyDistName`).
556
+ const nsName = proj.name && proj.name.trim() ? pyScrubIdent(proj.name, PY_RESERVED) : undefined;
557
+
550
558
  for (const p of packages) {
551
559
  const pkg = p.meta ?? {};
552
560
  // Mirror the CLI's `pkgDir` fallback so a nameless package's source dir
@@ -554,27 +562,31 @@ export class PythonBackend implements Backend {
554
562
  const dir = pkg.name ?? "package";
555
563
  pkgDirs.push(dir);
556
564
  distNames.push(pyDistName(proj, pkg));
557
- files.set(`${dir}/pyproject.toml`, generateSubPyproject(proj, pkg));
565
+ // Import package: `<project>.<suite>` so it shares the metapackage's
566
+ // namespace; setuptools maps the flat suite dir onto this dotted path.
567
+ const importPkg = nsName ? `${nsName}.${dir}` : dir;
568
+ files.set(`${dir}/pyproject.toml`, generateSubPyproject(proj, pkg, importPkg));
558
569
  files.set(`${dir}/README.md`, generateSubReadme(proj, pkg));
559
570
  }
560
571
 
561
- // The metapackage ships its own importable module: a thin styxkit re-export
562
- // so `import <project>; <project>.use_docker()` works (PEP 561 typed). Scrub
563
- // the project name to a valid, non-keyword identifier, then dodge any suite
564
- // directory so the stub never clobbers a suite's real wrapper module.
565
- const rawMod = proj.name && proj.name.trim() ? proj.name : "project";
566
- let rootMod = pyScrubIdent(rawMod, PY_RESERVED);
567
- if (pkgDirs.includes(rootMod)) {
568
- const collided = rootMod;
569
- while (pkgDirs.includes(rootMod)) rootMod += "_";
572
+ // The metapackage ships `<project>/__init__.py` (a thin styxkit re-export so
573
+ // `<project>.use_docker()` works, PEP 561 typed) into the same `<project>/`
574
+ // package the suites nest under. Its on-disk source dir must dodge a suite
575
+ // named after the project (both would write `<project>/`); the import name
576
+ // stays `<project>` via a `package-dir` remap so the namespace is preserved.
577
+ const metaImport = nsName ?? "project";
578
+ let metaDir = metaImport;
579
+ if (pkgDirs.includes(metaDir)) {
580
+ const collided = metaDir;
581
+ while (pkgDirs.includes(metaDir)) metaDir += "_";
570
582
  warnings.push({
571
- message: `metapackage module "${collided}" collides with a suite directory; emitting as "${rootMod}" instead`,
583
+ message: `metapackage directory "${collided}" collides with a suite directory; emitting its sources in "${metaDir}" instead (import name stays "${metaImport}")`,
572
584
  });
573
585
  }
574
- files.set(`${rootMod}/__init__.py`, generateRootInitPy());
575
- files.set(`${rootMod}/py.typed`, "");
586
+ files.set(`${metaDir}/__init__.py`, generateRootInitPy());
587
+ files.set(`${metaDir}/py.typed`, "");
576
588
 
577
- files.set("pyproject.toml", generateRootPyproject(proj, distNames, rootMod));
589
+ files.set("pyproject.toml", generateRootPyproject(proj, distNames, metaImport, metaDir));
578
590
  files.set("README.md", generateRootReadme(proj, distNames));
579
591
  files.set("requirements.txt", generateRequirementsTxt(pkgDirs));
580
592