sphinx-vite-builder 0.0.1a16.dev1__tar.gz → 0.0.1a16.dev2__tar.gz

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 (14) hide show
  1. {sphinx_vite_builder-0.0.1a16.dev1 → sphinx_vite_builder-0.0.1a16.dev2}/AGENTS.md +9 -3
  2. {sphinx_vite_builder-0.0.1a16.dev1 → sphinx_vite_builder-0.0.1a16.dev2}/PKG-INFO +11 -9
  3. {sphinx_vite_builder-0.0.1a16.dev1 → sphinx_vite_builder-0.0.1a16.dev2}/README.md +10 -7
  4. {sphinx_vite_builder-0.0.1a16.dev1 → sphinx_vite_builder-0.0.1a16.dev2}/pyproject.toml +1 -2
  5. {sphinx_vite_builder-0.0.1a16.dev1 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/__init__.py +18 -1
  6. {sphinx_vite_builder-0.0.1a16.dev1 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/_internal/process.py +23 -5
  7. {sphinx_vite_builder-0.0.1a16.dev1 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/_internal/vite.py +21 -4
  8. {sphinx_vite_builder-0.0.1a16.dev1 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/build.py +57 -2
  9. {sphinx_vite_builder-0.0.1a16.dev1 → sphinx_vite_builder-0.0.1a16.dev2}/.gitignore +0 -0
  10. {sphinx_vite_builder-0.0.1a16.dev1 → sphinx_vite_builder-0.0.1a16.dev2}/CLAUDE.md +0 -0
  11. {sphinx_vite_builder-0.0.1a16.dev1 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/_internal/__init__.py +0 -0
  12. {sphinx_vite_builder-0.0.1a16.dev1 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/_internal/bus.py +0 -0
  13. {sphinx_vite_builder-0.0.1a16.dev1 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/_internal/errors.py +0 -0
  14. {sphinx_vite_builder-0.0.1a16.dev1 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/py.typed +0 -0
@@ -29,6 +29,12 @@ asyncio loop in a daemon thread for sync↔async bridging),
29
29
  spawn install/build), and `errors.py` (`PnpmMissingError`,
30
30
  `NodeModulesInstallError`, `ViteFailedError`).
31
31
 
32
+ **Phase 1 status:** The PEP 517 backend is fully implemented and
33
+ tested. The Sphinx extension `setup()` is a placeholder — it
34
+ registers cleanly in `conf.py` but doesn't yet hook the docs build
35
+ lifecycle. The full extension implementation (event handlers, vite
36
+ watch, teardown) lands in a follow-up release.
37
+
32
38
  ## The design contract — keep this invariant
33
39
 
34
40
  > **Sources should check for node, pnpm, etc and error if it's not
@@ -89,9 +95,9 @@ runs the wheel-from-sdist chain:
89
95
  Net: **sdist install also requires zero toolchain**. The
90
96
  `web/`-absent short-circuit is the load-bearing primitive.
91
97
 
92
- ## The four QA permutations — keep them green
98
+ ## QA permutations — keep them green
93
99
 
94
- Verified end-to-end as of v0.0.1a16.dev0:
100
+ The install-path permutations every change must keep green:
95
101
 
96
102
  | # | Path | Toolchain | Expected |
97
103
  |---|---|---|---|
@@ -152,7 +158,7 @@ resolution or distribution metadata, so wrapping them would be wrong.
152
158
 
153
159
  The workspace's `release.yml` MUST keep the pnpm + Node setup steps
154
160
  that run before `uv build`, otherwise the wheels published to PyPI
155
- would be empty of static (the v0.0.1a15 broken-release pattern that
161
+ would be empty of static (the prior broken-release pattern that
156
162
  motivated this whole package). The required steps are:
157
163
 
158
164
  ```yaml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sphinx-vite-builder
3
- Version: 0.0.1a16.dev1
3
+ Version: 0.0.1a16.dev2
4
4
  Summary: PEP 517 build backend + Sphinx extension that orchestrates Vite via pnpm
5
5
  Project-URL: Repository, https://github.com/git-pull/gp-sphinx
6
6
  Author-email: Tony Narlock <tony@git-pull.com>
@@ -22,7 +22,6 @@ Classifier: Topic :: Documentation :: Sphinx
22
22
  Classifier: Topic :: Software Development :: Build Tools
23
23
  Classifier: Typing :: Typed
24
24
  Requires-Python: <4.0,>=3.10
25
- Requires-Dist: hatchling>=1.0
26
25
  Requires-Dist: sphinx>=8.1
27
26
  Description-Content-Type: text/markdown
28
27
 
@@ -70,8 +69,9 @@ backend at release time. The PEP 517 chain doesn't run on the
70
69
  consumer side. No backend invocation. No `pnpm`. No Node. The end
71
70
  user sees Python and only Python.
72
71
 
72
+ No pnpm, no Node — just Python:
73
+
73
74
  ```console
74
- $ # No pnpm, no Node, no problem
75
75
  $ pip install gp-furo-theme
76
76
  ```
77
77
 
@@ -145,14 +145,16 @@ exclude = ["web/"] # so the sdist→wheel chain hits the short-circuit
145
145
  artifacts = ["src/<your-theme>/theme/<theme-name>/static/"]
146
146
  ```
147
147
 
148
- ### Sphinx extension
148
+ ### Sphinx extension (Phase 1: placeholder)
149
149
 
150
- Loaded from `conf.py`. Runs Vite as part of the docs lifecycle:
150
+ The extension entry point is currently a placeholder registered in
151
+ `conf.py` to prevent import errors. Full lifecycle integration —
152
+ running Vite before the docs build and spawning a watched Vite
153
+ process during `sphinx-autobuild` — lands in a follow-up release.
151
154
 
152
- - `sphinx-build` `pnpm exec vite build` once before the docs build
153
- - `sphinx-autobuild` `pnpm exec vite build --watch` as a child process
154
- for the lifetime of the autobuild server, with idempotent re-fire on
155
- rebuilds and graceful teardown on signal / `atexit`
155
+ For now, the [PEP 517](https://peps.python.org/pep-0517/) backend
156
+ handles all Vite orchestration during source builds and wheel
157
+ generation; that path is fully implemented and tested.
156
158
 
157
159
  ```python
158
160
  # docs/conf.py
@@ -42,8 +42,9 @@ backend at release time. The PEP 517 chain doesn't run on the
42
42
  consumer side. No backend invocation. No `pnpm`. No Node. The end
43
43
  user sees Python and only Python.
44
44
 
45
+ No pnpm, no Node — just Python:
46
+
45
47
  ```console
46
- $ # No pnpm, no Node, no problem
47
48
  $ pip install gp-furo-theme
48
49
  ```
49
50
 
@@ -117,14 +118,16 @@ exclude = ["web/"] # so the sdist→wheel chain hits the short-circuit
117
118
  artifacts = ["src/<your-theme>/theme/<theme-name>/static/"]
118
119
  ```
119
120
 
120
- ### Sphinx extension
121
+ ### Sphinx extension (Phase 1: placeholder)
121
122
 
122
- Loaded from `conf.py`. Runs Vite as part of the docs lifecycle:
123
+ The extension entry point is currently a placeholder registered in
124
+ `conf.py` to prevent import errors. Full lifecycle integration —
125
+ running Vite before the docs build and spawning a watched Vite
126
+ process during `sphinx-autobuild` — lands in a follow-up release.
123
127
 
124
- - `sphinx-build` `pnpm exec vite build` once before the docs build
125
- - `sphinx-autobuild` `pnpm exec vite build --watch` as a child process
126
- for the lifetime of the autobuild server, with idempotent re-fire on
127
- rebuilds and graceful teardown on signal / `atexit`
128
+ For now, the [PEP 517](https://peps.python.org/pep-0517/) backend
129
+ handles all Vite orchestration during source builds and wheel
130
+ generation; that path is fully implemented and tested.
128
131
 
129
132
  ```python
130
133
  # docs/conf.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sphinx-vite-builder"
3
- version = "0.0.1a16.dev1"
3
+ version = "0.0.1a16.dev2"
4
4
  description = "PEP 517 build backend + Sphinx extension that orchestrates Vite via pnpm"
5
5
  requires-python = ">=3.10,<4.0"
6
6
  authors = [
@@ -31,7 +31,6 @@ keywords = ["sphinx", "extension", "vite", "pnpm", "pep517", "build", "backend"]
31
31
  # required at build time of consumer packages but not at runtime, so it
32
32
  # stays in [build-system].requires of *consumers* rather than here.
33
33
  dependencies = [
34
- "hatchling>=1.0",
35
34
  "sphinx>=8.1",
36
35
  ]
37
36
 
@@ -15,9 +15,13 @@ under :mod:`sphinx_vite_builder._internal`.
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ import logging
18
19
  import typing as t
19
20
 
20
- __version__ = "0.0.1a16.dev1"
21
+ __version__ = "0.0.1a16.dev2"
22
+
23
+ logger = logging.getLogger(__name__)
24
+ logger.addHandler(logging.NullHandler())
21
25
 
22
26
  if t.TYPE_CHECKING:
23
27
  from sphinx.application import Sphinx
@@ -31,6 +35,19 @@ def setup(app: Sphinx) -> dict[str, t.Any]:
31
35
  on ``sphinx-build``) lands in a follow-up commit. For now this
32
36
  stub registers the extension so consumers can declare it without
33
37
  a no-such-module error, and returns the safety metadata.
38
+
39
+ Examples
40
+ --------
41
+ The Phase-1 stub returns the parallel-safety metadata Sphinx
42
+ expects, regardless of the application object passed in:
43
+
44
+ >>> class FakeApp:
45
+ ... pass
46
+ >>> metadata = setup(FakeApp()) # type: ignore[arg-type]
47
+ >>> metadata["parallel_read_safe"]
48
+ True
49
+ >>> metadata["parallel_write_safe"]
50
+ True
34
51
  """
35
52
  del app
36
53
  return {
@@ -9,9 +9,11 @@ backend and extension heads need:
9
9
  - ``PYTHONUNBUFFERED=1`` is forced into the child env so Python tools
10
10
  invoked via the package-manager bridge don't withhold their output.
11
11
  - On POSIX, the child runs in a new session (``start_new_session=True``)
12
- so ``SIGTERM`` cleanly takes down the entire process tree (``pnpm exec``
13
- shells out to multiple intermediate processes without session
14
- isolation, only the top-level pnpm wrapper would exit).
12
+ and :meth:`AsyncProcess.terminate` signals the whole process group
13
+ via :func:`os.killpg` so ``pnpm exec`` plus every descendant exits
14
+ together. ``asyncio.subprocess.Process.terminate`` would only signal
15
+ the leader's PID, leaving the vite child orphaned (pnpm does not
16
+ forward signals to its ``exec`` target).
15
17
  - :meth:`AsyncProcess.terminate` is graceful-then-forceful: SIGTERM,
16
18
  await up to ``timeout`` seconds, escalate to SIGKILL if the child is
17
19
  still alive. Idempotent: calling on an already-exited process is a
@@ -32,6 +34,7 @@ import contextlib
32
34
  import logging
33
35
  import os
34
36
  import pathlib
37
+ import signal
35
38
  import sys
36
39
  import typing as t
37
40
 
@@ -196,7 +199,19 @@ class AsyncProcess:
196
199
  if self._process.returncode is not None:
197
200
  return self._process.returncode
198
201
 
199
- self._process.terminate()
202
+ # POSIX: the child was spawned with ``start_new_session=True``,
203
+ # so ``self._process.pid`` is the leader of its own session and
204
+ # equals its process-group ID. SIGTERM the whole group so
205
+ # ``pnpm exec`` plus all its descendants (including the vite
206
+ # process pnpm doesn't forward signals to) exit. asyncio's
207
+ # ``Process.terminate()`` is PID-only — it would leave vite
208
+ # orphaned. Windows has no ``killpg``; the asyncio default is
209
+ # the right primitive there.
210
+ with contextlib.suppress(ProcessLookupError):
211
+ if sys.platform != "win32":
212
+ os.killpg(self._process.pid, signal.SIGTERM)
213
+ else:
214
+ self._process.terminate()
200
215
  try:
201
216
  await asyncio.wait_for(self._process.wait(), timeout=timeout)
202
217
  except asyncio.TimeoutError:
@@ -208,7 +223,10 @@ class AsyncProcess:
208
223
  # ProcessLookupError race: the child can exit between
209
224
  # TimeoutError and kill().
210
225
  with contextlib.suppress(ProcessLookupError):
211
- self._process.kill()
226
+ if sys.platform != "win32":
227
+ os.killpg(self._process.pid, signal.SIGKILL)
228
+ else:
229
+ self._process.kill()
212
230
  await self._process.wait()
213
231
 
214
232
  # Wait for drainers to consume their last buffered line before
@@ -1,13 +1,14 @@
1
- """Vite + pnpm orchestration: detection, install, one-shot build, watch.
1
+ """Vite + pnpm orchestration: detection, install, one-shot build.
2
2
 
3
3
  This module is the shared orchestration core consumed by both heads:
4
4
 
5
5
  - The PEP 517 backend (:mod:`sphinx_vite_builder.build`) calls
6
6
  :func:`run_vite_build` from each of its hooks, before delegating to
7
7
  hatchling.
8
- - The Sphinx extension (:mod:`sphinx_vite_builder`) calls
9
- :func:`run_vite_build` (one-shot) or its watch sibling from
10
- ``builder-inited``.
8
+ - The Sphinx extension (:mod:`sphinx_vite_builder`) — Phase 1
9
+ placeholder — will call :func:`run_vite_build` from
10
+ ``builder-inited`` once the extension head lands in a follow-up
11
+ release.
11
12
 
12
13
  Fast-fail discipline: every prerequisite is checked up front so the
13
14
  caller gets an actionable diagnostic instead of a generic spawn-failure
@@ -340,6 +341,22 @@ def run_vite_build(
340
341
  non-zero.
341
342
  - :class:`ViteFailedError` if ``pnpm exec vite build`` exits
342
343
  non-zero.
344
+
345
+ Examples
346
+ --------
347
+ The ``SPHINX_VITE_BUILDER_SKIP`` environment variable
348
+ short-circuits the whole orchestration before any subprocess is
349
+ spawned — exercising it from a doctest verifies the escape hatch
350
+ keeps working without touching pnpm, Node, or the filesystem
351
+ tree:
352
+
353
+ >>> import os, pathlib, tempfile
354
+ >>> os.environ["SPHINX_VITE_BUILDER_SKIP"] = "1"
355
+ >>> try:
356
+ ... with tempfile.TemporaryDirectory() as tmp:
357
+ ... run_vite_build(pathlib.Path(tmp))
358
+ ... finally:
359
+ ... del os.environ["SPHINX_VITE_BUILDER_SKIP"]
343
360
  """
344
361
  if os.environ.get("SPHINX_VITE_BUILDER_SKIP"):
345
362
  logger.info("SPHINX_VITE_BUILDER_SKIP set; skipping vite build")
@@ -37,7 +37,41 @@ def build_wheel(
37
37
  config_settings: dict[str, t.Any] | None = None,
38
38
  metadata_directory: str | None = None,
39
39
  ) -> str:
40
- """PEP 517 ``build_wheel``: vite-build, then hatchling-pack."""
40
+ """PEP 517 ``build_wheel``: vite-build, then hatchling-pack.
41
+
42
+ Examples
43
+ --------
44
+ With ``SPHINX_VITE_BUILDER_SKIP=1`` set, the hook short-circuits
45
+ vite and delegates straight to :mod:`hatchling.build` against a
46
+ minimal synthetic project, producing a real ``.whl``:
47
+
48
+ >>> import os, pathlib, tempfile, textwrap
49
+ >>> os.environ["SPHINX_VITE_BUILDER_SKIP"] = "1"
50
+ >>> cwd = os.getcwd()
51
+ >>> with tempfile.TemporaryDirectory() as tmp:
52
+ ... project = pathlib.Path(tmp)
53
+ ... _ = (project / "pyproject.toml").write_text(textwrap.dedent('''
54
+ ... [build-system]
55
+ ... requires = ["hatchling"]
56
+ ... build-backend = "hatchling.build"
57
+ ... [project]
58
+ ... name = "doctest-pkg"
59
+ ... version = "0.0.0"
60
+ ... ''').lstrip())
61
+ ... pkg = project / "doctest_pkg"
62
+ ... pkg.mkdir()
63
+ ... _ = (pkg / "__init__.py").write_text("")
64
+ ... dist = project / "dist"
65
+ ... dist.mkdir()
66
+ ... os.chdir(project)
67
+ ... try:
68
+ ... name = build_wheel(str(dist))
69
+ ... finally:
70
+ ... os.chdir(cwd)
71
+ ... del os.environ["SPHINX_VITE_BUILDER_SKIP"]
72
+ >>> name.endswith(".whl")
73
+ True
74
+ """
41
75
  run_vite_build()
42
76
  return _hatchling.build_wheel(wheel_directory, config_settings, metadata_directory)
43
77
 
@@ -47,7 +81,18 @@ def build_editable(
47
81
  config_settings: dict[str, t.Any] | None = None,
48
82
  metadata_directory: str | None = None,
49
83
  ) -> str:
50
- """PEP 660 ``build_editable``: vite-build, then hatchling-pack-editable."""
84
+ """PEP 660 ``build_editable``: vite-build, then hatchling-pack-editable.
85
+
86
+ The delegation pattern matches :func:`build_wheel`; see that
87
+ function's docstring for an end-to-end example exercising the
88
+ ``SPHINX_VITE_BUILDER_SKIP=1`` short-circuit.
89
+
90
+ Examples
91
+ --------
92
+ >>> import inspect
93
+ >>> sorted(inspect.signature(build_editable).parameters)
94
+ ['config_settings', 'metadata_directory', 'wheel_directory']
95
+ """
51
96
  run_vite_build()
52
97
  return _hatchling.build_editable(
53
98
  wheel_directory, config_settings, metadata_directory
@@ -66,6 +111,16 @@ def build_sdist(
66
111
  the sdist without pnpm or Node — the wheel-from-sdist build will
67
112
  skip vite (no ``web/`` in the unpacked tree) and ship the
68
113
  pre-baked assets via hatchling's normal file selection.
114
+
115
+ The delegation pattern matches :func:`build_wheel`; see that
116
+ function's docstring for an end-to-end example exercising the
117
+ ``SPHINX_VITE_BUILDER_SKIP=1`` short-circuit.
118
+
119
+ Examples
120
+ --------
121
+ >>> import inspect
122
+ >>> sorted(inspect.signature(build_sdist).parameters)
123
+ ['config_settings', 'sdist_directory']
69
124
  """
70
125
  run_vite_build()
71
126
  return _hatchling.build_sdist(sdist_directory, config_settings)