Skip to content

feat: decode an unknown proto enum number as a marked fallback case - #321

Merged
tconley1428 merged 4 commits into
mainfrom
moe/up/python-unknown-enum-number
Nov 2, 2026
Merged

tconley1428 merged 4 commits into
mainfrom
moe/up/python-unknown-enum-number

Conversation

@moetemp

@moetemp moetemp commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What changed?

This PR adds @nexus.fallback, which marks one case of a proto-backed enum. When a proto value converts to the model, a number that the enum doesn't know becomes that case, in Python, TypeScript and .NET. An enum without the mark keeps its behavior, so Python still raises.

  • nexgen refuses the directive on an enum without @nexus.proto, on a second case of one enum, and anywhere other than an enum case.
  • The stream WIT marks the unspecified case of each enum that a server sends and can extend. In the stream output, those enums decode an unknown number as their Unspecified case.

The first commit adds tests that decode a DescribeStreamResponse with status 9 and expect today's result, and the second is the change.

Why?

Proto3 enums are open, so a newer server can send a case that an older reader doesn't know. In Python, that failed the whole message, so one new close status could fail a whole read page. The WIT names the case, so an enum that has no good default stays strict.

How did you test it?

Link to a test plan if any -

  • Unit Tests
  • Staging
  • End to End Tests

The stream tests in Python, TypeScript and .NET decode status 9 as StreamStatusUnspecified. A Python test checks that StreamReadStart, which has no fallback, still raises. Rejection fixtures cover the refusals.

This was referenced Oct 28, 2026
@moetemp
moetemp force-pushed the moe/up/python-unknown-enum-number branch from 6aa1acb to 66eee48 Compare October 29, 2026 17:08
@moetemp
moetemp force-pushed the moe/up/ts-long-resource-params branch from e1d283e to eeab36c Compare October 29, 2026 17:18
@moetemp
moetemp force-pushed the moe/up/python-unknown-enum-number branch from 66eee48 to df16584 Compare October 29, 2026 17:18
@moetemp
moetemp marked this pull request as ready for review October 29, 2026 18:17
@moetemp
moetemp requested a review from a team as a code owner October 29, 2026 18:17
@moetemp
moetemp force-pushed the moe/up/python-unknown-enum-number branch from df16584 to 5d3c1c0 Compare October 29, 2026 18:27
@moetemp
moetemp force-pushed the moe/up/ts-long-resource-params branch from eeab36c to 2986e8f Compare October 29, 2026 18:27
@moetemp
moetemp force-pushed the moe/up/python-unknown-enum-number branch from 5d3c1c0 to fd46000 Compare October 30, 2026 00:00
@moetemp
moetemp force-pushed the moe/up/ts-long-resource-params branch 2 times, most recently from c7b747b to 08816c8 Compare October 30, 2026 01:00
@moetemp
moetemp force-pushed the moe/up/python-unknown-enum-number branch from fd46000 to 412faaa Compare October 30, 2026 01:00
@moetemp moetemp changed the title fix(python): keep an unknown enum number feat: decode an unknown proto enum number as a marked fallback case Oct 30, 2026
@moetemp
moetemp force-pushed the moe/up/python-unknown-enum-number branch from 412faaa to e6ab98d Compare October 30, 2026 04:56

[error]
kind = "invalid-wit-directive"
contains = ["@nexus.fallback", "case `severity-unspecified`", "does not take arguments"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was feedback to try to keep this to single strings when possible since it is more readable. I added it to the agents instructions, but I think it wasn't in yet at the time.

@moetemp moetemp Oct 31, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Changed it in 5828c6a. Each kept fixture now asserts two full clauses, the directive with its location and then the reason, the same as experimental-arguments. The input path sits between them, so one string can't match on every machine.

interface models {
/// @nexus.fallback
/// @nexus.proto "temporal.api.enums.v1.Severity"
enum alert-level {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eeeh. I would review some of these negative cases. Like do we really need to test every annotation for being put in the wrong place? I am going to make a set of these as part of the issue for always checking the annotation is valid.

@moetemp moetemp Oct 31, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I dropped the two placement fixtures, on the enum and on a record field, in 5828c6a. I kept the placement checks, so a misplaced fallback still fails at generation time until your generic check covers it. Happy to drop them too if you'd rather keep this PR smaller.

ExecutionTypeNexusOperation = 3


def _execution_type_from_number(value: int) -> ExecutionType:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't call it from "number" in python.

@moetemp moetemp Oct 31, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed it to _<enum>_from_proto in 3a6fc36, to match from_transfer_type. I kept it a separate decode function and not _missing_ on the enum. The fallback should apply only to values that come from the wire, as in TS and .NET. With _missing_, ExecutionType(99) in user code would quietly give the fallback too. Happy to switch if you prefer _missing_.

@moetemp moetemp left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tconley1428 Thanks for the comments.


[error]
kind = "invalid-wit-directive"
contains = ["@nexus.fallback", "case `severity-unspecified`", "does not take arguments"]

@moetemp moetemp Oct 31, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Changed it in 5828c6a. Each kept fixture now asserts two full clauses, the directive with its location and then the reason, the same as experimental-arguments. The input path sits between them, so one string can't match on every machine.

interface models {
/// @nexus.fallback
/// @nexus.proto "temporal.api.enums.v1.Severity"
enum alert-level {

@moetemp moetemp Oct 31, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I dropped the two placement fixtures, on the enum and on a record field, in 5828c6a. I kept the placement checks, so a misplaced fallback still fails at generation time until your generic check covers it. Happy to drop them too if you'd rather keep this PR smaller.

ExecutionTypeNexusOperation = 3


def _execution_type_from_number(value: int) -> ExecutionType:

@moetemp moetemp Oct 31, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed it to _<enum>_from_proto in 3a6fc36, to match from_transfer_type. I kept it a separate decode function and not _missing_ on the enum. The fallback should apply only to values that come from the wire, as in TS and .NET. With _missing_, ExecutionType(99) in user code would quietly give the fallback too. Happy to switch if you prefer _missing_.

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, so a newer server can send a case that an older reader doesn't know.
`@nexus.fallback` on one case of a proto-backed enum makes Python, TypeScript and .NET decode such
a number as that case. An enum without it keeps its behavior, so Python still raises.
The generic annotation check will cover placement. The kept fixtures assert the directive clause and the reason, since the input path sits between them.
@moetemp
moetemp force-pushed the moe/up/python-unknown-enum-number branch from 3ed36a9 to d2e8d90 Compare October 31, 2026 03:12
@moetemp
moetemp changed the base branch from moe/up/ts-long-resource-params to main October 31, 2026 03:12
@moetemp
moetemp requested a review from tconley1428 October 31, 2026 03:16
It matches `from_transfer_type`. The fallback applies to decoded values only, so building the enum from an int stays strict.

Co-authored-by: Tim Conley <timothy.conley@temporal.io>
@moetemp
moetemp force-pushed the moe/up/python-unknown-enum-number branch from d2e8d90 to 3a6fc36 Compare October 31, 2026 03:18
@tconley1428
tconley1428 merged commit 423b164 into main Nov 2, 2026
5 checks passed
@tconley1428
tconley1428 deleted the moe/up/python-unknown-enum-number branch November 2, 2026 16:46
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.

2 participants