Skip to content

fix: erase the type arguments of generic models in the System Nexus interceptors - #322

Closed
moetemp wants to merge 19 commits into
moe/up/python-unknown-enum-numberfrom
moe/up/system-nexus-generic-interceptors
Closed

moetemp wants to merge 19 commits into
moe/up/python-unknown-enum-numberfrom
moe/up/system-nexus-generic-interceptors

Conversation

@moetemp

@moetemp moetemp commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What changed?

This PR makes the Python and TypeScript System Nexus interceptors name a generic model with its type arguments erased, as the service definition does. Python writes models.ValueRequest[typing.Any]. The TypeScript hook takes ValueRequest<unknown>, so a hook narrows a value before it uses it.

The Python terminal still passes the model's class as output_type, since the SDK's JSON converter can't decode with a parameterized alias.

The first commit gives multi-operation-service a generic ValueOperation with today's output, and the second is the fix.

The .NET System Nexus dispatch still rejects each concrete type argument of a generic model. Its interceptor partial needs an sdk-dotnet that isn't released, so nothing can compile it yet, and that fix waits for one.

It's based on #321.

Why?

One interceptor handles every type argument of an operation. Python named the model with no type arguments, which basedpyright refused, and TypeScript named a type parameter that isn't in scope. Generic resources (moetemp#78) have generic System Nexus models, so they need this.

How did you test it?

Link to a test plan if any -

  • Unit Tests
  • Staging
  • End to End Tests

At the first commit, basedpyright and tsc fail on the sample's interceptors. A new Python test decodes a generic result with the output type that the terminal passes.

This was referenced Oct 28, 2026
@moetemp
moetemp force-pushed the moe/up/system-nexus-generic-interceptors branch from 5c03b49 to e3c7d8a Compare October 29, 2026 04:00
@moetemp
moetemp force-pushed the moe/up/python-unknown-enum-number branch from 11c4295 to 44f3b6d Compare October 29, 2026 06:28
@moetemp
moetemp force-pushed the moe/up/system-nexus-generic-interceptors branch from e3c7d8a to 0fe04d5 Compare October 29, 2026 06:28
@moetemp
moetemp force-pushed the moe/up/python-unknown-enum-number branch from 44f3b6d to 517b216 Compare October 29, 2026 07:00
@moetemp
moetemp force-pushed the moe/up/system-nexus-generic-interceptors branch from 0fe04d5 to 3c0cbb6 Compare October 29, 2026 07:00
The sample takes Brook's WIT, on the Stream protos of temporalio/api#895 at 482ec010. No SDK has
those protos, so the sample projects hold their bindings. Only .NET generates the service, since
Python and TypeScript panic on the read item's oneof.
Python and TypeScript panic on it, so their outputs keep their previous content. .NET types each
case as the raw proto message, not as its record.
Variant planning kept such a case as the raw proto message. Python and TypeScript then panicked
on a message conversion that wasn't model-shaped, and .NET typed the case as the proto class.
The case plans as the record, as a record field does. A .NET case record names its payload's
model in full, since it hides a model of its own name. Brook's stream service generates for
TypeScript, and its .NET read item holds the records.
nexgen maps every temporal/... proto to a temporalio module. The Stream protos aren't in
temporalio yet, so the service's imports don't resolve, and basedpyright skips it.
nexgen ships `proto/nexus/options.proto`, whose `nexus.python_module` file option names the
Python module of a file's messages and enums. nexgen reads it from the raw descriptor bytes,
since prost drops extension fields. The stream sample's protos set it, so Python imports them
from the sample's streamprotos package, and basedpyright checks the stream service.
A proto-backed request holds a 64-bit integer as a `Long`, but a resource method took a number. A
Brook offset doesn't fit in a number. A resource field or method param that a proto message of
the request reads is a `Long`, so it goes into the request as it is. A 64-bit map key there is a
string, as in the request model. Brook's stream service type-checks.
Python's client of a service with no endpoint sets up a flattened record at the module indent, so
its services.py doesn't parse and the Python output keeps its previous content.
The setup of a @nexus.flatten-in-api record always took the indent of a module function. In a
method of the client of a service with no endpoint, the code then left the method body, and
services.py didn't parse. The user-service client generates its update_profile method.
Before planning, a proto-backed record is named by its proto type, so the walk for a resource's
type parameters stopped there. nexgen then wrote generic models for a resource class that isn't
generic, and the code didn't compile. The checks of the resource and of the operations that its
methods bind both walk through protos.
The flag has the name of the user's own email, with another type. Today's output fills the flag
with the user's email.
A resource method filled each request field from the resource field or param of the same name,
whatever its type. So the user's own email went into the notify request's email flag. A source
binds only when its type is the field's type, or the inner type of an optional field.
Python renders a bare `*` with no keyword parameter after it, in the user-service client method
and in the type-showcase operation function. So those files don't parse, and the Python output
keeps its previous content.
Python refuses a bare `*` with no parameter after it. An unpacked operation with an empty request
rendered one, in the client of a service with no endpoint and in an operation function. The
user-service client generates its `notify_all` method, and type-showcase its `ping` function.
Python's client method takes the docstring indent of a module function, so its services.py
doesn't parse and the Python output keeps its previous content.
The client of a service with no endpoint took the docstring indent of a module function, so a
documented or experimental operation broke the module. The user-service client generates with
the doc of `notify_all`.
Proto3 enums are open. Python fails the whole message on a Stream status that it doesn't know.
TypeScript and .NET pass the number through, and their tests pin it.
Proto3 enums are open, but a generated Python enum raised `ValueError` on a number that it
didn't know, so the whole message failed. A newer server's Producer kind stopped an old reader's
poll loop. The enum now makes a member named `Unknown<number>` with that value, as TypeScript and
.NET pass the number through.
Its System Nexus interceptors name the generic models without type arguments. basedpyright
refuses the Python one. The TypeScript one names a type parameter that isn't in scope, so `tsc`
fails.
An interceptor handles every type argument of an operation's models. Python named a generic model
with no type arguments, which basedpyright refuses. TypeScript named the operation's own type
parameter, which isn't in scope. Both erase them, as the service definition does. The Python
terminal still passes the model's class as the output type, since the SDK decodes with it.
@moetemp
moetemp force-pushed the moe/up/system-nexus-generic-interceptors branch from 3c0cbb6 to fd062d3 Compare October 29, 2026 07:35
@moetemp
moetemp force-pushed the moe/up/python-unknown-enum-number branch from 2509f52 to 6aa1acb Compare October 29, 2026 09:29
@moetemp

moetemp commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Closing this. moetemp#78 takes the fix, where the stream sample needs it.

@moetemp moetemp closed this Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant