Skip to content

Conversation

@edan-bainglass
Copy link
Member

@edan-bainglass edan-bainglass commented Nov 7, 2025

This PR extracts the nested ORM entity models out to the module level for consistency. It builds on the work of #6255 and #6990. Similar to what was done in #5183 for collections.

The of this PR is as follows (referencing the example below):

  1. It lifts the nested model to the module level and properly names it with the corresponding entity prefix
  2. It adds a Model class attribute as a type alias for the module-level model

2 ensures that much of the existing model mechanics are retained while avoiding the nested model.

Before

class SomeEntity(SomeBase):
   class Model(SomeBase.Model):
      ...

After

class SomeEntityModel(SomeBaseModel):
   ...

class SomeEntity(SomeBase):
   Model: TypeAlias = SomeEntityModel

Why do this?

I found it difficult type hinting for the nested model system. I had hoped that providing the model at the module level will resolve this matter, for example, by allowing the definition of EntityModelType = TypeVar('EntityModelType', bound=EntityModel), instead of binding it to Entity.Model, which seemed to not work with mypy.

It ended up being a bit of a typing hell. However, I am uncertain if it is due to the design or a misunderstanding of typing on my part. Help welcomed @danielhollas 🙏

@codecov
Copy link

codecov bot commented Nov 7, 2025

Codecov Report

❌ Patch coverage is 74.14075% with 158 lines in your changes missing coverage. Please review.
✅ Project coverage is 24.35%. Comparing base (5c1b2f4) to head (1a2c038).

Files with missing lines Patch % Lines
src/aiida/orm/entities.py 42.19% 37 Missing ⚠️
src/aiida/orm/nodes/data/array/kpoints.py 32.56% 29 Missing ⚠️
src/aiida/orm/nodes/data/array/array.py 42.86% 16 Missing ⚠️
src/aiida/orm/nodes/node.py 70.00% 15 Missing ⚠️
src/aiida/orm/nodes/repository.py 23.08% 10 Missing ⚠️
src/aiida/cmdline/commands/cmd_code.py 0.00% 8 Missing ⚠️
src/aiida/orm/nodes/data/array/trajectory.py 70.38% 8 Missing ⚠️
src/aiida/orm/nodes/data/code/installed.py 56.25% 7 Missing ⚠️
src/aiida/orm/nodes/data/singlefile.py 69.57% 7 Missing ⚠️
src/aiida/cmdline/groups/dynamic.py 0.00% 5 Missing ⚠️
... and 9 more

❗ There is a different number of reports uploaded between BASE (5c1b2f4) and HEAD (1a2c038). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (5c1b2f4) HEAD (1a2c038)
2 1
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #7093       +/-   ##
===========================================
- Coverage   77.61%   24.35%   -53.25%     
===========================================
  Files         566      566               
  Lines       43546    43720      +174     
===========================================
- Hits        33794    10645    -23149     
- Misses       9752    33075    +23323     

☔ 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.

@edan-bainglass edan-bainglass marked this pull request as draft November 8, 2025 07:20
@edan-bainglass
Copy link
Member Author

edan-bainglass commented Nov 8, 2025

@danielhollas I'm trying to make this one work, but it seems to require a lot of typing tweaks. Anyhow, it would be easier to understand for a reviewer if and when #6990 is merged. For now, if possible, can you make sense of why the test-install tests are failing? They work locally for me, so unclear.

Update

I force-pushed the PR onto a single commit. Check out the commit to see changes in this PR.

@edan-bainglass edan-bainglass force-pushed the refactor-models branch 2 times, most recently from d7c200e to 44b70b5 Compare November 8, 2025 09:09
Comment on lines 22 to 25
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
Copy link
Member Author

Choose a reason for hiding this comment

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

@danielhollas you do this in #7036 in age_entities.py. Moving it here for consistency.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I've opened #7096 as a better solution for this. I don't think it makes sense to put these common types in our own module.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, it makes sense if the try...except dance (as you call it) is required, to avoid excessive dancing 😅 I think in #7096 your claim is that the dance itself is not required.

self._backend.authinfos.delete(pk)


class AuthInfoModel(entities.Entity.Model):
Copy link
Member Author

Choose a reason for hiding this comment

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

As mentioned in the PR description, I provided the Model attribute (see below) to allow retaining Entity.Model access. However, this required me to define it as a TypeAlias (which is recommended by PEP). But it is unclear to me if this is correct, or rather if this helps with typing. More on this in entities.py.

Copy link
Collaborator

@danielhollas danielhollas Nov 9, 2025

Choose a reason for hiding this comment

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

Which PEP do you mean?

