@styx-api/core 0.5.0 → 0.5.1

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.1",
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))}"`);
@@ -135,12 +141,18 @@ export function generateRootInitPy(): string {
135
141
  * Root `pyproject.toml`: a metapackage depending on `styxkit[all]` (the runner
136
142
  * stack it re-exports) plus each per-suite distribution. `packages` lists only
137
143
  * the metapackage's own module so setuptools ships the styxkit re-export without
138
- * sweeping the sibling suite directories into this distribution.
144
+ * sweeping the sibling suite directories into this distribution. The module is
145
+ * the `<project>/` namespace package the suites nest into, so installing the
146
+ * metapackage makes `<project>.use_docker()` reachable alongside the suites'
147
+ * `from <project> import <pkg>`. `moduleDir` is the on-disk source directory; it
148
+ * differs from the import `moduleName` only when a suite is named after the
149
+ * project, in which case a `package-dir` remap keeps the import name intact.
139
150
  */
140
151
  export function generateRootPyproject(
141
152
  proj: ProjectMeta,
142
153
  distNames: string[],
143
154
  moduleName: string,
155
+ moduleDir: string,
144
156
  ): string {
145
157
  const cb = new CodeBuilder(" ");
146
158
  cb.line("[project]");
@@ -158,6 +170,9 @@ export function generateRootPyproject(
158
170
  cb.blank();
159
171
  cb.line("[tool.setuptools]");
160
172
  cb.line(`packages = ["${moduleName}"]`);
173
+ if (moduleDir !== moduleName) {
174
+ cb.line(`package-dir = { "${moduleName}" = "${moduleDir}" }`);
175
+ }
161
176
  cb.blank();
162
177
  cb.line("[tool.setuptools.package-data]");
163
178
  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