Skip to content

Fix token_count for discriminated unions#789

Open
BrianPugh wants to merge 1 commit intomainfrom
bugfix/785
Open

Fix token_count for discriminated unions#789
BrianPugh wants to merge 1 commit intomainfrom
bugfix/785

Conversation

@BrianPugh
Copy link
Copy Markdown
Owner

Addresses #785. @AntoninRousset can you please give this a try? thanks!

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 10, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.79%. Comparing base (d003ce3) to head (6e7b5b4).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #789      +/-   ##
==========================================
+ Coverage   89.75%   89.79%   +0.04%     
==========================================
  Files          71       71              
  Lines        8218     8251      +33     
  Branches     1861     1863       +2     
==========================================
+ Hits         7376     7409      +33     
  Misses        477      477              
  Partials      365      365              
Flag Coverage Δ
unittests 89.72% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes incorrect token_count computation for Pydantic discriminated unions (notably within lists), preventing crashes like ValueError: Cannot Union types that consume different numbers of tokens (issue #785).

Changes:

  • Adds a token_count() fast-path for Annotated[..., Field(discriminator=...)] so discriminated unions consume a single token.
  • Adds a regression test covering list[DiscriminatedUnion] inside a Pydantic BaseModel.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
cyclopts/_convert.py Updates token_count() to treat discriminated unions as single-token values.
tests/test_pydantic.py Adds regression coverage for discriminated-union lists in Pydantic models.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cyclopts/_convert.py
# Check before get_parameters strips the Annotated metadata.
from cyclopts.argument.utils import get_annotated_discriminator

if get_annotated_discriminator(resolve_optional(type_)) is not None:
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

get_annotated_discriminator(resolve_optional(type_)) is called without first verifying the hint is Annotated[...]. Since get_annotated_discriminator() currently iterates over get_args(annotation)[1:] for any parameterized type, it can return a non-None value for non-Annotated hints whose type parameters happen to have a discriminator attribute, causing token_count() to incorrectly return (1, False). Consider guarding with is_annotated(...) / get_origin(...) is Annotated, or tightening get_annotated_discriminator() to return None unless the origin is Annotated.

Suggested change
if get_annotated_discriminator(resolve_optional(type_)) is not None:
resolved_optional_type = resolve_optional(type_)
if (
get_origin(resolved_optional_type) is typing.Annotated
and get_annotated_discriminator(resolved_optional_type) is not None
):

Copilot uses AI. Check for mistakes.
Comment thread cyclopts/_convert.py
Comment on lines +890 to +894
# Discriminated unions (e.g. Annotated[Cat | Dog, pydantic.Field(discriminator="type")])
# consume a single JSON string token regardless of member field counts.
# Check before get_parameters strips the Annotated metadata.
from cyclopts.argument.utils import get_annotated_discriminator

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

This introduces a mutual dependency between cyclopts/_convert.py and cyclopts/argument/utils.py (argument.utils already imports convert_enum_flag from _convert). Even though the import here is inside token_count(), the circular module relationship is fragile and can become an import-time failure if future changes call token_count() during module initialization. Consider moving get_annotated_discriminator to a lower-level module (e.g., cyclopts.annotations) or duplicating a minimal discriminator check locally to avoid the cycle.

Copilot uses AI. Check for mistakes.
@AntoninRousset
Copy link
Copy Markdown

Yes it fixes it, thank you!

The reported missing argument is however wrong, when both Home and Cat or Dog have the same attribute name:

from dataclasses import dataclass
from typing import Annotated, Literal

import cyclopts
import pydantic

class AnimalBase(pydantic.BaseModel):
    name: str

class Cat(AnimalBase):
    dead_birds: int
    dead_rats: int
    type: Literal["cat"] = "cat"


class Dog(AnimalBase):
    type: Literal["dog"] = "dog"

Animal = Annotated[Cat | Dog, pydantic.Field(discriminator="type")]

class Home(pydantic.BaseModel):
    animals: list[Animal]
    name: str

app = cyclopts.App()


@app.default
def main(*, home: Home):
    pass


app(["--home.animals", '{"type": "dog"}', '--home.name', "ISS"])

Prints Parameter "--home.name" requires an argument., despite being given. It should instead print:

1 validation error for Home 
  animals.0.dog.name 
    Field required [type=missing, input_value={'type': 'dog'}, input_type=dict]
      For further information visit https://errors.pydantic.dev/2.12/v/missing 

Do you want me to open another issue for this?

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.

3 participants