EDIT: In this specific files, if I remove the TypeAlias annotation it still type-checks. So I don't think it's needed here?
(also it seems very weird for it to be TypeAlias, it's a (generic) class?

Copy link
Member Author

@edan-bainglass edan-bainglass Nov 9, 2025

Choose a reason for hiding this comment

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

Sorry, not PEP, but mypy - see https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases. However, this is not quite correct. The recommendation is that the class variable Model should be explicitly typed as a TypeAlias, as compared to global variables. In any case, I don't care for this solution in general. I don't think TypeAlias is the correct approach, but I'm uncertain about all of this.

Copy link
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 if it is good to inherit from a class property reference. However, I encounter strange behavior from pydantic when I expose the model classes directly. I'll paste the error here shortly.

Copy link
Member Author

@edan-bainglass edan-bainglass Nov 9, 2025

Choose a reason for hiding this comment

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

Right, so after switching from Entity.Model to EntityModel, etc. in the inheritance chain Entity -> Node -> Data -> AbstractCode -> InstalledCode, I get the following mypy error

(aiida-dev) (refactor-models) aiida-core > mypy src/aiida/orm/nodes/data/code/
src/aiida/orm/nodes/data/code/installed.py:38: error: INTERNAL ERROR -- Please try using mypy master on GitHub:
https://mypy.readthedocs.io/en/stable/common_issues.html#using-a-development-mypy-build
Please report a bug at https://github.com/python/mypy/issues
version: 1.18.2
Traceback (most recent call last):
  File "mypy/semanal.py", line 7350, in accept
  File "mypy/nodes.py", line 1440, in accept
  File "mypy/semanal.py", line 1775, in visit_class_def
  File "mypy/semanal.py", line 1991, in analyze_class
  File "mypy/semanal.py", line 2038, in analyze_class_body_common
  File "mypy/semanal.py", line 2123, in apply_class_plugin_hooks
  File "/home/edanb/miniforge3/envs/aiida-dev/lib/python3.10/site-packages/pydantic/mypy.py", line 184, in _pydantic_model_class_maker_callback
    return transformer.transform()
  File "/home/edanb/miniforge3/envs/aiida-dev/lib/python3.10/site-packages/pydantic/mypy.py", line 518, in transform
    fields, class_vars = self.collect_fields_and_class_vars(config, is_root_model)
  File "/home/edanb/miniforge3/envs/aiida-dev/lib/python3.10/site-packages/pydantic/mypy.py", line 682, in collect_fields_and_class_vars
    self._api.fail(
  File "mypy/semanal.py", line 7325, in fail
AttributeError: attribute 'column' of 'Context' undefined
src/aiida/orm/nodes/data/code/installed.py:38: : note: use --pdb to drop into pdb

I have not yet attempted further debugging.

Notice that this is an apparent issue of the pydantic mypy plugin, which you recently introduced in #7064. Indeed, disabling the plugin removes the error.

Copy link
Collaborator

@danielhollas danielhollas Nov 9, 2025

Choose a reason for hiding this comment

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

Oof, we're getting into some dark magic territory. Maybe if you managed to get a minimal reproducer you could open a bug report against pydantic? Internal error seems like something that's not your fault.

(I don't really care about the plugin, I hoped it would be helpful, but we can remove it if it would conflict with this PR)

Btw: this discussion seems relevant
python/typing#1424

Edit: also this?
python/mypy#11538 (comment)

Comment on lines 241 to 244
class Entity(abc.ABC, Generic[BackendEntityType, EntityCollectionType], metaclass=EntityFieldMeta):
"""An AiiDA entity"""

Model: TypeAlias = EntityModel
Copy link
Member Author

Choose a reason for hiding this comment

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

As promised, more on EntityModelType...

Here I initially tried to add an EntityModelType to the Generic list on Entity and wire it throughout Entity, for example, Model: type[EntityModelType] = EntityModel. But this did not yield the desired outcome. Again, it may just be due to a misunderstanding of typing on my part. @danielhollas great if we can discuss this.

@edan-bainglass edan-bainglass force-pushed the refactor-models branch 3 times, most recently from 90d9c1b to 4a87230 Compare November 8, 2025 10:09
except IndexError:
raise ValueError(
'To understand if it is a metal or insulator, ' 'need more bands than n_band=number_electrons'
'To understand if it is a metal or insulator, need more bands than n_band=number_electrons'
Copy link
Member Author

Choose a reason for hiding this comment

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

Unrelated formatting (a few more later)


@classmethod
def from_model(cls, model: Model) -> Self: # type: ignore[override]
def from_model(cls, model: NodeModel) -> Self: # type: ignore[override]
Copy link
Member Author

@edan-bainglass edan-bainglass Nov 8, 2025

Choose a reason for hiding this comment

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

Unclear why mypy complains. NodeModel is a subclass of EntityModel. But I imagine here I'm misunderstanding typing/subtyping, etc. @danielhollas what do you think?

@edan-bainglass edan-bainglass force-pushed the refactor-models branch 4 times, most recently from 043db37 to b0e1a22 Compare November 8, 2025 12:13
@danielhollas
Copy link
Collaborator

'm trying to make this one work, but it seems to require a lot of typing tweaks.

I'll have a look once I am back but no promises as this seem to touch on the generics / variance topics which are still very murky to me.

Anyhow, it would be easier to understand for a reviewer if and when #6990 is merged.

You can push the branch from #6990 to the origin repo, and then make it a target for this PR.

For now, if possible, can you make sense of why the test-install tests are failing? They work locally for me, so unclear.

The job fails at the setup step, not during the tests. Have you looked into it? The error seems to suggest something is broken here, but I don't have time to dig deeper (still on holidays 😅)

@edan-bainglass
Copy link
Member Author

For now, if possible, can you make sense of why the test-install tests are failing? They work locally for me, so unclear.

The job fails at the setup step, not during the tests. Have you looked into it? The error seems to suggest something is broken here, but I don't have time to dig deeper (still on holidays 😅)

It seems to fail at verdi code create core.code.installed --non-interactive --config "${CONFIG}/doubler.yaml", which runs fine locally. No worries. Enjoy your vacay 😎

@edan-bainglass edan-bainglass force-pushed the refactor-models branch 2 times, most recently from cd0f29f to 42230ac Compare November 12, 2025 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants