Skip to content

Python SDK: support phase="both" as decorator sugar #37

Description

@olaservo

Follow-up to #30, from @jeongukjae's review thread on types.py about the both phase.

Context

The SEP is explicit that both is not a wire value (docs/sep.md, Hooks section):

Hooks define which Lifecycle Events trigger an interceptor's invocation. The hooks array in the interceptor definition contains one or more entries, each declaring a set of Lifecycle Events (events) and a phase (phase: "request" or "response") where this interceptor will be invoked. Interceptors that run on both phases use two entries — one per phase.

The C# SDK follows this: InterceptorPhase.Both is documented as valid only at the attribute/SDK convenience layer, and the reflection layer expands it into two InterceptorHook entries before anything reaches the wire.

The Python SDK currently has no equivalent convenience — Phase = Literal["request", "response"] is used both as the wire type and as the decorator parameter, so an interceptor that runs on both phases must pass an explicit two-entry hooks=[...].

Proposal

Add both as decorator-level sugar only, matching the C# model:

  • Introduce a separate literal for the convenience layer (e.g. HookPhase = Literal["request", "response", "both"]) and accept it as the phase= parameter on Interceptors.validator(), .mutator(), and .sink().
  • Expand it inside build_hooks() in interceptor.py into two Hook entries, one per phase.
  • Leave the wire type Phase = Literal["request", "response"] unchanged, so Hook, InterceptorResult, and the invoke/execute params keep rejecting both.
  • Add a test asserting that phase="both" produces exactly two hook entries and that Hook(phase="both") still fails validation.

This is purely additive and has no wire impact.

Non-goal

Accepting both on the wire. Hook matching in chain.py should keep comparing against request/response only — the expansion happens at registration time, not at dispatch time.

Related: cross-SDK divergence

Worth tracking separately, but it's the reason this came up. The Go SDK takes the dispatch-time approach rather than the expansion approach: PhaseBoth is a value of InterceptionPhase, which is the type of the JSON-serialized Hook.Phase field, and matchesHooks in chain/chain.go treats PhaseBoth as matching any target phase. That means a Go interceptor declaring both serializes "phase": "both" in its interceptors/list response, which the SEP disallows and which the Python and C# SDKs would reject when parsing. Filing a separate issue for that.

cc @PederHP @Degiorgio @jeongukjae

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions