Skip to content

Rework nested models#47

Merged
loichuder merged 5 commits into
mainfrom
rework-nested-models
Apr 21, 2026
Merged

Rework nested models#47
loichuder merged 5 commits into
mainfrom
rework-nested-models

Conversation

@loichuder

Copy link
Copy Markdown
Member

Fix #46

@loichuder loichuder marked this pull request as draft April 1, 2026 12:59
@codecov

codecov Bot commented Apr 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.98246% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/ewokssphinx/ewoks_task_utils.py 92.00% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@loichuder loichuder force-pushed the rework-nested-models branch 2 times, most recently from a9c5aa2 to 6b0a7dd Compare April 2, 2026 07:10
Comment thread src/ewokssphinx/tests/dummy_tasks_nested.py
Comment thread src/ewokssphinx/tests/test_utils.py
Comment thread src/ewokssphinx/ewoks_task_utils.py Outdated
Comment thread src/ewokssphinx/ewoks_task_utils.py Outdated
Comment thread src/ewokssphinx/ewoks_task_utils.py
@loichuder

Copy link
Copy Markdown
Member Author

Works fine with ewoksndreg and ewoks3dxrd

@loichuder loichuder requested a review from woutdenolf April 2, 2026 07:23
@loichuder loichuder force-pushed the rework-nested-models branch from f4d1023 to cbae215 Compare April 2, 2026 07:26
@loichuder loichuder marked this pull request as ready for review April 2, 2026 07:28
@woutdenolf

woutdenolf commented Apr 2, 2026

Copy link
Copy Markdown
Member

I would propose parse_pydantic_model to take the class, not the qualname:

def parse_pydantic_model(model_cls: Type[BaseModel] | None) -> list[ParameterDescription]:
    parameters = []
    if model_cls is None:
        return parameters

    if not issubclass(model_cls, BaseModel):
        raise ValueError("Not a pydantic model")
    for name, field_info in model_cls.model_fields.items():
        parameters.append(_parse_pydantic_field(name, field_info))
    return parameters

Not sure why parse_pydantic_model this is not a private method. I think it should be.

Then we can avoid the qualname-import roundtrip

input_model = task.get("input_model")
if input_model is not None:
    input_model = _import_model(input_model)
    inputs = parse_pydantic_model(input_model)
else:
    inputs = _parse_parameter_names(
        task.get("required_input_names", []),
        task.get("optional_input_names", []),
    )

output_model = task.get("output_model")
if output_model:
    output_model = _import_model(output_model)
    outputs = parse_pydantic_model(output_model)
else:
    outputs = _parse_parameter_names(task.get("output_names", []), [])

submodels = extract_submodels(input_model, output_model)

The extract_submodels method (should probably be private as well)

def extract_submodels(*models: Type[BaseModel] | None) -> dict[str, List[TaskParameter]]:
    models = tuple(m for m in models if m is not None)

    if not models:
        return {}

    annotation = reduce(lambda a, b: a | b, models)

    return {
        qualname(model_cls): parse_pydantic_model(model_cls)
        for model_cls in _iter_models(annotation, set())
        if model_cls not in models
    }

@loichuder loichuder force-pushed the rework-nested-models branch 2 times, most recently from 78c3a7d to 3d9d0c7 Compare April 2, 2026 12:46
@loichuder

Copy link
Copy Markdown
Member Author

Thanks for the review!

I changed extract_submodels accordingly but it felt more natural to me to still have a single model as input. I also made all functions private as advised.

@woutdenolf

woutdenolf commented Apr 2, 2026

Copy link
Copy Markdown
Member

it felt more natural to me to still have a single model as input

The reason I propose extract_submodels to work on a single annotation that combines both models is that the ordering of the result will be better and clearer imo: order is provided as declared (depth-first).

If you call extract_submodels for both models individually you have to merge the results and make an extra decision on the ordering and the duplicates. This is a problem when the inputs and outputs have shared models. For example

class Shared(BaseModel):

class Model1(BaseModel):
    name: str
    shared: Shared

class Model2(BaseModel)
    name: str
    shared: Shared

class InputModel(BaseInputModel):
    var: Model1

class OutputModel(BaseOutputModel):
    var: Model2
  • When extract_submodels runs on one single annotation, the order is as declared: Model1, Shared, Model2.
  • When extract_submodels runs on the individual models and we merge later: Model1, Model2, Shared.

When I start reading the docs I would want to know everything about InputModel and Model1 which involves Shared before I continue to OutputModel and Model2. That's how I would read the code as well.

Edit: even if we would prefer breadth-first instead of depth-first like we have now, we would still want only one extract_submodels imo.

@loichuder

Copy link
Copy Markdown
Member Author

The reason I propose extract_submodels to work on a single annotation that combines both models is that the ordering of the result will be better and clearer imo: order is provided as declared (depth-first).

I hadn't thought of that! Yes, in this case, you are right, it is best to do as you said.

@loichuder loichuder force-pushed the rework-nested-models branch from 3caf39c to 58ead0a Compare April 20, 2026 14:39
@loichuder

Copy link
Copy Markdown
Member Author

@woutdenolf Should be good now!

@loichuder loichuder merged commit 3a798b3 into main Apr 21, 2026
8 checks passed
@loichuder loichuder deleted the rework-nested-models branch April 21, 2026 07:08
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.

additional_model_nodes: annotation parsing is brittle and can cause failures

2 participants