Skip to content

Resolve model rebuild issue - #7519

Open
edan-bainglass wants to merge 15 commits into
aiidateam:mainfrom
edan-bainglass:resolve-model-rebuild-issue
Open

Resolve model rebuild issue#7519
edan-bainglass wants to merge 15 commits into
aiidateam:mainfrom
edan-bainglass:resolve-model-rebuild-issue

Conversation

@edan-bainglass

@edan-bainglass edan-bainglass commented Aug 4, 2026

Copy link
Copy Markdown
Member

Supersedes #7518

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Dynamic Pydantic model factories now pass copied configuration and qualified names to create_model. Minimal models copy fields before modification and cache derived models on the source class. Node field mappings use general dictionary declarations.

Changes

Dynamic model generation

Layer / File(s) Summary
Minimal model construction
src/aiida/orm/pydantic.py
The minimal-model factory caches derived models on the source class, copies field definitions, and creates models with copied configuration and extra='ignore'.
Compatibility and write-model construction
src/aiida/orm/entities.py
Compatibility and write models pass copied configuration and qualified names directly to pydantic.create_model.
Node and CLI model wiring
src/aiida/orm/nodes/node.py, src/aiida/orm/nodes/data/code/abstract.py
Dynamic node and CLI models pass qualified names and copied configuration during creation. Node field mappings use dict[str, Any] declarations.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: agoscinski

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description only identifies a superseded pull request and does not summarize the model rebuild changes. Add a brief summary of the Pydantic model creation and typing changes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: resolving a model rebuild issue.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/aiida/orm/pydantic.py (1)

72-82: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Copy FieldInfo before modifying nested-model metadata.

field references cls.model_fields[key]. The code changes field.annotation and field.default_factory before line 82 copies it. After minimal-model creation, non-minimal serialization can use the nested minimal model and omit fields marked may_be_large.

Proposed fix
-        for key, field in cls.model_fields.items():
+        for key, source_field in cls.model_fields.items():
+            field = deepcopy(source_field)
             annotation = field.annotation
             if get_metadata(field, 'may_be_large'):
                 continue
             if isinstance(annotation, type) and issubclass(annotation, OrmModel):
                 sub_minimal_model = annotation._as_minimal_model()
                 field.annotation = sub_minimal_model
                 if any(f.is_required() for f in sub_minimal_model.model_fields.values()):
                     field.default_factory = None
