processkit-py 1.1.1__tar.gz → 1.2.1__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.
- {processkit_py-1.1.1 → processkit_py-1.2.1}/CHANGELOG.md +159 -1
- {processkit_py-1.1.1 → processkit_py-1.2.1}/CONTRIBUTING.md +5 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/Cargo.lock +3 -3
- {processkit_py-1.1.1 → processkit_py-1.2.1}/Cargo.toml +4 -4
- {processkit_py-1.1.1 → processkit_py-1.2.1}/PKG-INFO +13 -2
- {processkit_py-1.1.1 → processkit_py-1.2.1}/README.md +12 -1
- {processkit_py-1.1.1 → processkit_py-1.2.1}/RELEASING.md +6 -10
- {processkit_py-1.1.1 → processkit_py-1.2.1}/SECURITY.md +8 -0
- processkit_py-1.2.1/benchmarks/README.md +57 -0
- processkit_py-1.2.1/benchmarks/__init__.py +11 -0
- processkit_py-1.2.1/benchmarks/_shared.py +20 -0
- processkit_py-1.2.1/benchmarks/test_output_all.py +52 -0
- processkit_py-1.2.1/benchmarks/test_process_group.py +26 -0
- processkit_py-1.2.1/benchmarks/test_spawn_capture.py +61 -0
- processkit_py-1.2.1/benchmarks/test_streaming_throughput.py +41 -0
- processkit_py-1.2.1/deny.toml +44 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/README.md +5 -2
- {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/api-reference.md +3 -1
- processkit_py-1.2.1/docs/cli.md +114 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/commands.md +116 -4
- {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/cookbook.md +75 -14
- {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/event-loops.md +4 -4
- {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/internals.md +62 -18
- {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/migrating.md +1 -1
- {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/process-groups.md +15 -0
- processkit_py-1.2.1/docs/sandboxing.md +208 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/streaming.md +124 -34
- {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/supervision.md +40 -2
- {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/testing.md +1 -1
- {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/timeouts-and-cancellation.md +7 -5
- processkit_py-1.2.1/examples/04_sandbox_resource_limits.py +98 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/examples/README.md +4 -2
- {processkit_py-1.1.1 → processkit_py-1.2.1}/mkdocs.yml +2 -1
- {processkit_py-1.1.1 → processkit_py-1.2.1}/pyproject.toml +52 -3
- {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/ci-privileged-guard.py +7 -3
- {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/gen_api_reference.py +3 -2
- {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/release/cargo_lock.py +4 -1
- {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/release/changelog.py +31 -12
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/batch.rs +10 -7
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/cli.rs +36 -2
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/command.rs +203 -50
- processkit_py-1.2.1/src/convert.rs +848 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/errors.rs +4 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/group.rs +56 -24
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/lib.rs +8 -2
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/processkit/__init__.py +2 -1
- processkit_py-1.2.1/src/processkit/__main__.py +263 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/processkit/_aio.py +101 -11
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/processkit/_processkit.pyi +221 -82
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/processkit/_protocols.py +5 -5
- processkit_py-1.2.1/src/processkit/_types.py +117 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/processkit/pytest_plugin.py +7 -0
- processkit_py-1.2.1/src/result.rs +867 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/runner.rs +155 -63
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/running.rs +115 -54
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/runtime.rs +1 -8
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/supervisor.rs +209 -8
- processkit_py-1.2.1/tests/_liveness.py +195 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/_programs.py +16 -6
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/_typing_pins.py +81 -3
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/property/test_argv_env_roundtrip.py +2 -2
- processkit_py-1.2.1/tests/test_batch.py +362 -0
- processkit_py-1.2.1/tests/test_ci_privileged_guard.py +102 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_cli_client.py +16 -2
- processkit_py-1.2.1/tests/test_cli_main.py +138 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_command.py +691 -4
- processkit_py-1.2.1/tests/test_event_loops.py +235 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_hardening.py +8 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_logging.py +20 -1
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_pipelines.py +41 -9
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_pytest_plugin.py +22 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_readiness.py +151 -4
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_release_scripts.py +129 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_streaming.py +174 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_supervisor.py +30 -0
- processkit_py-1.2.1/tests/test_threading.py +521 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/uv.lock +314 -217
- processkit_py-1.1.1/examples/04_sandbox_resource_limits.py +0 -82
- processkit_py-1.1.1/src/convert.rs +0 -289
- processkit_py-1.1.1/src/processkit/_types.py +0 -52
- processkit_py-1.1.1/src/result.rs +0 -458
- processkit_py-1.1.1/tests/_liveness.py +0 -97
- processkit_py-1.1.1/tests/test_batch.py +0 -164
- {processkit_py-1.1.1 → processkit_py-1.2.1}/.editorconfig +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/.gitattributes +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/.gitignore +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/.pre-commit-config.yaml +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/.python-version +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/.yamllint.yml +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/LICENSE +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/ROADMAP.md +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/cliff.toml +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/conftest.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/pipelines.md +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/docs/platforms.md +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/examples/01_no_orphan_guarantee.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/examples/02_wait_for_server.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/examples/03_supervise_until_healthy.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/rust-toolchain.toml +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/check-env.ps1 +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/check-env.sh +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/ci-privileged-check.sh +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/release/__init__.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/scripts/smoke.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/cancellation.rs +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/logging.rs +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/processkit/py.typed +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/src/processkit/testing.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/stubtest-allowlist.txt +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/__init__.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/_docs_snippets.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/conftest.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/property/__init__.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/property/conftest.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/property/test_numeric_validation.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/property/test_output_limit.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/property/test_signals.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/property/test_wait_for_line.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_api_reference.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_api_surface.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_async.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_docs_snippets.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_examples.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_exceptions.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_process_group.py +0 -0
- {processkit_py-1.1.1 → processkit_py-1.2.1}/tests/test_runner_seam.py +0 -0
|
@@ -16,6 +16,162 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
16
|
### Fixed
|
|
17
17
|
-
|
|
18
18
|
|
|
19
|
+
## [1.2.1] - 2026-07-09
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Add Command.prefer_local, exposing crate 2.2's bare-name resolution override
|
|
24
|
+
- Add Command.prefer_local, exposing crate 2.2's bare-name resolution override
|
|
25
|
+
- Add a runnable Command.prefer_local example to docs
|
|
26
|
+
- Add ProcessStdin.send_control for interactive control-byte delivery
|
|
27
|
+
- Add ProcessStdin.send_control for interactive control-byte delivery
|
|
28
|
+
- Add Command.prefer_local usage example to docs/commands.md
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Broaden Command.priority docstring privilege caveat to cover above_normal and niced-parent normal
|
|
34
|
+
- Bump processkit dependency requirement and lockfile to 2.2.0
|
|
35
|
+
- Bump processkit dependency requirement and lockfile to 2.2.0
|
|
36
|
+
- Broaden Command.priority docstring privilege caveat to cover above_normal and niced-parent normal
|
|
37
|
+
- Initialize integration workspace for batch B-20260709T132808Z
|
|
38
|
+
- Initialize integration workspace for batch B-20260709T144637Z
|
|
39
|
+
- Apply rustfmt to send_control signature
|
|
40
|
+
- Bump processkit dependency to 2.2.1
|
|
41
|
+
- Initialize integration workspace for batch B-20260709T162529Z
|
|
42
|
+
- Bump processkit dependency to 2.2.1
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- Fix Windows-incompatible relative path-form assertion in the prefer_local example
|
|
48
|
+
|
|
49
|
+
## [1.2.0] - 2026-07-08
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- `Command.stdout_tee` / `Command.stderr_tee` now accept a **Python writer**
|
|
53
|
+
object (anything with a callable `write()` — `io.StringIO`, `sys.stderr`, a
|
|
54
|
+
text-mode file, a logger wrapper) in addition to a file path, mirroring the
|
|
55
|
+
child's output straight into your own console/buffer/logger while still
|
|
56
|
+
capturing it. Each decoded line (plus a `"\n"`) is passed to `write()` as a
|
|
57
|
+
`str` via an async-write bridge: every write is dispatched to the runtime's
|
|
58
|
+
blocking pool (re-acquiring the GIL there) and awaited on the capture pump, so
|
|
59
|
+
a slow — even sleeping — `write()` applies backpressure without blocking the
|
|
60
|
+
event loop or deadlocking the runtime. The object is discriminated from a path
|
|
61
|
+
by exposing `write` (neither `str` nor `pathlib.Path` does) and is never closed
|
|
62
|
+
for you; `append=True` is meaningful only for a file path and raises
|
|
63
|
+
`ValueError` if combined with a writer. A `write()` exception disables the tee
|
|
64
|
+
for the rest of the run (a `tracing` warning under `enable_logging()`, the same
|
|
65
|
+
isolation as the file tee) and is additionally reported via `sys.unraisablehook`
|
|
66
|
+
— the run and its captured result are unaffected. The previous "a file path
|
|
67
|
+
only, an arbitrary Python writer is deliberately not supported" restriction is
|
|
68
|
+
lifted. See `docs/streaming.md#tee-output-to-a-file`.
|
|
69
|
+
- `Command.on_stdout_line(callback)` / `Command.on_stderr_line(callback)`: a
|
|
70
|
+
`Callable[[str], None]` invoked with every decoded line as it is produced —
|
|
71
|
+
the way to give the **synchronous** surface (`.output()`/`.run()`) live
|
|
72
|
+
progress observation during an otherwise-blocking call, without losing the
|
|
73
|
+
full capture. Also fires on the async verbs and on a streamed run
|
|
74
|
+
(`start()`/`astart()` + `stdout_lines()`/`output_events()`); at most one
|
|
75
|
+
handler per stream (a repeat call replaces the previous one); a raising
|
|
76
|
+
callback is reported via `sys.unraisablehook` rather than propagated or
|
|
77
|
+
breaking the run. Inert under `stdout("inherit")`/`stdout("null")` (resp.
|
|
78
|
+
`stderr(...)`) and, for `on_stdout_line` only, under `output_bytes()` (which
|
|
79
|
+
captures stdout raw, bypassing the line pump — stderr still goes through it,
|
|
80
|
+
so `on_stderr_line` still fires there). See
|
|
81
|
+
`docs/streaming.md#live-per-line-callbacks`.
|
|
82
|
+
- A `benchmarks/` suite (`pytest-benchmark`, new `bench` dependency-group)
|
|
83
|
+
measuring spawn+capture overhead against `subprocess`/`asyncio.subprocess`,
|
|
84
|
+
`ProcessGroup` start/exit, line-streaming throughput, and `output_all`
|
|
85
|
+
concurrency scaling — dev tooling only, no public API change. Runs nightly
|
|
86
|
+
via the `bench` job in `nightly-hardening.yml`, never in the PR gate; see
|
|
87
|
+
`benchmarks/README.md`.
|
|
88
|
+
- `wait_for_path(path, *, timeout, interval=0.05)` — a new async readiness
|
|
89
|
+
helper alongside `wait_until` / `wait_for_port` / `wait_for_line`, polling
|
|
90
|
+
until a filesystem path appears (a unix socket, a pid file, or any other
|
|
91
|
+
marker a daemon creates once ready). Same timeout/interval discipline as its
|
|
92
|
+
siblings (NaN/negative `timeout` and non-positive `interval` raise
|
|
93
|
+
`ValueError`; `timeout=0` still checks the path at least once) and raises
|
|
94
|
+
`WaitTimeout` (also a `TimeoutError`) on expiry, now carrying a `path` field
|
|
95
|
+
(`WaitTimeout.__init__` gained a `path: StrPath | None = None` parameter).
|
|
96
|
+
- `python -m processkit run -- <cmd> [args...]`: a CLI wrapper that runs a
|
|
97
|
+
command inside a kill-on-exit `ProcessGroup` with inherited stdio, for
|
|
98
|
+
shell scripts and CI steps with no Python to write. Supports `--timeout`,
|
|
99
|
+
`--timeout-grace`, `--max-memory`, `--max-processes`, and `--cpu-quota`;
|
|
100
|
+
the child's own exit code is passed through unchanged,
|
|
101
|
+
and a timeout / missing program / rejected resource limit is reported as a
|
|
102
|
+
one-line stderr message with a documented, GNU-`timeout`-style exit code
|
|
103
|
+
instead of a traceback. See `docs/cli.md`.
|
|
104
|
+
- `Finished` gains `timed_out` and `signal` properties that delegate to the
|
|
105
|
+
nested `outcome`, so it now mirrors `Outcome` fully — matching `code` and
|
|
106
|
+
`exited_zero`, which were already exposed directly — instead of requiring
|
|
107
|
+
`finished.outcome.timed_out` / `finished.outcome.signal`.
|
|
108
|
+
- `Command.stdin_file(path)` — feed the child's stdin from a file, streamed in
|
|
109
|
+
chunks by the crate rather than read whole into a Python `bytes` object, for
|
|
110
|
+
large inputs (a `psql` dump, a `tar` archive, a multi-gigabyte log). Like
|
|
111
|
+
most other builder methods (`stdout_tee`/`stderr_tee` are the deliberate
|
|
112
|
+
exception), it does not touch the filesystem at build time — the path is
|
|
113
|
+
opened lazily at spawn, so a missing/unreadable file surfaces as the generic
|
|
114
|
+
`ProcessError` from the run/output verb, not `FileNotFoundError`. Reusable
|
|
115
|
+
across retries/re-runs, like `stdin_bytes`/`stdin_text`; the usual "last
|
|
116
|
+
stdin method wins" rule applies alongside `stdin_bytes()`/`stdin_text()`/
|
|
117
|
+
`keep_stdin_open()`.
|
|
118
|
+
- `ProcessResult` and `BytesResult` gain `diagnostic: str | None` (stderr if it
|
|
119
|
+
carries text, otherwise stdout, otherwise `None` — the same preference order
|
|
120
|
+
as `NonZeroExit`/`Timeout`/`Signalled.diagnostic` on the exceptions) and
|
|
121
|
+
`outcome: Outcome` (the same value `RunProfile.outcome` and the checking-verb
|
|
122
|
+
exceptions expose). A result held as data (`output()`/`output_bytes()`
|
|
123
|
+
without `ensure_success()`) no longer requires re-deriving these by hand.
|
|
124
|
+
(An `output_contains_any` convenience was considered alongside these and
|
|
125
|
+
rejected: the underlying `processkit` crate has no such method, so it
|
|
126
|
+
wouldn't be parity with the crate or the exceptions like `diagnostic`/
|
|
127
|
+
`outcome` are — and it's a one-liner callers can already write themselves
|
|
128
|
+
via `combined`, e.g. `any(s in result.combined for s in needles)`.)
|
|
129
|
+
- Value semantics for the result types: `ProcessResult`, `BytesResult`,
|
|
130
|
+
`Outcome`, `Finished`, `RunProfile`, and `SupervisionOutcome` now define
|
|
131
|
+
`__eq__` (comparing every field the underlying `processkit` crate's own
|
|
132
|
+
`PartialEq` compares — not `object`'s previous identity comparison) and a
|
|
133
|
+
consistent `__hash__` (none of their fields are stored floats, so hashing is
|
|
134
|
+
sound), so two results can now be compared with `==` and used in a `set` or
|
|
135
|
+
as a `dict` key without a manual field-by-field comparison.
|
|
136
|
+
`ProcessResult`/`Outcome`/`Finished`/`SupervisionOutcome` are also picklable
|
|
137
|
+
— e.g. to return a `ProcessResult` from a
|
|
138
|
+
`concurrent.futures.ProcessPoolExecutor` worker. The underlying crate has no
|
|
139
|
+
public constructor for any of these types, so unpickling reconstructs one via
|
|
140
|
+
`processkit.testing.ScriptedRunner` (an in-memory, no-subprocess replay) —
|
|
141
|
+
faithful for every field the Python binding exposes, but a command that
|
|
142
|
+
customized `success_codes()`/`timeout()` is not guaranteed to compare `==`
|
|
143
|
+
its original after a round trip (those two fields have no Python accessor to
|
|
144
|
+
reconstruct exactly). `BytesResult` (raw stdout may not be valid UTF-8, and
|
|
145
|
+
the only reconstruction channel available is text-only) and `RunProfile`
|
|
146
|
+
(reports live OS resource-sampling telemetry with no synthesis path outside
|
|
147
|
+
an actual monitored run) explicitly do **not** support pickling and raise a
|
|
148
|
+
clear `TypeError` rather than failing silently or fabricating the missing
|
|
149
|
+
data.
|
|
150
|
+
|
|
151
|
+
### Changed
|
|
152
|
+
- `CliClient(default_env_fn=...)` now validates that every value in the
|
|
153
|
+
mapping is callable **at construction time**, raising `TypeError` (naming
|
|
154
|
+
the offending key) immediately instead of silently accepting a non-callable
|
|
155
|
+
value and only discovering the mistake later — once per built command, as
|
|
156
|
+
an unraisable-hook warning plus an always-empty resolved env var. Valid
|
|
157
|
+
callables behave exactly as before.
|
|
158
|
+
|
|
159
|
+
### Fixed
|
|
160
|
+
- `Args` (`from processkit import Args`) no longer rejects the single most
|
|
161
|
+
common real call site — a variable annotated `list[str]` (or
|
|
162
|
+
`list[pathlib.Path]` / `list[os.PathLike[str]]`) passed straight through to
|
|
163
|
+
an argv-like parameter, e.g. `args: list[str] = [...]; cmd.args(args)`.
|
|
164
|
+
`list` is invariant, so the original `list[StrPath] | tuple[StrPath, ...]`
|
|
165
|
+
spelling only ever accepted a `list[StrPath]`-annotated variable or a
|
|
166
|
+
literal, not a `list[str]`/`list[Path]`/`list[os.PathLike[str]]`-annotated
|
|
167
|
+
one, even though the values are runtime-identical — a static-typing-only
|
|
168
|
+
false positive with no runtime effect. `Args` is now a union of the concrete
|
|
169
|
+
homogeneous list shapes (`list[str]`, `list[Path]`, `list[os.PathLike[str]]`)
|
|
170
|
+
instead of the single invariant `list[StrPath]`; a *mixed* `str`/
|
|
171
|
+
`os.PathLike[str]` argv is still accepted, now spelled as a `tuple` rather
|
|
172
|
+
than a `list` literal (e.g. `cmd.args((path, "literal"))`). A bare `str`
|
|
173
|
+
still does not type-check as `Args` (unchanged; see the `Args` docstring).
|
|
174
|
+
|
|
19
175
|
## [1.1.1] - 2026-07-06
|
|
20
176
|
|
|
21
177
|
### Added
|
|
@@ -709,7 +865,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
709
865
|
- `enable_logging()` enables the crate's `tracing` feature; the bridge pulls
|
|
710
866
|
`tracing` / `tracing-subscriber` (registry only) into the compiled wheel.
|
|
711
867
|
|
|
712
|
-
[Unreleased]: https://github.com/ZelAnton/processkit-py/compare/v1.
|
|
868
|
+
[Unreleased]: https://github.com/ZelAnton/processkit-py/compare/v1.2.1...HEAD
|
|
869
|
+
[1.2.1]: https://github.com/ZelAnton/processkit-py/compare/v1.2.0...v1.2.1
|
|
870
|
+
[1.2.0]: https://github.com/ZelAnton/processkit-py/compare/v1.1.1...v1.2.0
|
|
713
871
|
[1.1.1]: https://github.com/ZelAnton/processkit-py/compare/v1.1.0...v1.1.1
|
|
714
872
|
[1.1.0]: https://github.com/ZelAnton/processkit-py/compare/v1.0.0...v1.1.0
|
|
715
873
|
[1.0.0]: https://github.com/ZelAnton/processkit-py/releases/tag/v1.0.0
|
|
@@ -80,6 +80,11 @@ native `uv run pytest`, which is faster for day-to-day work.
|
|
|
80
80
|
warning-free `pytest`, plus `cargo fmt` / `clippy` on the Rust side — all
|
|
81
81
|
configured in [`pyproject.toml`](pyproject.toml); run the
|
|
82
82
|
[gates above](#build-and-test) locally before opening a pull request.
|
|
83
|
+
- **Docs are built here, not published here.** `docs.yml` runs `mkdocs build
|
|
84
|
+
--strict` as a link/anchor check only; this repo does not deploy to GitHub
|
|
85
|
+
Pages or anywhere else (a separate project owns docs publishing). Do not
|
|
86
|
+
add a Pages/`mike`/`gh-pages` deploy step back — see RELEASING.md's "Docs
|
|
87
|
+
site" note.
|
|
83
88
|
|
|
84
89
|
## Changelog
|
|
85
90
|
|
|
@@ -165,9 +165,9 @@ dependencies = [
|
|
|
165
165
|
|
|
166
166
|
[[package]]
|
|
167
167
|
name = "processkit"
|
|
168
|
-
version = "2.1
|
|
168
|
+
version = "2.2.1"
|
|
169
169
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
170
|
-
checksum = "
|
|
170
|
+
checksum = "d5291d099b0b4780f8fdf0e8d9c9f1308f61dd5562bd22115e8d054576eb23bb"
|
|
171
171
|
dependencies = [
|
|
172
172
|
"async-trait",
|
|
173
173
|
"encoding_rs",
|
|
@@ -184,7 +184,7 @@ dependencies = [
|
|
|
184
184
|
|
|
185
185
|
[[package]]
|
|
186
186
|
name = "processkit-py"
|
|
187
|
-
version = "1.
|
|
187
|
+
version = "1.2.1"
|
|
188
188
|
dependencies = [
|
|
189
189
|
"processkit",
|
|
190
190
|
"pyo3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "processkit-py"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.1"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "PyO3 binding crate for the processkit Python package (compiled into wheels)"
|
|
6
6
|
license = "MIT"
|
|
@@ -34,10 +34,10 @@ pyo3 = { version = "0.29", features = ["abi3-py310"] }
|
|
|
34
34
|
# runtime we reuse for BOTH the sync surface (`block_on`) and the async surface
|
|
35
35
|
# (`future_into_py`), so the binding owns exactly one runtime. Tracks pyo3 0.29.
|
|
36
36
|
pyo3-async-runtimes = { version = "0.29", features = ["tokio-runtime"] }
|
|
37
|
-
# The Rust core. Compatible-version requirement `2.
|
|
37
|
+
# The Rust core. Compatible-version requirement `2.2` (>=2.2.0, <3.0.0) — the
|
|
38
38
|
# binding tracks API/behaviour churn deliberately, not transitively. 1.3.0 (a
|
|
39
39
|
# semver-broken minor) and 2.0.0 (a mistaken publish) were yanked upstream, so
|
|
40
|
-
# 2.
|
|
40
|
+
# 2.2.0 is the floor; the requirement stays within the 2.x major on purpose.
|
|
41
41
|
#
|
|
42
42
|
# Features are listed EXPLICITLY with `default-features = false`, so a change to
|
|
43
43
|
# the crate's default set can never silently alter our public surface:
|
|
@@ -53,7 +53,7 @@ pyo3-async-runtimes = { version = "0.29", features = ["tokio-runtime"] }
|
|
|
53
53
|
# tests; it has no Python value (the Python test doubles come from the crate's
|
|
54
54
|
# always-on `doubles` surface — ScriptedRunner / RecordingRunner — plus the
|
|
55
55
|
# `record`-gated cassette RecordReplayRunner enabled above).
|
|
56
|
-
processkit = { version = "2.
|
|
56
|
+
processkit = { version = "2.2", default-features = false, features = [
|
|
57
57
|
"process-control",
|
|
58
58
|
"limits",
|
|
59
59
|
"record",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: processkit-py
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -102,7 +102,7 @@ The first column is the differentiator: a child's *descendants* are contained an
|
|
|
102
102
|
reaped as a unit (Job Object / cgroup v2 / process group), not just the direct
|
|
103
103
|
child.
|
|
104
104
|
|
|
105
|
-
> **
|
|
105
|
+
> **Stable API.** The public API has been stable since 1.0 and follows
|
|
106
106
|
> [Semantic Versioning](https://semver.org/): breaking changes land only in a new
|
|
107
107
|
> major version, so `1.x` upgrades are backward-compatible. See
|
|
108
108
|
> [CHANGELOG.md](CHANGELOG.md), and [ROADMAP.md](ROADMAP.md) for how it was built.
|
|
@@ -186,6 +186,17 @@ async def main():
|
|
|
186
186
|
asyncio.run(main())
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
+
No Python to write? `python -m processkit run -- <cmd> [args...]` gives a
|
|
190
|
+
shell script or CI step the same kill-on-exit containment and resource
|
|
191
|
+
limits from the command line, no code required:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
python -m processkit run --timeout 30 --max-memory 536870912 -- pytest -x
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
See [Command-line usage](docs/cli.md) for the full flag list and exit-code
|
|
198
|
+
contract.
|
|
199
|
+
|
|
189
200
|
## Documentation
|
|
190
201
|
|
|
191
202
|
This README is the quick tour. The **[`docs/` guide set](docs/README.md)** goes
|
|
@@ -69,7 +69,7 @@ The first column is the differentiator: a child's *descendants* are contained an
|
|
|
69
69
|
reaped as a unit (Job Object / cgroup v2 / process group), not just the direct
|
|
70
70
|
child.
|
|
71
71
|
|
|
72
|
-
> **
|
|
72
|
+
> **Stable API.** The public API has been stable since 1.0 and follows
|
|
73
73
|
> [Semantic Versioning](https://semver.org/): breaking changes land only in a new
|
|
74
74
|
> major version, so `1.x` upgrades are backward-compatible. See
|
|
75
75
|
> [CHANGELOG.md](CHANGELOG.md), and [ROADMAP.md](ROADMAP.md) for how it was built.
|
|
@@ -153,6 +153,17 @@ async def main():
|
|
|
153
153
|
asyncio.run(main())
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
+
No Python to write? `python -m processkit run -- <cmd> [args...]` gives a
|
|
157
|
+
shell script or CI step the same kill-on-exit containment and resource
|
|
158
|
+
limits from the command line, no code required:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
python -m processkit run --timeout 30 --max-memory 536870912 -- pytest -x
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
See [Command-line usage](docs/cli.md) for the full flag list and exit-code
|
|
165
|
+
contract.
|
|
166
|
+
|
|
156
167
|
## Documentation
|
|
157
168
|
|
|
158
169
|
This README is the quick tour. The **[`docs/` guide set](docs/README.md)** goes
|
|
@@ -68,17 +68,13 @@ GitHub Release (wheels + sdist + `SHA256SUMS`).
|
|
|
68
68
|
## Docs site
|
|
69
69
|
|
|
70
70
|
The guides in `docs/` render as a [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/)
|
|
71
|
-
site
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
site. `docs.yml` builds it (`mkdocs build --strict`) on every docs change as a
|
|
72
|
+
link/anchor check only — this repo does **not** publish the site (no GitHub
|
|
73
|
+
Pages, no `gh-pages` branch, no `mike`, no `DOCS_DEPLOY`). A separate project
|
|
74
|
+
owns docs publishing. **Do not re-add a Pages/mike deploy job here** — if a
|
|
75
|
+
future dependency bump or template sync tries to reintroduce one, undo it.
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
2. Repo **Settings → Secrets and variables → Actions → Variables**: add
|
|
77
|
-
`DOCS_DEPLOY` = `true`.
|
|
78
|
-
|
|
79
|
-
The next push to `main` that touches `docs/` or `mkdocs.yml` then deploys to
|
|
80
|
-
`https://zelanton.github.io/processkit-py/`. Preview locally with
|
|
81
|
-
`uvx --with mkdocs-material mkdocs serve`.
|
|
77
|
+
Preview the docs locally with `uv run --group docs mkdocs serve`.
|
|
82
78
|
|
|
83
79
|
## If a release fails
|
|
84
80
|
|
|
@@ -36,6 +36,14 @@ ready, a patched release is published to PyPI and the advisory is disclosed.
|
|
|
36
36
|
[`.github/workflows/ci.yml`](.github/workflows/ci.yml)). It scans the resolved
|
|
37
37
|
Python dependency tree against the [PyPI Advisory Database](https://github.com/pypa/advisory-database)
|
|
38
38
|
and fails the build on a known vulnerability.
|
|
39
|
+
- **[cargo-deny](https://github.com/EmbarkStudios/cargo-deny)** is the Rust
|
|
40
|
+
analogue of pip-audit: the `rust-audit` job in
|
|
41
|
+
[`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs
|
|
42
|
+
`cargo deny check advisories bans licenses sources` on every pull request
|
|
43
|
+
and every push to `main`, scanning the compiled Rust dependency tree
|
|
44
|
+
(configured in [`deny.toml`](deny.toml)) against the RustSec Advisory
|
|
45
|
+
Database and failing the build on a known vulnerability, a yanked crate, or
|
|
46
|
+
a disallowed license.
|
|
39
47
|
- **[Dependabot](.github/dependabot.yml)** opens weekly pull requests to keep
|
|
40
48
|
GitHub Actions, Python packages (`uv` ecosystem), and Rust crates (`cargo`
|
|
41
49
|
ecosystem) current, so advisory fixes land promptly.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Benchmarks
|
|
2
|
+
|
|
3
|
+
`pytest-benchmark` timings for the questions [ROADMAP.md](../ROADMAP.md)'s
|
|
4
|
+
Phase 5 asks — "does the bridge add silly overhead?" — with a real number
|
|
5
|
+
attached instead of only the loose pass/fail bound in
|
|
6
|
+
`tests/test_hardening.py::test_no_silly_per_call_overhead`:
|
|
7
|
+
|
|
8
|
+
- **`test_spawn_capture.py`** — spawn + capture a single short-lived command:
|
|
9
|
+
`processkit`'s `Command(...).output()` against the two stdlib "naive"
|
|
10
|
+
equivalents, `subprocess.run(..., capture_output=True)` and
|
|
11
|
+
`asyncio.create_subprocess_exec(...)` + `communicate()`. Same payload on
|
|
12
|
+
all three, so the comparison is per-call overhead, not a differing
|
|
13
|
+
workload.
|
|
14
|
+
- **`test_process_group.py`** — `ProcessGroup` start/exit: creating the
|
|
15
|
+
group's kernel container, entering it, starting one short-lived child,
|
|
16
|
+
tearing the whole tree down.
|
|
17
|
+
- **`test_streaming_throughput.py`** — `RunningProcess.stdout_lines()` (see
|
|
18
|
+
[`docs/streaming.md`](../docs/streaming.md)) draining a known number of
|
|
19
|
+
lines end to end.
|
|
20
|
+
- **`test_output_all.py`** — `output_all()` / `aoutput_all()` at 1/10/50-way
|
|
21
|
+
concurrency (see [`docs/cookbook.md`](../docs/cookbook.md)).
|
|
22
|
+
|
|
23
|
+
## Running locally
|
|
24
|
+
|
|
25
|
+
This suite is **not** part of the PR gate — it lives in its own
|
|
26
|
+
`bench` dependency-group and is excluded from `testpaths` (`tests/` only), so
|
|
27
|
+
an ordinary `pytest`/`uv run pytest` never collects it. Install the group and
|
|
28
|
+
run it explicitly:
|
|
29
|
+
|
|
30
|
+
```console
|
|
31
|
+
uv sync --group bench
|
|
32
|
+
uv run pytest benchmarks/ --benchmark-only -p no:xdist -o addopts=""
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`-p no:xdist -o addopts=""` disables `-n auto` (the repo's default
|
|
36
|
+
`addopts`) — `pytest-benchmark` needs to run in the main process, in a single
|
|
37
|
+
worker, to produce meaningful timings; under `pytest-xdist` it silently skips
|
|
38
|
+
measuring instead.
|
|
39
|
+
|
|
40
|
+
Useful extras:
|
|
41
|
+
|
|
42
|
+
- `--benchmark-only` skips the normal (non-benchmark) test collection outside
|
|
43
|
+
this directory should it ever leak in; harmless here since `benchmarks/`
|
|
44
|
+
has none, but keeps the invocation copy-pasteable elsewhere.
|
|
45
|
+
- `--benchmark-compare` / `--benchmark-autosave` — compare a run against a
|
|
46
|
+
previously saved one, to check a change before landing it.
|
|
47
|
+
- `--benchmark-json=out.json` — machine-readable results (what the nightly
|
|
48
|
+
CI job uses to render the job-summary table; see below).
|
|
49
|
+
|
|
50
|
+
## CI
|
|
51
|
+
|
|
52
|
+
The `bench` job in
|
|
53
|
+
[`.github/workflows/nightly-hardening.yml`](../.github/workflows/nightly-hardening.yml)
|
|
54
|
+
runs this suite on the same `schedule`/`workflow_dispatch` triggers as the
|
|
55
|
+
`stress` job — never on `push`/`pull_request` — and publishes the results as
|
|
56
|
+
a table in the job summary, so a regression shows up as a trend across nights
|
|
57
|
+
rather than only when someone happens to run this locally.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Benchmark suite (`pytest-benchmark`) — overhead vs `subprocess`.
|
|
2
|
+
|
|
3
|
+
Not part of the PR gate: `[tool.pytest.ini_options] testpaths` in
|
|
4
|
+
`pyproject.toml` is `["tests"]`, so an ordinary `pytest` run never collects
|
|
5
|
+
this directory. It runs on a schedule (and on manual dispatch) from the
|
|
6
|
+
`bench` job in `.github/workflows/nightly-hardening.yml`, which installs the
|
|
7
|
+
separate `bench` dependency-group and invokes `pytest benchmarks/
|
|
8
|
+
--benchmark-only` explicitly. See `benchmarks/README.md` to run it locally.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Small pieces shared by the benchmark modules — no test collection here."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
#: The interpreter under test, for building child `Command`s/`subprocess`
|
|
8
|
+
#: calls — same idea as `tests/conftest.py`'s `PY`, kept local so this
|
|
9
|
+
#: directory has no import-time dependency on the `tests/` package.
|
|
10
|
+
PY = sys.executable
|
|
11
|
+
|
|
12
|
+
#: A short-lived child that does no work (the pure per-call-overhead
|
|
13
|
+
#: baseline) — the same style as `tests/test_hardening.py`'s
|
|
14
|
+
#: `test_no_silly_per_call_overhead`.
|
|
15
|
+
NOOP_CODE = "pass"
|
|
16
|
+
|
|
17
|
+
#: A payload that also exercises stdout *capture* (not just spawn/exit),
|
|
18
|
+
#: identical across all three "spawn + capture" benchmarks below so the
|
|
19
|
+
#: comparison isolates per-call overhead rather than differing workloads.
|
|
20
|
+
CAPTURE_CODE = "import sys; sys.stdout.write('x' * 4096)"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""`output_all` / `aoutput_all` at different concurrency levels — bounded
|
|
2
|
+
parallel batches of short-lived commands (see `docs/cookbook.md`'s
|
|
3
|
+
"many commands, bounded concurrency" recipe).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from processkit import Command, aoutput_all, output_all
|
|
14
|
+
|
|
15
|
+
from ._shared import NOOP_CODE, PY
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pytest_benchmark.fixture import BenchmarkFixture
|
|
19
|
+
|
|
20
|
+
#: Concurrency == batch size in every case below: N commands, all allowed to
|
|
21
|
+
#: run at once, at each of the sizes the task calls out (1/10/50) — that
|
|
22
|
+
#: isolates how the batch dispatch machinery itself scales, rather than
|
|
23
|
+
#: mixing in a fixed backlog queued behind a narrower `concurrency=`.
|
|
24
|
+
_CONCURRENCIES = (1, 10, 50)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.parametrize("concurrency", _CONCURRENCIES)
|
|
28
|
+
def test_output_all_concurrency(benchmark: BenchmarkFixture, concurrency: int) -> None:
|
|
29
|
+
commands = [Command(PY, ["-c", NOOP_CODE]) for _ in range(concurrency)]
|
|
30
|
+
benchmark.group = f"output_all(concurrency={concurrency})"
|
|
31
|
+
|
|
32
|
+
def run() -> None:
|
|
33
|
+
results = output_all(commands, concurrency=concurrency)
|
|
34
|
+
assert len(results) == concurrency
|
|
35
|
+
|
|
36
|
+
benchmark(run)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.parametrize("concurrency", _CONCURRENCIES)
|
|
40
|
+
def test_aoutput_all_concurrency(benchmark: BenchmarkFixture, concurrency: int) -> None:
|
|
41
|
+
commands = [Command(PY, ["-c", NOOP_CODE]) for _ in range(concurrency)]
|
|
42
|
+
benchmark.group = f"aoutput_all(concurrency={concurrency})"
|
|
43
|
+
|
|
44
|
+
async def scenario() -> int:
|
|
45
|
+
results = await aoutput_all(commands, concurrency=concurrency)
|
|
46
|
+
return len(results)
|
|
47
|
+
|
|
48
|
+
def run() -> None:
|
|
49
|
+
count = asyncio.run(scenario())
|
|
50
|
+
assert count == concurrency
|
|
51
|
+
|
|
52
|
+
benchmark(run)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""`ProcessGroup` start/exit — the cost of creating the group's kernel
|
|
2
|
+
container (Job Object / cgroup-v2 / process group, see `docs/platforms.md`),
|
|
3
|
+
entering the context manager, starting one short-lived child into it, and
|
|
4
|
+
tearing the whole tree down on exit.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from processkit import Command, ProcessGroup
|
|
12
|
+
|
|
13
|
+
from ._shared import NOOP_CODE, PY
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pytest_benchmark.fixture import BenchmarkFixture
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_process_group_start_exit(benchmark: BenchmarkFixture) -> None:
|
|
20
|
+
def run() -> None:
|
|
21
|
+
with ProcessGroup() as group:
|
|
22
|
+
proc = group.start(Command(PY, ["-c", NOOP_CODE]))
|
|
23
|
+
outcome = proc.outcome()
|
|
24
|
+
assert outcome.exited_zero
|
|
25
|
+
|
|
26
|
+
benchmark(run)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Spawn + capture a single short-lived command: `processkit` vs the two
|
|
2
|
+
"naive" ways of doing the same thing from stdlib — `subprocess.run` and
|
|
3
|
+
`asyncio.create_subprocess_exec` + `communicate()`. Same payload on all
|
|
4
|
+
three (`_shared.CAPTURE_CODE`) so the comparison isolates the bridge's
|
|
5
|
+
per-call overhead rather than a differing workload.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import subprocess
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from processkit import Command
|
|
15
|
+
|
|
16
|
+
from ._shared import CAPTURE_CODE, PY
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pytest_benchmark.fixture import BenchmarkFixture
|
|
20
|
+
|
|
21
|
+
_EXPECTED = "x" * 4096
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_spawn_capture_processkit(benchmark: BenchmarkFixture) -> None:
|
|
25
|
+
def run() -> str:
|
|
26
|
+
result = Command(PY, ["-c", CAPTURE_CODE]).output()
|
|
27
|
+
assert result.is_success
|
|
28
|
+
return result.stdout
|
|
29
|
+
|
|
30
|
+
stdout = benchmark(run)
|
|
31
|
+
assert stdout == _EXPECTED
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_spawn_capture_subprocess(benchmark: BenchmarkFixture) -> None:
|
|
35
|
+
def run() -> str:
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
[PY, "-c", CAPTURE_CODE], capture_output=True, text=True, check=True
|
|
38
|
+
)
|
|
39
|
+
return result.stdout
|
|
40
|
+
|
|
41
|
+
stdout = benchmark(run)
|
|
42
|
+
assert stdout == _EXPECTED
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_spawn_capture_asyncio_subprocess(benchmark: BenchmarkFixture) -> None:
|
|
46
|
+
# `asyncio.run` opens/closes a fresh event loop per call — the same
|
|
47
|
+
# "naive per-call" shape a caller reaching for asyncio.subprocess without
|
|
48
|
+
# already running inside a long-lived loop would write.
|
|
49
|
+
async def scenario() -> str:
|
|
50
|
+
proc = await asyncio.create_subprocess_exec(
|
|
51
|
+
PY, "-c", CAPTURE_CODE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
52
|
+
)
|
|
53
|
+
stdout, _stderr = await proc.communicate()
|
|
54
|
+
assert proc.returncode == 0
|
|
55
|
+
return stdout.decode()
|
|
56
|
+
|
|
57
|
+
def run() -> str:
|
|
58
|
+
return asyncio.run(scenario())
|
|
59
|
+
|
|
60
|
+
stdout = benchmark(run)
|
|
61
|
+
assert stdout == _EXPECTED
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Line-streaming throughput: `RunningProcess.stdout_lines()` (see
|
|
2
|
+
`docs/streaming.md`) draining a child that writes a known number of lines as
|
|
3
|
+
fast as it can, end to end (spawn through `afinish()`).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from processkit import Command
|
|
12
|
+
|
|
13
|
+
from ._shared import PY
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pytest_benchmark.fixture import BenchmarkFixture
|
|
17
|
+
|
|
18
|
+
#: How many lines the child emits per run. Large enough that the loop
|
|
19
|
+
#: dominates over the fixed spawn cost, small enough that a bench round stays
|
|
20
|
+
#: fast (this file runs once per nightly job, at whatever --benchmark-min-rounds
|
|
21
|
+
#: pytest-benchmark's calibration lands on).
|
|
22
|
+
_LINE_COUNT = 5_000
|
|
23
|
+
|
|
24
|
+
_PRODUCER = f"for i in range({_LINE_COUNT}): print(i)"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_stdout_lines_throughput(benchmark: BenchmarkFixture) -> None:
|
|
28
|
+
async def scenario() -> int:
|
|
29
|
+
proc = await Command(PY, ["-c", _PRODUCER]).astart()
|
|
30
|
+
count = 0
|
|
31
|
+
async for _line in proc.stdout_lines():
|
|
32
|
+
count += 1
|
|
33
|
+
finished = await proc.afinish()
|
|
34
|
+
assert finished.exited_zero
|
|
35
|
+
return count
|
|
36
|
+
|
|
37
|
+
def run() -> int:
|
|
38
|
+
return asyncio.run(scenario())
|
|
39
|
+
|
|
40
|
+
lines = benchmark(run)
|
|
41
|
+
assert lines == _LINE_COUNT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# cargo-deny configuration. CI runs
|
|
2
|
+
# `cargo deny check advisories bans licenses sources`
|
|
3
|
+
# (see .github/workflows/ci.yml, `rust-audit` job). Kept in the PR gate (not
|
|
4
|
+
# nightly-hardening.yml) for the same reason
|
|
5
|
+
# pip-audit is: the crate is compiled into every wheel users install, so a
|
|
6
|
+
# newly-disclosed RustSec advisory or license violation should block the next
|
|
7
|
+
# merge, not wait for the nightly schedule. Adapted from the sibling
|
|
8
|
+
# ProcessKit-rs repository's `deny.toml` (same categories), scoped to this
|
|
9
|
+
# binding's own Cargo tree (pyo3, pyo3-async-runtimes, tokio, tracing, ...).
|
|
10
|
+
[advisories]
|
|
11
|
+
# Fail on crates with a RustSec security advisory or that have been yanked.
|
|
12
|
+
version = 2
|
|
13
|
+
yanked = "deny"
|
|
14
|
+
|
|
15
|
+
[licenses]
|
|
16
|
+
# This crate is MIT and is compiled into the distributed wheels; the
|
|
17
|
+
# dependency tree must stay on permissive licenses a downstream MIT/Apache
|
|
18
|
+
# consumer can absorb. A new transitive dependency with anything outside this
|
|
19
|
+
# list (copyleft, source-available, unknown) fails CI loudly instead of
|
|
20
|
+
# slipping into a release.
|
|
21
|
+
version = 2
|
|
22
|
+
allow = [
|
|
23
|
+
"MIT",
|
|
24
|
+
"Apache-2.0",
|
|
25
|
+
# target-lexicon (a pyo3-build-config build-dependency) uses this LLVM
|
|
26
|
+
# linking exception on top of Apache-2.0 -- still permissive, no
|
|
27
|
+
# copyleft/attribution burden beyond plain Apache-2.0.
|
|
28
|
+
"Apache-2.0 WITH LLVM-exception",
|
|
29
|
+
"BSD-3-Clause",
|
|
30
|
+
"Unicode-3.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[bans]
|
|
34
|
+
# Duplicate versions of the same crate are a warning, not a hard failure.
|
|
35
|
+
multiple-versions = "warn"
|
|
36
|
+
# Wildcard version requirements hide breakage; flag them.
|
|
37
|
+
wildcards = "deny"
|
|
38
|
+
|
|
39
|
+
[sources]
|
|
40
|
+
# Only allow dependencies from the official crates.io registry -- no git
|
|
41
|
+
# dependencies or unknown/private registries sneaking into the tree that
|
|
42
|
+
# gets compiled into every wheel.
|
|
43
|
+
unknown-registry = "deny"
|
|
44
|
+
unknown-git = "deny"
|