feat: carry the context out of the process, with export() and adopt() - #9
Open
paqstd-dev wants to merge 1 commit into
Open
feat: carry the context out of the process, with export() and adopt()#9paqstd-dev wants to merge 1 commit into
paqstd-dev wants to merge 1 commit into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9 +/- ##
==========================================
Coverage 100.00% 100.00%
==========================================
Files 11 12 +1
Lines 1100 1214 +114
Branches 137 164 +27
==========================================
+ Hits 1100 1214 +114 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
Summary
wrapandExecutorcarry the context into threads, and nothing carried it anywhere else.The boundaries that matter in a real service are mostly elsewhere, a
ProcessPoolExecutor, a Celery task, a message on a queue, an outgoing HTTP call, and every project hand-rolls the same four lines per boundary and gets them wrong differently each time.Implements
_backlog/planned/RFC-0006-portable-context.The boundary names what travels, not the provider
The RFC proposed
provider("trace", portable=True, ...), and that is not what this does.A new
provider()keyword has to pass the two tests indesign.rst, and the second is that nothing at the call site can say the same thing.export("trace", "audit")says it exactly, on the line where the developer is already thinking about serialisation.What the flag was defended with was error attribution, the scalars-only check running at
provider()time so a bad value is reported by the line that made it.That does not survive the mutable case, since a
Namespacestays writable while its block is open andctx.tags.append(conn)is ordinary code, so an entry-time check can only say the value was portable once.So
provider()is untouched, keeps four reserved names, and no hot path changed, which is why the benchmark table is not part of this PR.Rules
Nothing travels unless
export()names it, so what leaves the process is one reviewable line rather than a mark spread across providers.Values are checked as exactly
str,int,float,bool,None,listordict, all the way down.Exactly, because a
UUID, aStrEnummember or atuplewould arrive as a different type than the one provided, and a boundary that quietly changes a type is worse than one that refuses.The message names the provider, the path to the value and its type.
The envelope is
{"v": 1, "ctx": {...}}.Two keys rather than the RFC's flat mapping, where a provider named
vwould collide with the version field, and both are frozen for every future version so any version can be read far enough to report its own number.adopt()layers ordinaryprovider()calls through anExitStack, so shadowing, unwinding on an exception, thedebug()ledger and exception notes all come for free.The namespaces are handed over by key, so a payload attribute named
frozenorkeystays data instead of becoming a parameter.Class-keyed providers do not travel, since a class cannot be reopened on the other side without importing it.
An adopted payload is input, and the docs say so in those words.
adopt()checks that a payload is shaped like an envelope and cannot check that it is true.Codecs
set_codec(dump=..., load=...)registers one process-wide pair that maps a namespace's values at each end, for what the JSON rule refuses.A
dumpresult is checked exactly as an unencoded value is, so a codec widens what you may provide and never what goes on the wire.A pickle codec therefore renders itself as a string, which is the point rather than an obstacle, since the queue and header recipes need the envelope to survive
json.dumpsand the decision to run arbitrary code from a payload belongs where a reader can see it.loadruns after the payload has been checked, never before.The how-to shows three, a tagged dataclass codec that runs on the standard library alone, an msgspec one that validates an incoming payload against the type it claims to be, and what pickle costs.
Public surface
Four names, 23 to 27.
EnvelopeVersionErroris a class of its own because the handling case is real, a consumer a release behind a producer wants to log and run without the context, andexcept ValueErroraround awith adopt(...)block would swallow whatever the body raises as well.Not in this change
No Celery integration, no HTTP client, no OpenTelemetry bridge, so
scope.rststill says what it said.The adapters are how-to pages, and each program was extracted from its
.rstand run before the page shipped.Checklist
make -kpasses: lint, mypy, pyright, 100 percent branch coverage, docs, workflow audit.pyproject.tomlwith a comment saying why.