-            model_fields[key] = (field.annotation, deepcopy(field))
+            model_fields[key] = (field.annotation, field)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiida/orm/pydantic.py` around lines 72 - 82, Deep-copy each field’s
FieldInfo before changing nested-model metadata in the minimal-model
construction flow around _as_minimal_model. Apply annotation and default_factory
updates only to the copy, then store that copied field in model_fields, leaving
cls.model_fields unchanged so regular serialization retains the full nested
model.
🧹 Nitpick comments (1)
src/aiida/orm/entities.py (1)

561-575: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document copy_decorator and assign the exception message.

Add Sphinx :param:, :return:, and :raises: fields to the helper docstring. Assign the RuntimeError message to msg before raising it.

As per coding guidelines, “Use Sphinx-style docstrings (:param:, :return:, :raises:)” and “Assign exception messages to a variable before raising.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiida/orm/entities.py` around lines 561 - 575, Update copy_decorator’s
docstring with Sphinx :param: entries for model_cls, name, and decorator, plus
:return: and :raises: descriptions. In the missing-attribute branch, assign the
RuntimeError message to a msg variable before raising it.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/aiida/orm/pydantic.py`:
- Around line 72-82: Deep-copy each field’s FieldInfo before changing
nested-model metadata in the minimal-model construction flow around
_as_minimal_model. Apply annotation and default_factory updates only to the
copy, then store that copied field in model_fields, leaving cls.model_fields
unchanged so regular serialization retains the full nested model.

---

Nitpick comments:
In `@src/aiida/orm/entities.py`:
- Around line 561-575: Update copy_decorator’s docstring with Sphinx :param:
entries for model_cls, name, and decorator, plus :return: and :raises:
descriptions. In the missing-attribute branch, assign the RuntimeError message
to a msg variable before raising it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 264d0491-791f-488a-b47b-01c98516e3d0

📥 Commits

Reviewing files that changed from the base of the PR and between 8a2747e and e1fedf4.

📒 Files selected for processing (4)
  • src/aiida/orm/entities.py
  • src/aiida/orm/nodes/data/code/abstract.py
  • src/aiida/orm/nodes/node.py
  • src/aiida/orm/pydantic.py

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.61538% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.65%. Comparing base (8a2747e) to head (ed43c48).

Files with missing lines Patch % Lines
src/aiida/orm/pydantic.py 83.34% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7519      +/-   ##
==========================================
- Coverage   80.67%   80.65%   -0.01%     
==========================================
  Files         581      581              
  Lines       46998    46989       -9     
==========================================
- Hits        37909    37894      -15     
- Misses       9089     9095       +6     

☔ View full report in Codecov by Harness.
📢 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.

@edan-bainglass

Copy link
Copy Markdown
Member Author

@agoscinski most mypy complaints are due to the type signature of Pydantic's create_model. I recall this issue being one of the reasons why I patched after the fact. Anyhow, working on resolving this now.

Comment thread src/aiida/orm/entities.py Outdated
Comment thread src/aiida/orm/entities.py Outdated
Comment thread src/aiida/orm/entities.py Outdated
Comment thread src/aiida/orm/entities.py
type[OrmModel],
pdt.create_model(
'CliModel',
__config__=deepcopy(cls.ConstructorArgsModel.model_config) | {'arbitrary_types_allowed': True},

@edan-bainglass edan-bainglass Aug 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think maybe the deepcopy here (and on __config__ in general) is not necessary 🤔 Will check

@agoscinski agoscinski Aug 4, 2026

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.

Lets keep deepcopy to be safe. It seems to not hurt the performance significant. Can we take some reasonable default from a parent class for | {'arbitrary_types_allowed': True} ?. Having this default in AbstractCode looks like a hardcoded hack.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm not sure what you mean by default here, but in any case, I cannot recall why I allowed arbitrary types on the code schema (tests pass without it). Looking into it. While I'm at it, I will revisit all config overrides (not many).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm failing to reproduce any issue when leaving arbitarary_types_allowed=False 🤷🏻‍♂️

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Removed

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.

So I am fine how it is now. With | {'arbitrary_types_allowed': True} you introduced a default value if the other one is None. I think it is okay to have a default value for this pydantic models but not in the abstract code. Now the default value is in src/aiida/orm/pydantic.py which is okay for me.

@edan-bainglass edan-bainglass Aug 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oh. That's not an or operator. It is a merge operation on dictionaries (| and & set operators). So the old config + {the mutation}. See here for another example.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I can't find an issue without it though. I imagine at some point of my testing, I had an issue with this sort of serialization. My guess is the code changed and I hadn't updated this part yet. In addition, see comment in ae3fb70 regarding redundant __config__ redefinitions.

@agoscinski agoscinski Aug 4, 2026

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.

Right I mixed it with or usage. Anyway, its not in abstract code anymore so I am fine with it.

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.

ae3fb70 okay.

@edan-bainglass

edan-bainglass commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

@agoscinski most mypy complaints are due to the type signature of Pydantic's create_model. I recall this issue being one of the reasons why I patched after the fact. Anyhow, working on resolving this now.

The issue appears to be with the type of model_fields. Fixing... (update - this is now resolved)

@edan-bainglass
edan-bainglass force-pushed the resolve-model-rebuild-issue branch from b77b3c4 to feb39df Compare August 4, 2026 09:20

@agoscinski agoscinski left a comment

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.

I cannot really reason on this meta class logic changes especially in src/aiida/orm/pydantic.py. We already discussed that there will be a redesign. I hope the meta class logic will there be drastically reduced as it is impossible for me now to understand all the details about it. I checked that this does not introduce another performance regression.

@edan-bainglass

Copy link
Copy Markdown
Member Author

I checked that this does not introduce another performance regression.

Which part?

@agoscinski

agoscinski commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Sorry for not being clear. I checked it (by running the production benchmarks), and it does not introduce another performance regression. Its all good.

They are only needed if modifying (see minimal model) or if there are
multiple inheritance (see write model). In all other cases, the default
`__config__` is already set in the base class and does not need to be
redefined.
@edan-bainglass

Copy link
Copy Markdown
Member Author

Sorry for not being clear. I checked it (by running the production benchmarks), and it does not introduce another performance regression. Its all good.

To be clear, do we still have a performance regression w.r.t pre-#6990? You mentioned there was still something there.

@agoscinski

agoscinski commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

So it might be that if you completely remove the meta class initialization and inheritance from pydantic classes inside the orm classes then you gain another 10% but I did not verify this rigorously. Here the code changes agoscinski@e0f7e55#diff-859fbb51fc0e6832b53b0e85123464fd19c508c4778a73a7514c2f7ada8fa8ce Note that I also did not factor out variance in the benchmark. This effects the estimate of 10%. So far I only care about large shifts in the benchmarks. It is too much work at the moment to consider smaller shifts as well. Also completely separating the pydantic orm models from the python orm objects for v2.9.0 is an unreasonable workload.

@edan-bainglass

Copy link
Copy Markdown
Member Author

So it might be that if you completely remove the meta class initialization and inheritance from pydantic classes inside the orm classes then you gain another 10% but I did not verify this rigorously. Here the code changes agoscinski@e0f7e55#diff-859fbb51fc0e6832b53b0e85123464fd19c508c4778a73a7514c2f7ada8fa8ce Note that I also did not factor out variance in the benchmark. This effects the estimate of 10%. So far I only care about large shifts in the benchmarks. It is too much work at the moment to consider smaller shifts as well. Also completely separating the pydantic orm models from the python orm objects for v2.9.0 is an unreasonable workload.

Thanks @agoscinski. I was asking as part of my assessment for the refactor work in v3. Just wanted to know if improvements are still warranted quantitatively. Cheers.

@agoscinski

Copy link
Copy Markdown
Collaborator

In any case, even if there would be no performance benefit, I would decouple the orm pydantic models for the python objects since the models are not needed for running calculations and a coupling makes the maintenance of modules that do not require the pydantic serialization harder. I feel like the pydantic validation is not a strong enough reason to keep pydantic models for the construction of the python object, especially not when we have this complex metaclass logic.

@edan-bainglass

Copy link
Copy Markdown
Member Author

In any case, even if there would be no performance benefit, I would decouple the orm pydantic models for the python objects since the models are not needed for running calculations and a coupling makes the maintenance of modules that do not require the pydantic serialization harder. I feel like the pydantic validation is not a strong enough reason to keep pydantic models for the construction of the python object, especially not when we have this complex metaclass logic.

Yes. That is indeed the plan regardless. Just planning out my priorities across several projects 🙂 Thanks

@agoscinski

agoscinski commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

So one thing, because removing the model rebuild did not cause any failure in the aiida-core test suite but was clearly required for the aiida-restapi. Can we add tests for this? So basically the change in 987a56f would have failed with this additional test.

Comment on lines +654 to +656
cls.WriteModel.model_json_schema()['$defs']['AttributesWriteModel']['title']
== AttributesWriteModel.model_json_schema()['title']
== f'{name}AttributesWriteModel'

@edan-bainglass edan-bainglass Aug 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

To clarify, the issue was a wrong title in the $defs part of the JSON schema (tested in the REST API), but clearly not on the model itself, since the existing test here passed. However, if you simply add the $defs check after, it passes, because the check on the model triggers the deferred model build, making the $defs check miss the mark. Hence, we check equality simultaneously.

@edan-bainglass

Copy link
Copy Markdown
Member Author

@agoscinski okay, test added. Good to merge?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

2 participants