v0.6 (complete rewrite)#7
Merged
Merged
Conversation
Rewrite CLAUDE.md and README.md to match the current codebase, and add overview + quickstart + key-concepts READMEs for every module. Mark hosting / scheduler / http / di as stable and consumers / openapi as experimental. CLAUDE.md uses @-imports to pull module READMEs into Claude Code's context. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Importing HostRSGIApp at package init pulled localpost.http (and transitively localpost.threadtools) into the hosting bootstrap, closing a cycle the moment any non-hosting module needed current_service. Move HostRSGIApp behind localpost.hosting.rsgi so the hosting package init no longer depends on http; update tests, docs, and the docstring example. threadtools._run_async can now import current_service at the top level. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BlockingPortal.call() raises if invoked from the loop thread, so every caller had to know its calling context up-front. AsyncExecutor.stop(), AsyncWorkerExecutor.stop(), and friends carried "must be called from a non-loop thread" caveats; hosting/_host.py duplicated the pattern with a hand-rolled thread_id field, same_thread property, and in_host_thread helper. Introduce localpost.Portal: a wrapper that snapshots the loop thread's ident at construction and exposes run_sync (direct call on-loop, portal.call off-loop) and run_async (start_task_soon().result(), with a clean RuntimeError on the loop thread instead of a deadlock). Public API changes: - ServiceLifetimeView.portal, AsyncExecutor(portal=), and AsyncWorkerExecutor(portal=) now use Portal instead of BlockingPortal. - AsyncExecutor.stop() / AsyncWorkerExecutor.stop() are safe from any thread; their docstring caveats go away. - Drop ServiceLifetime.thread_id, ServiceLifetime.same_thread, hosting._host.in_host_thread — replaced by Portal.same_thread / Portal.run_sync. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…async CMs Counterpart to run_async for context managers: lets a worker thread enter an async CM via the current service's portal, with a same-thread guard to prevent loop deadlocks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Workers in WorkerExecutor / AsyncWorkerExecutor now live for the executor's lifetime — no per-worker `idle_timeout`, no `DEFAULT_IDLE_TIMEOUT` constant. With workers no longer exiting independently, switch to a single shared receiver: drop `_rx_template`, `_open_receivers`, the per-worker `rx.clone()` / `rx.close()`, and the `get_nowait` recheck under lock that resolved the producer-vs-timeout race. The worker loop reduces to `for task in self._rx: task.run()`. Rationale, alternatives, and the cross-language survey behind this choice are captured in ADR-0005. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d concurrency caps Worker pools no longer manage their own concurrency cap or backlog — callers control concurrency upstream of submit (Cloud Run-like gate, consumer-level Semaphore, etc.). With the cap gone, the channel-backed storage layer collapses to a plain deque + threading.Condition: one lock per submit instead of put_nowait → WouldBlock → put, and no dependence on Channel's capacity / clones / broadcast machinery. AsyncExecutor (per-task spawn gated by CapacityLimiter) is removed; it had no in-tree callers and per-task Future.cancel() propagation isn't needed when concurrency is controlled at the call site. ADR-0005 updated to reflect the deque-based shape and the broader "caller owns concurrency" stance.
…xecutor Both executors share the same wait/notify discipline (submit, _run_worker, mark-closed). Moving them onto a private _WorkerPoolBase removes ~80 lines of duplication and gives a single source of truth for the queue protocol; leaves stay @Final and own their own context-manager lifecycle plus _spawn_worker. WorkerExecutor now has stop() (was async-only): marks the pool closed and wakes idle workers — busy workers finish their current task and exit on the next iteration. Useful from a signal handler or any thread that needs to start shutdown without waiting for the with-block to exit. The WorkerExecutor.workers property is dropped — it was test-only and leaked the internal Thread list. Tests now verify the contract via threading.get_ident() inside tasks (which is what the property was indirectly checking anyway). Both executors expose worker_count: int as the symmetric public observation point.
…kers Outer-scope cancellation already correctly unblocks idle workers stuck in cond.wait, but only because __aexit__'s _mark_closed() runs synchronously before the await on tg.__aexit__. If a future refactor swaps these or sneaks an await between them, idle workers would stay parked while AnyIO's abandon_on_cancel=False waits for the thread to return — a permanent hang. Add a test that wraps the scenario in fail_after(2.0) so the hang would surface as a test failure, and a comment in __aexit__ pointing at the test as the guard for the invariant. Verified passing on both asyncio and trio.
for more information, see https://pre-commit.ci
- pyproject: add dev-tools group with ruff/ty/basedpyright so `uv sync --all-groups` installs the lint binaries the workflow runs. - hosting/middleware: install the signal receiver via tg.start (with task_status.started) before the wrapped service runs, so a signal arriving right after READY can't kill the process before open_signal_receiver registers a handler. - ci/tests-free-threaded: add the pure-Python optional groups (dev-http, dev-sentry, click extra) so tests/http and tests/hosting/services collect; ignore tests/openapi (needs the msgspec C ext, not part of the FT subset). - tests/http: gate test_many_requests_served_from_worker_threads on a Barrier so two concurrent in-flight requests force a second worker; the previous handler returned fast enough that one worker could serve all 10 sequentially. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Re-add the ``# noqa: PLC0415`` directives the pre-commit.ci bot stripped:
v0.9.7 of ruff (pinned in .pre-commit-config) doesn't know the rule
and treats the comments as unused, but v0.15 (the workflow's ruff)
still raises PLC0415, so the GH Actions lint-types job fails on the
bare lazy-import sites. Bump the pre-commit ruff to v0.15.12 to align
with dev-tools so the next autoupdate won't strip them again.
- tests-free-threaded: gate three httptools-specific tests in
tests/http/app.py on pytest.importorskip("httptools") so they skip
cleanly on the FT subset, and --ignore tests/hosting/rsgi.py since it
drives lifecycle hooks that pull in granian's mocks but don't run
cleanly on 3.14t.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ests on FT
- tests/http/app.py::TestWorkerPool::test_handlers_run_on_worker_threads
now gates each handler on a 2-thread Barrier so two requests run
concurrently and force a second worker; without the barrier the
pool's single worker can serve all 8 requests sequentially (race
observed on 3.12).
- Add ``pytest.importorskip("httptools")`` to
tests/http/service.py::TestAcceptorTopology::test_serves_requests_httptools
so it skips on the FT subset (httptools isn't installed because 0.7.x
re-enables the GIL on 3.14t).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rename the ``tests-free-threaded`` job to ``3.14t`` so the check name matches uv's interpreter naming. - Drop the ``MemoryStream[T]`` wrapper from ``localpost/_utils.py`` and inline ``anyio.create_memory_object_stream[T](...)`` at the two call sites. SonarCloud's analyzer reads PEP 695 ``class Foo[T]:`` subscription as instance ``__getitem__`` and false-positives on ``MemoryStream[X]``; anyio's own subscription pattern is recognised and the wrapper was a thin pass-through anyway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitHub Actions job IDs must match ^[A-Za-z_][A-Za-z0-9_-]*$; "3.14t" violates both rules (starts with a digit, contains a dot), so the workflow failed to parse. Rename the job to ``tests-3-14t`` and keep "3.14t" as the display ``name:`` so the check label is unchanged. Wire actionlint into pre-commit to catch this class of schema/syntax error locally — it flags the invalid job ID and validates expressions, action refs, and shell snippets via shellcheck. Drive-by: quote the ``$(which python)`` arg flagged by SC2046.
- Bump pre-commit-hooks v5 -> v6, codespell -> v2.4.2, actionlint -> v1.7.12. - Rename ``ruff`` -> ``ruff-check`` (new canonical hook id; old one prints a legacy-alias warning). - Repoint interrogate from ``tests`` to ``localpost`` with ``--fail-under=24`` so it measures library-docstring coverage and prevents regressions (floor matches current coverage; ratchet up as docstrings land). - Add three cheap safety nets from pre-commit-hooks: check-merge-conflict, check-case-conflict, check-added-large-files. - Add ``validate-pyproject`` (catches pyproject.toml schema typos). - Add ``astral-sh/uv-pre-commit`` (``uv-lock``) so the lockfile can't drift from ``pyproject.toml`` without a commit-time failure. - Pin ``minimum_pre_commit_version`` for clarity.
Drop codespell in favour of crate-ci/typos: Rust-based, ~10x faster, identifier-aware tokenizer (handles camelCase / snake_case), and a smaller curated dictionary with materially fewer false positives. The previous ``-L alog -L abl`` allowlist translates to zero entries in typos — both tokens are correctly left alone by its tokenizer. The only allowlisted word now is ``ands`` (genuine verb form for the selector ``ANDs`` test name in tests/benchmarks/http_stacks.py).
Apply typos's preferred spelling (``unparsable`` rather than the equally-valid ``unparseable``) across docs, the static-file handler, and its tests. Includes the ``test_unparseable`` → ``test_unparsable`` rename and the matching docstring on ``_parse_range``.
…ained Restructure CI test jobs so the free-threaded build isn't an oddball single-job sibling of the stable matrix: - ``tests`` matrix: 3.12, 3.13, 3.14, 3.15 (beta). Drop ``--cov`` flags and the upload-artifact step — coverage is owned by sonar now. - ``tests-ft`` matrix (new): 3.14t, 3.15t (beta). Same FT-clean dependency subset and ``PYTHON_GIL=0`` env as before, now applied uniformly across the two free-threaded interpreters. - ``sonarcloud`` now runs pytest itself with coverage (no download-artifact, no ``needs: tests``) on the project-pinned Python from ``.python-version``. Runs in parallel with the test jobs.
Remove the ``lint-types`` job. ruff is already covered by pre-commit.ci on every PR; ty and basedpyright-verifytypes were the only checks unique to this job, and basedpyright-verifytypes has been unmaintainable in CI (requires 100% type completeness; reached 97.2% even after ``--ignoreexternal``). Move both to local-only tooling — ``just types`` and ``just type-coverage`` are still wired up. Also convert the ``tests`` matrix to ``include`` form and add ``continue-on-error: true`` for the 3.15 entry. Python 3.15 is still beta and ``granian``'s PyO3 0.27.2 doesn't compile against it yet — this keeps 3.15 as an early-signal channel without blocking the PR.
- ``localpost/threadtools/_channel.py``: annotate ``ReceiveChannel.__exit__/__aexit__`` and ``SendChannel.__exit__/__aexit__`` with standard ``type[BaseException] | None`` / ``BaseException | None`` / ``TracebackType | None``. Improves the type hints downstream users see when implementing or consuming the channel protocols. - ``pyproject.toml``: ignore ``PLC0415`` (function-level imports) under ``tests/`` — tests legitimately use lazy / conditional imports for optional extras and internals under test. - ``tests/http/backend_parity.py``: move ``import pytest`` to the top of the file and call ``pytest.skip`` directly; the per-function ``import pytest as _pytest`` was tripping both PT013 and PLC0415.
Mirror the ``tests`` matrix: convert ``tests-ft`` to ``include`` form and mark 3.15t as ``experimental: true`` so its failure won't block the PR. Free-threaded 3.15 is at the same beta-readiness as the stable 3.15 build, and there's no reason to gate merges on it.
``just release-check`` prints a pre-release report: - current type completeness (basedpyright --verifytypes, now with ``--ignoreexternal`` so the score reflects our own code only — was ~94% with werkzeug/flask noise, ~97% without) - public-API breaking changes vs the previous stable tag, via ``griffe check`` — same tool Pydantic / FastAPI use for the same purpose ``just api-diff [base]`` is the griffe call on its own. Both recipes default ``base`` to the latest tag matching strictly ``vX.Y.Z`` (no ``b1`` / ``rc1`` / ``.dev0`` suffix), via a justfile-level variable ``previous_stable_tag``. Both are informational (always exit 0); for assessment, not gatekeeping. ``griffe`` is already installed at user level via ``uv tool install griffe`` in ``just doctor``. ``type-coverage`` now uses the ``-`` prefix to match the other type-check recipes — failure shouldn't abort the umbrella.
- Setup: surface ``just doctor`` (user-level toolchain) and the one-time ``pre-commit install`` step, and note the project keeps tools at user level (not in the project venv). - PR checklist: clarify that ``just type-coverage`` is informational now — basedpyright's score reports our public-API completeness but isn't a 100% gate (lint-types was dropped from CI). - Release process: add ``just release-check`` as step 1 (pre-flight review of type coverage + griffe BC report vs the previous stable tag), renumber the remaining steps.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



No description provided.