From 23981a5b7c11992c865e8fa5912b3862386ebba3 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 20 Oct 2021 07:37:36 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=8C=20IMPROVE:=20Add=20typing=20to=20a?= =?UTF-8?q?ll=20base=20Entity=20classes=20(#5185)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `AuthInfo`, `Comment`, `Computer`, `Group`, `Log`, `Node`, and `User`. Also remove `uuid` from `BackendNode`, since it is not available for `AuthInfo` and `User`. --- .pre-commit-config.yaml | 6 + aiida/orm/authinfos.py | 57 ++++------ aiida/orm/comments.py | 49 ++++---- aiida/orm/computers.py | 156 ++++++++++++-------------- aiida/orm/entities.py | 28 ++--- aiida/orm/groups.py | 98 ++++++++-------- aiida/orm/implementation/comments.py | 2 +- aiida/orm/implementation/computers.py | 12 +- aiida/orm/implementation/entities.py | 25 ++--- aiida/orm/implementation/nodes.py | 9 +- aiida/orm/implementation/users.py | 9 -- aiida/orm/logs.py | 64 +++++------ aiida/orm/nodes/node.py | 8 +- aiida/orm/querybuilder.py | 4 +- aiida/orm/users.py | 52 +++++---- aiida/orm/utils/loaders.py | 14 ++- aiida/plugins/factories.py | 5 +- docs/source/nitpick-exceptions | 4 + 18 files changed, 298 insertions(+), 304 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9ccf4c40fc..f67e5016b7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -82,6 +82,12 @@ repos: aiida/orm/implementation/querybuilder.py| aiida/orm/implementation/sqlalchemy/querybuilder/.*py| aiida/orm/entities.py| + aiida/orm/authinfos.py| + aiida/orm/comments.py| + aiida/orm/computers.py| + aiida/orm/groups.py| + aiida/orm/logs.py| + aiida/orm/users.py| aiida/orm/nodes/data/jsonable.py| aiida/orm/nodes/node.py| aiida/orm/nodes/process/.*py| diff --git a/aiida/orm/authinfos.py b/aiida/orm/authinfos.py index 49f03416bb..7a398b20bd 100644 --- a/aiida/orm/authinfos.py +++ b/aiida/orm/authinfos.py @@ -8,7 +8,7 @@ # For further information please visit http://www.aiida.net # ########################################################################### """Module for the `AuthInfo` ORM class.""" -from typing import Type +from typing import TYPE_CHECKING, Any, Dict, Optional, Type from aiida.common import exceptions from aiida.common.lang import classproperty @@ -17,6 +17,11 @@ from . import entities, users +if TYPE_CHECKING: + from aiida.orm import Computer, User + from aiida.orm.implementation import Backend, BackendAuthInfo + from aiida.transports import Transport + __all__ = ('AuthInfo',) @@ -35,7 +40,7 @@ def delete(self, pk: int) -> None: self._backend.authinfos.delete(pk) -class AuthInfo(entities.Entity): +class AuthInfo(entities.Entity['BackendAuthInfo']): """ORM class that models the authorization information that allows a `User` to connect to a `Computer`.""" Collection = AuthInfoCollection @@ -46,36 +51,33 @@ def objects(cls) -> AuthInfoCollection: # pylint: disable=no-self-argument PROPERTY_WORKDIR = 'workdir' - def __init__(self, computer, user, backend=None) -> None: + def __init__(self, computer: 'Computer', user: 'User', backend: Optional['Backend'] = None) -> None: """Create an `AuthInfo` instance for the given computer and user. :param computer: a `Computer` instance - :type computer: :class:`aiida.orm.Computer` - :param user: a `User` instance - :type user: :class:`aiida.orm.User` + :param backend: the backend to use for the instance, or use the default backend if None """ backend = backend or get_manager().get_backend() model = backend.authinfos.create(computer=computer.backend_entity, user=user.backend_entity) super().__init__(model) - def __str__(self): + def __str__(self) -> str: if self.enabled: return f'AuthInfo for {self.user.email} on {self.computer.label}' return f'AuthInfo for {self.user.email} on {self.computer.label} [DISABLED]' @property - def enabled(self): + def enabled(self) -> bool: """Return whether this instance is enabled. :return: True if enabled, False otherwise - :rtype: bool """ return self._backend_entity.enabled @enabled.setter - def enabled(self, enabled): + def enabled(self, enabled: bool) -> None: """Set the enabled state :param enabled: boolean, True to enable the instance, False to disable it @@ -83,71 +85,58 @@ def enabled(self, enabled): self._backend_entity.enabled = enabled @property - def computer(self): - """Return the computer associated with this instance. - - :rtype: :class:`aiida.orm.computers.Computer` - """ + def computer(self) -> 'Computer': + """Return the computer associated with this instance.""" from . import computers # pylint: disable=cyclic-import return computers.Computer.from_backend_entity(self._backend_entity.computer) @property - def user(self): - """Return the user associated with this instance. - - :rtype: :class:`aiida.orm.users.User` - """ + def user(self) -> 'User': + """Return the user associated with this instance.""" return users.User.from_backend_entity(self._backend_entity.user) - def get_auth_params(self): + def get_auth_params(self) -> Dict[str, Any]: """Return the dictionary of authentication parameters :return: a dictionary with authentication parameters - :rtype: dict """ return self._backend_entity.get_auth_params() - def set_auth_params(self, auth_params): + def set_auth_params(self, auth_params: Dict[str, Any]) -> None: """Set the dictionary of authentication parameters :param auth_params: a dictionary with authentication parameters """ self._backend_entity.set_auth_params(auth_params) - def get_metadata(self): + def get_metadata(self) -> Dict[str, Any]: """Return the dictionary of metadata :return: a dictionary with metadata - :rtype: dict """ return self._backend_entity.get_metadata() - def set_metadata(self, metadata): + def set_metadata(self, metadata: Dict[str, Any]) -> None: """Set the dictionary of metadata :param metadata: a dictionary with metadata - :type metadata: dict """ self._backend_entity.set_metadata(metadata) - def get_workdir(self): + def get_workdir(self) -> str: """Return the working directory. If no explicit work directory is set for this instance, the working directory of the computer will be returned. :return: the working directory - :rtype: str """ try: return self.get_metadata()[self.PROPERTY_WORKDIR] except KeyError: return self.computer.get_workdir() - def get_transport(self): - """Return a fully configured transport that can be used to connect to the computer set for this instance. - - :rtype: :class:`aiida.transports.Transport` - """ + def get_transport(self) -> 'Transport': + """Return a fully configured transport that can be used to connect to the computer set for this instance.""" computer = self.computer transport_type = computer.transport_type diff --git a/aiida/orm/comments.py b/aiida/orm/comments.py index 08a04ace7a..760c3e2a69 100644 --- a/aiida/orm/comments.py +++ b/aiida/orm/comments.py @@ -8,13 +8,18 @@ # For further information please visit http://www.aiida.net # ########################################################################### """Comment objects and functions""" -from typing import List, Type +from datetime import datetime +from typing import TYPE_CHECKING, List, Optional, Type from aiida.common.lang import classproperty from aiida.manage.manager import get_manager from . import entities, users +if TYPE_CHECKING: + from aiida.orm import Node, User + from aiida.orm.implementation import Backend, BackendComment + __all__ = ('Comment',) @@ -59,7 +64,7 @@ def delete_many(self, filters) -> List[int]: return self._backend.comments.delete_many(filters) -class Comment(entities.Entity): +class Comment(entities.Entity['BackendComment']): """Base class to map a DbComment that represents a comment attached to a certain Node.""" Collection = CommentCollection @@ -68,55 +73,59 @@ class Comment(entities.Entity): def objects(cls) -> CommentCollection: # pylint: disable=no-self-argument return CommentCollection.get_cached(cls, get_manager().get_backend()) - def __init__(self, node, user, content=None, backend=None): - """ - Create a Comment for a given node and user + def __init__(self, node: 'Node', user: 'User', content: Optional[str] = None, backend: Optional['Backend'] = None): + """Create a Comment for a given node and user :param node: a Node instance - :type node: :class:`aiida.orm.Node` - :param user: a User instance - :type user: :class:`aiida.orm.User` - :param content: the comment content - :type content: str + :param backend: the backend to use for the instance, or use the default backend if None :return: a Comment object associated to the given node and user - :rtype: :class:`aiida.orm.Comment` """ backend = backend or get_manager().get_backend() model = backend.comments.create(node=node.backend_entity, user=user.backend_entity, content=content) super().__init__(model) - def __str__(self): + def __str__(self) -> str: arguments = [self.uuid, self.node.pk, self.user.email, self.content] return 'Comment<{}> for node<{}> and user<{}>: {}'.format(*arguments) @property - def ctime(self): + def uuid(self) -> str: + """Return the UUID for this comment. + + This identifier is unique across all entities types and backend instances. + + :return: the entity uuid + """ + return self._backend_entity.uuid + + @property + def ctime(self) -> datetime: return self._backend_entity.ctime @property - def mtime(self): + def mtime(self) -> datetime: return self._backend_entity.mtime - def set_mtime(self, value): + def set_mtime(self, value: datetime) -> None: return self._backend_entity.set_mtime(value) @property - def node(self): + def node(self) -> 'Node': return self._backend_entity.node @property - def user(self): + def user(self) -> 'User': return users.User.from_backend_entity(self._backend_entity.user) - def set_user(self, value): + def set_user(self, value: 'User') -> None: self._backend_entity.user = value.backend_entity @property - def content(self): + def content(self) -> str: return self._backend_entity.content - def set_content(self, value): + def set_content(self, value: str) -> None: return self._backend_entity.set_content(value) diff --git a/aiida/orm/computers.py b/aiida/orm/computers.py index f95f362602..4c77d96f41 100644 --- a/aiida/orm/computers.py +++ b/aiida/orm/computers.py @@ -10,16 +10,21 @@ """Module for Computer entities""" import logging import os -from typing import List, Optional, Tuple, Type +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union from aiida.common import exceptions from aiida.common.lang import classproperty from aiida.manage.manager import get_manager -from aiida.orm.implementation import Backend from aiida.plugins import SchedulerFactory, TransportFactory from . import entities, users +if TYPE_CHECKING: + from aiida.orm import AuthInfo, User + from aiida.orm.implementation import Backend, BackendComputer + from aiida.schedulers import Scheduler + from aiida.transports import Transport + __all__ = ('Computer',) @@ -30,7 +35,7 @@ class ComputerCollection(entities.Collection['Computer']): def _entity_base_cls() -> Type['Computer']: return Computer - def get_or_create(self, label: Optional[str] = None, **kwargs) -> Tuple['Computer', bool]: + def get_or_create(self, label: Optional[str] = None, **kwargs) -> Tuple[bool, 'Computer']: """ Try to retrieve a Computer from the DB with the given arguments; create (and store) a new Computer if such a Computer was not present yet. @@ -57,7 +62,7 @@ def delete(self, pk: int) -> None: return self._backend.computers.delete(pk) -class Computer(entities.Entity): +class Computer(entities.Entity['BackendComputer']): """ Computer entity. """ @@ -84,8 +89,8 @@ def __init__( # pylint: disable=too-many-arguments transport_type: str = '', scheduler_type: str = '', workdir: str = None, - backend: Backend = None, - ) -> 'Computer': + backend: Optional['Backend'] = None, + ) -> None: """Construct a new computer.""" backend = backend or get_manager().get_backend() model = backend.computers.create( @@ -106,11 +111,21 @@ def __str__(self): return f'{self.label} ({self.hostname}), pk: {self.pk}' @property - def logger(self): + def uuid(self) -> str: + """Return the UUID for this computer. + + This identifier is unique across all entities types and backend instances. + + :return: the entity uuid + """ + return self._backend_entity.uuid + + @property + def logger(self) -> logging.Logger: return self._logger @classmethod - def _label_validator(cls, label): + def _label_validator(cls, label: str) -> None: """ Validates the label. """ @@ -118,7 +133,7 @@ def _label_validator(cls, label): raise exceptions.ValidationError('No label specified') @classmethod - def _hostname_validator(cls, hostname): + def _hostname_validator(cls, hostname: str) -> None: """ Validates the hostname. """ @@ -126,14 +141,14 @@ def _hostname_validator(cls, hostname): raise exceptions.ValidationError('No hostname specified') @classmethod - def _description_validator(cls, description): + def _description_validator(cls, description: str) -> None: """ Validates the description. """ # The description is always valid @classmethod - def _transport_type_validator(cls, transport_type): + def _transport_type_validator(cls, transport_type: str) -> None: """ Validates the transport string. """ @@ -142,7 +157,7 @@ def _transport_type_validator(cls, transport_type): raise exceptions.ValidationError('The specified transport is not a valid one') @classmethod - def _scheduler_type_validator(cls, scheduler_type): + def _scheduler_type_validator(cls, scheduler_type: str) -> None: """ Validates the transport string. """ @@ -151,21 +166,21 @@ def _scheduler_type_validator(cls, scheduler_type): raise exceptions.ValidationError(f'The specified scheduler `{scheduler_type}` is not a valid one') @classmethod - def _prepend_text_validator(cls, prepend_text): + def _prepend_text_validator(cls, prepend_text: str) -> None: """ Validates the prepend text string. """ # no validation done @classmethod - def _append_text_validator(cls, append_text): + def _append_text_validator(cls, append_text: str) -> None: """ Validates the append text string. """ # no validation done @classmethod - def _workdir_validator(cls, workdir): + def _workdir_validator(cls, workdir: str) -> None: """ Validates the transport string. """ @@ -182,7 +197,7 @@ def _workdir_validator(cls, workdir): if not os.path.isabs(convertedwd): raise exceptions.ValidationError('The workdir must be an absolute path') - def _mpirun_command_validator(self, mpirun_cmd): + def _mpirun_command_validator(self, mpirun_cmd: Union[List[str], Tuple[str, ...]]) -> None: """ Validates the mpirun_command variable. MUST be called after properly checking for a valid scheduler. @@ -206,7 +221,7 @@ def _mpirun_command_validator(self, mpirun_cmd): except ValueError as exc: raise exceptions.ValidationError(f"Error in the string: '{exc}'") - def validate(self): + def validate(self) -> None: """ Check if the attributes and files retrieved from the DB are valid. Raise a ValidationError if something is wrong. @@ -236,7 +251,7 @@ def validate(self): self._mpirun_command_validator(mpirun_cmd) @classmethod - def _default_mpiprocs_per_machine_validator(cls, def_cpus_per_machine): + def _default_mpiprocs_per_machine_validator(cls, def_cpus_per_machine: Optional[int]) -> None: """ Validates the default number of CPUs per machine (node) """ @@ -249,13 +264,13 @@ def _default_mpiprocs_per_machine_validator(cls, def_cpus_per_machine): 'do not want to provide a default value.' ) - def copy(self): + def copy(self) -> 'Computer': """ Return a copy of the current object to work with, not stored yet. """ return Computer.from_backend_entity(self._backend_entity.copy()) - def store(self): + def store(self) -> 'Computer': """ Store the computer in the DB. @@ -274,7 +289,7 @@ def label(self) -> str: return self._backend_entity.label @label.setter - def label(self, value: str): + def label(self, value: str) -> None: """Set the computer label. :param value: the label to set. @@ -290,7 +305,7 @@ def description(self) -> str: return self._backend_entity.description @description.setter - def description(self, value: str): + def description(self, value: str) -> None: """Set the computer description. :param value: the description to set. @@ -306,7 +321,7 @@ def hostname(self) -> str: return self._backend_entity.hostname @hostname.setter - def hostname(self, value: str): + def hostname(self, value: str) -> None: """Set the computer hostname. :param value: the hostname to set. @@ -322,7 +337,7 @@ def scheduler_type(self) -> str: return self._backend_entity.get_scheduler_type() @scheduler_type.setter - def scheduler_type(self, value: str): + def scheduler_type(self, value: str) -> None: """Set the computer scheduler type. :param value: the scheduler type to set. @@ -338,7 +353,7 @@ def transport_type(self) -> str: return self._backend_entity.get_transport_type() @transport_type.setter - def transport_type(self, value: str): + def transport_type(self, value: str) -> None: """Set the computer transport type. :param value: the transport_type to set. @@ -346,7 +361,7 @@ def transport_type(self, value: str): self._backend_entity.set_transport_type(value) @property - def metadata(self) -> str: + def metadata(self) -> Dict[str, Any]: """Return the computer metadata. :return: the metadata. @@ -354,22 +369,19 @@ def metadata(self) -> str: return self._backend_entity.get_metadata() @metadata.setter - def metadata(self, value: str): + def metadata(self, value: Dict[str, Any]) -> None: """Set the computer metadata. :param value: the metadata to set. """ self._backend_entity.set_metadata(value) - def delete_property(self, name, raise_exception=True): + def delete_property(self, name: str, raise_exception: bool = True) -> None: """ Delete a property from this computer :param name: the name of the property - :type name: str - :param raise_exception: if True raise if the property does not exist, otherwise return None - :type raise_exception: bool """ olddata = self.metadata try: @@ -379,9 +391,8 @@ def delete_property(self, name, raise_exception=True): if raise_exception: raise AttributeError(f"'{name}' property not found") - def set_property(self, name, value): - """ - Set a property on this computer + def set_property(self, name: str, value: Any) -> None: + """Set a property on this computer :param name: the property name :param value: the new value @@ -390,13 +401,10 @@ def set_property(self, name, value): metadata[name] = value self.metadata = metadata - def get_property(self, name, *args): - """ - Get a property of this computer + def get_property(self, name: str, *args: Any) -> Any: + """Get a property of this computer :param name: the property name - :type name: str - :param args: additional arguments :return: the property value @@ -411,19 +419,19 @@ def get_property(self, name, *args): raise AttributeError(f"'{name}' property not found") return args[0] - def get_prepend_text(self): + def get_prepend_text(self) -> str: return self.get_property('prepend_text', '') - def set_prepend_text(self, val): + def set_prepend_text(self, val: str) -> None: self.set_property('prepend_text', str(val)) - def get_append_text(self): + def get_append_text(self) -> str: return self.get_property('append_text', '') - def set_append_text(self, val): + def set_append_text(self, val: str) -> None: self.set_property('append_text', str(val)) - def get_mpirun_command(self): + def get_mpirun_command(self) -> List[str]: """ Return the mpirun command. Must be a list of strings, that will be then joined with spaces when submitting. @@ -432,7 +440,7 @@ def get_mpirun_command(self): """ return self.get_property('mpirun_command', ['mpirun', '-np', '{tot_num_mpiprocs}']) - def set_mpirun_command(self, val): + def set_mpirun_command(self, val: Union[List[str], Tuple[str, ...]]) -> None: """ Set the mpirun command. It must be a list of strings (you can use string.split() if you have a single, space-separated string). @@ -441,14 +449,14 @@ def set_mpirun_command(self, val): raise TypeError('the mpirun_command must be a list of strings') self.set_property('mpirun_command', val) - def get_default_mpiprocs_per_machine(self): + def get_default_mpiprocs_per_machine(self) -> Optional[int]: """ Return the default number of CPUs per machine (node) for this computer, or None if it was not set. """ return self.get_property('default_mpiprocs_per_machine', None) - def set_default_mpiprocs_per_machine(self, def_cpus_per_machine): + def set_default_mpiprocs_per_machine(self, def_cpus_per_machine: Optional[int]) -> None: """ Set the default number of CPUs per machine (node) for this computer. Accepts None if you do not want to set this value. @@ -460,43 +468,40 @@ def set_default_mpiprocs_per_machine(self, def_cpus_per_machine): raise TypeError('def_cpus_per_machine must be an integer (or None)') self.set_property('default_mpiprocs_per_machine', def_cpus_per_machine) - def get_minimum_job_poll_interval(self): + def get_minimum_job_poll_interval(self) -> float: """ Get the minimum interval between subsequent requests to update the list of jobs currently running on this computer. :return: The minimum interval (in seconds) - :rtype: float """ return self.get_property( self.PROPERTY_MINIMUM_SCHEDULER_POLL_INTERVAL, self.PROPERTY_MINIMUM_SCHEDULER_POLL_INTERVAL__DEFAULT ) - def set_minimum_job_poll_interval(self, interval): + def set_minimum_job_poll_interval(self, interval: float) -> None: """ Set the minimum interval between subsequent requests to update the list of jobs currently running on this computer. :param interval: The minimum interval in seconds - :type interval: float """ self.set_property(self.PROPERTY_MINIMUM_SCHEDULER_POLL_INTERVAL, interval) - def get_workdir(self): + def get_workdir(self) -> str: """ Get the working directory for this computer :return: The currently configured working directory - :rtype: str """ return self.get_property(self.PROPERTY_WORKDIR, '/scratch/{username}/aiida_run/') - def set_workdir(self, val): + def set_workdir(self, val: str) -> None: self.set_property(self.PROPERTY_WORKDIR, val) - def get_shebang(self): + def get_shebang(self) -> str: return self.get_property(self.PROPERTY_SHEBANG, '#!/bin/bash') - def set_shebang(self, val): + def set_shebang(self, val: str) -> None: """ :param str val: A valid shebang line """ @@ -508,7 +513,7 @@ def set_shebang(self, val): metadata['shebang'] = val self.metadata = metadata - def get_authinfo(self, user): + def get_authinfo(self, user: 'User') -> 'AuthInfo': """ Return the aiida.orm.authinfo.AuthInfo instance for the given user on this computer, if the computer @@ -531,13 +536,12 @@ def get_authinfo(self, user): return authinfo - def is_user_configured(self, user): + def is_user_configured(self, user: 'User') -> bool: """ Is the user configured on this computer? :param user: the user to check :return: True if configured, False otherwise - :rtype: bool """ try: self.get_authinfo(user) @@ -545,13 +549,12 @@ def is_user_configured(self, user): except exceptions.NotExistent: return False - def is_user_enabled(self, user): + def is_user_enabled(self, user: 'User') -> bool: """ Is the given user enabled to run on this computer? :param user: the user to check :return: True if enabled, False otherwise - :rtype: bool """ try: authinfo = self.get_authinfo(user) @@ -560,7 +563,7 @@ def is_user_enabled(self, user): # Return False if the user is not configured (in a sense, it is disabled for that user) return False - def get_transport(self, user=None): + def get_transport(self, user: Optional['User'] = None) -> 'Transport': """ Return a Transport class, configured with all correct parameters. The Transport is closed (meaning that if you want to run any operation with @@ -585,12 +588,8 @@ def get_transport(self, user=None): authinfo = authinfos.AuthInfo.objects(self.backend).get(dbcomputer=self, aiidauser=user) return authinfo.get_transport() - def get_transport_class(self): - """ - Get the transport class for this computer. Can be used to instantiate a transport instance. - - :return: the transport class - """ + def get_transport_class(self) -> Type['Transport']: + """Get the transport class for this computer. Can be used to instantiate a transport instance.""" try: return TransportFactory(self.transport_type) except exceptions.EntryPointError as exception: @@ -598,13 +597,8 @@ def get_transport_class(self): f'No transport found for {self.label} [type {self.transport_type}], message: {exception}' ) - def get_scheduler(self): - """ - Get a scheduler instance for this computer - - :return: the scheduler instance - :rtype: :class:`aiida.schedulers.Scheduler` - """ + def get_scheduler(self) -> 'Scheduler': + """Get a scheduler instance for this computer""" try: scheduler_class = SchedulerFactory(self.scheduler_type) # I call the init without any parameter @@ -614,14 +608,12 @@ def get_scheduler(self): f'No scheduler found for {self.label} [type {self.scheduler_type}], message: {exception}' ) - def configure(self, user=None, **kwargs): - """ - Configure a computer for a user with valid auth params passed via kwargs + def configure(self, user: Optional['User'] = None, **kwargs: Any) -> 'AuthInfo': + """Configure a computer for a user with valid auth params passed via kwargs :param user: the user to configure the computer for :kwargs: the configuration keywords with corresponding values :return: the authinfo object for the configured user - :rtype: :class:`aiida.orm.AuthInfo` """ from . import authinfos @@ -647,12 +639,10 @@ def configure(self, user=None, **kwargs): return authinfo - def get_configuration(self, user=None): - """ - Get the configuration of computer for the given user as a dictionary + def get_configuration(self, user: Optional['User'] = None) -> Dict[str, Any]: + """Get the configuration of computer for the given user as a dictionary :param user: the user to to get the configuration for. Uses default user if `None` - :type user: :class:`aiida.orm.User` """ backend = self.backend diff --git a/aiida/orm/entities.py b/aiida/orm/entities.py index 77c068e403..5f07cd0b1d 100644 --- a/aiida/orm/entities.py +++ b/aiida/orm/entities.py @@ -33,6 +33,7 @@ CollectionType = TypeVar('CollectionType', bound='Collection') EntityType = TypeVar('EntityType', bound='Entity') +BackendEntityType = TypeVar('BackendEntityType', bound='BackendEntity') _NO_DEFAULT: Any = tuple() @@ -99,7 +100,7 @@ def backend(self) -> 'Backend': def query( self, - filters: Optional[Dict[str, 'FilterType']] = None, + filters: Optional['FilterType'] = None, order_by: Optional['OrderByType'] = None, limit: Optional[int] = None, offset: Optional[int] = None @@ -133,7 +134,7 @@ def get(self, **filters: Any) -> EntityType: def find( self, - filters: Optional[Dict[str, 'FilterType']] = None, + filters: Optional['FilterType'] = None, order_by: Optional['OrderByType'] = None, limit: Optional[int] = None ) -> List[EntityType]: @@ -155,7 +156,7 @@ def all(self) -> List[EntityType]: """ return cast(List[EntityType], self.query().all(flat=True)) # pylint: disable=no-member - def count(self, filters: Optional[Dict[str, 'FilterType']] = None) -> int: + def count(self, filters: Optional['FilterType'] = None) -> int: """Count entities in this collection according to criteria. :param filters: the keyword value pair filters to match @@ -165,7 +166,7 @@ def count(self, filters: Optional[Dict[str, 'FilterType']] = None) -> int: return self.query(filters=filters).count() -class Entity(abc.ABC): +class Entity(abc.ABC, Generic[BackendEntityType]): """An AiiDA entity""" @classproperty @@ -181,7 +182,7 @@ def get(cls, **kwargs): return cls.objects.get(**kwargs) # pylint: disable=no-member @classmethod - def from_backend_entity(cls: Type[EntityType], backend_entity: 'BackendEntity') -> EntityType: + def from_backend_entity(cls: Type[EntityType], backend_entity: BackendEntityType) -> EntityType: """ Construct an entity from a backend entity instance @@ -197,14 +198,14 @@ def from_backend_entity(cls: Type[EntityType], backend_entity: 'BackendEntity') call_with_super_check(entity.initialize) return entity - def __init__(self, backend_entity: 'BackendEntity') -> None: + def __init__(self, backend_entity: BackendEntityType) -> None: """ :param backend_entity: the backend model supporting this entity """ self._backend_entity = backend_entity call_with_super_check(self.initialize) - def init_from_backend(self, backend_entity: 'BackendEntity') -> None: + def init_from_backend(self, backend_entity: BackendEntityType) -> None: """ :param backend_entity: the backend model supporting this entity """ @@ -237,17 +238,6 @@ def pk(self) -> int: """ return self.id - @property - def uuid(self): - """Return the UUID for this entity. - - This identifier is unique across all entities types and backend instances. - - :return: the entity uuid - :rtype: :class:`uuid.UUID` - """ - return self._backend_entity.uuid - def store(self: EntityType) -> EntityType: """Store the entity.""" self._backend_entity.store() @@ -264,7 +254,7 @@ def backend(self) -> 'Backend': return self._backend_entity.backend @property - def backend_entity(self) -> 'BackendEntity': + def backend_entity(self) -> BackendEntityType: """Get the implementing class for this object""" return self._backend_entity diff --git a/aiida/orm/groups.py b/aiida/orm/groups.py index cddf71db51..731d820553 100644 --- a/aiida/orm/groups.py +++ b/aiida/orm/groups.py @@ -9,7 +9,7 @@ ########################################################################### """AiiDA Group entites""" from abc import ABCMeta -from typing import ClassVar, Optional, Tuple, Type +from typing import TYPE_CHECKING, ClassVar, Optional, Sequence, Tuple, Type, TypeVar, Union, cast import warnings from aiida.common import exceptions @@ -18,10 +18,16 @@ from . import convert, entities, users +if TYPE_CHECKING: + from aiida.orm import Node, User + from aiida.orm.implementation import Backend, BackendGroup + __all__ = ('Group', 'AutoGroup', 'ImportGroup', 'UpfFamily') +SelfType = TypeVar('SelfType', bound='Group') + -def load_group_class(type_string): +def load_group_class(type_string: str) -> Type['Group']: """Load the sub class of `Group` that corresponds to the given `type_string`. .. note:: will fall back on `aiida.orm.groups.Group` if `type_string` cannot be resolved to loadable entry point. @@ -54,11 +60,12 @@ def __new__(cls, name, bases, namespace, **kwargs): entry_point_group, entry_point = get_entry_point_from_class(mod, name) if entry_point_group is None or entry_point_group != 'aiida.groups': - newcls._type_string = None + newcls._type_string = None # type: ignore[attr-defined] message = f'no registered entry point for `{mod}:{name}` so its instances will not be storable.' warnings.warn(message) # pylint: disable=no-member else: - newcls._type_string = entry_point.name # pylint: disable=protected-access + assert entry_point is not None + newcls._type_string = cast(str, entry_point.name) # type: ignore[attr-defined] # pylint: disable=protected-access return newcls @@ -102,11 +109,11 @@ def delete(self, pk: int) -> None: self._backend.groups.delete(pk) -class Group(entities.Entity, entities.EntityExtrasMixin, metaclass=GroupMeta): +class Group(entities.Entity['BackendGroup'], entities.EntityExtrasMixin, metaclass=GroupMeta): """An AiiDA ORM implementation of group of nodes.""" # added by metaclass - _type_string = ClassVar[Optional[str]] + _type_string: ClassVar[Optional[str]] Collection = GroupCollection @@ -114,24 +121,24 @@ class Group(entities.Entity, entities.EntityExtrasMixin, metaclass=GroupMeta): def objects(cls) -> GroupCollection: # pylint: disable=no-self-argument return GroupCollection.get_cached(cls, get_manager().get_backend()) - def __init__(self, label=None, user=None, description='', type_string=None, backend=None): + def __init__( + self, + label: Optional[str] = None, + user: Optional['User'] = None, + description: str = '', + type_string: Optional[str] = None, + backend: Optional['Backend'] = None + ): """ Create a new group. Either pass a dbgroup parameter, to reload a group from the DB (and then, no further parameters are allowed), or pass the parameters for the Group creation. :param label: The group label, required on creation - :type label: str - :param description: The group description (by default, an empty string) - :type description: str - :param user: The owner of the group (by default, the automatic user) - :type user: :class:`aiida.orm.User` - :param type_string: a string identifying the type of group (by default, an empty string, indicating an user-defined group. - :type type_string: str """ if not label: raise ValueError('Group label must be provided') @@ -146,16 +153,16 @@ def __init__(self, label=None, user=None, description='', type_string=None, back ) super().__init__(model) - def __repr__(self): + def __repr__(self) -> str: return ( f'<{self.__class__.__name__}: {self.label!r} ' f'[{"type " + self.type_string if self.type_string else "user-defined"}], of user {self.user.email}>' ) - def __str__(self): + def __str__(self) -> str: return f'{self.__class__.__name__}<{self.label}>' - def store(self): + def store(self: SelfType) -> SelfType: """Verify that the group is allowed to be stored, which is the case along as `type_string` is set.""" if self._type_string is None: raise exceptions.StoringNotAllowed('`type_string` is `None` so the group cannot be stored.') @@ -163,14 +170,24 @@ def store(self): return super().store() @property - def label(self): + def uuid(self) -> str: + """Return the UUID for this group. + + This identifier is unique across all entities types and backend instances. + + :return: the entity uuid + """ + return self._backend_entity.uuid + + @property + def label(self) -> str: """ :return: the label of the group as a string """ return self._backend_entity.label @label.setter - def label(self, label): + def label(self, label: str) -> None: """ Attempt to change the label of the group instance. If the group is already stored and the another group of the same type already exists with the desired label, a @@ -184,79 +201,63 @@ def label(self, label): self._backend_entity.label = label @property - def description(self): + def description(self) -> str: """ :return: the description of the group as a string - :rtype: str """ return self._backend_entity.description @description.setter - def description(self, description): + def description(self, description: str) -> None: """ :param description: the description of the group as a string - :type description: str - """ self._backend_entity.description = description @property - def type_string(self): + def type_string(self) -> str: """ :return: the string defining the type of the group """ return self._backend_entity.type_string @property - def user(self): + def user(self) -> 'User': """ :return: the user associated with this group """ return users.User.from_backend_entity(self._backend_entity.user) @user.setter - def user(self, user): + def user(self, user: 'User') -> None: """Set the user. :param user: the user - :type user: :class:`aiida.orm.User` """ type_check(user, users.User) self._backend_entity.user = user.backend_entity - @property - def uuid(self): - """ - :return: a string with the uuid - :rtype: str - """ - return self._backend_entity.uuid - - def count(self): + def count(self) -> int: """Return the number of entities in this group. :return: integer number of entities contained within the group - :rtype: int """ return self._backend_entity.count() @property - def nodes(self): + def nodes(self) -> convert.ConvertIterator: """ Return a generator/iterator that iterates over all nodes and returns the respective AiiDA subclasses of Node, and also allows to ask for the number of nodes in the group using len(). - - :rtype: :class:`aiida.orm.convert.ConvertIterator` """ return convert.ConvertIterator(self._backend_entity.nodes) @property - def is_empty(self): + def is_empty(self) -> bool: """Return whether the group is empty, i.e. it does not contain any nodes. :return: True if it contains no nodes, False otherwise - :rtype: bool """ try: self.nodes[0] @@ -265,17 +266,16 @@ def is_empty(self): else: return False - def clear(self): + def clear(self) -> None: """Remove all the nodes from this group.""" return self._backend_entity.clear() - def add_nodes(self, nodes): + def add_nodes(self, nodes: Union['Node', Sequence['Node']]) -> None: """Add a node or a set of nodes to the group. :note: all the nodes *and* the group itself have to be stored. :param nodes: a single `Node` or a list of `Nodes` - :type nodes: :class:`aiida.orm.Node` or list """ from .nodes import Node @@ -291,13 +291,12 @@ def add_nodes(self, nodes): self._backend_entity.add_nodes([node.backend_entity for node in nodes]) - def remove_nodes(self, nodes): + def remove_nodes(self, nodes: Union['Node', Sequence['Node']]) -> None: """Remove a node or a set of nodes to the group. :note: all the nodes *and* the group itself have to be stored. :param nodes: a single `Node` or a list of `Nodes` - :type nodes: :class:`aiida.orm.Node` or list """ from .nodes import Node @@ -313,10 +312,9 @@ def remove_nodes(self, nodes): self._backend_entity.remove_nodes([node.backend_entity for node in nodes]) - def is_user_defined(self): + def is_user_defined(self) -> bool: """ :return: True if the group is user defined, False otherwise - :rtype: bool """ return not self.type_string diff --git a/aiida/orm/implementation/comments.py b/aiida/orm/implementation/comments.py index 50ec1273c2..0c2793b171 100644 --- a/aiida/orm/implementation/comments.py +++ b/aiida/orm/implementation/comments.py @@ -20,7 +20,7 @@ class BackendComment(BackendEntity): """Base class for a node comment.""" @property - def uuid(self): + def uuid(self) -> str: return str(self._dbmodel.uuid) @property diff --git a/aiida/orm/implementation/computers.py b/aiida/orm/implementation/computers.py index 88b96a616e..2884ff87c2 100644 --- a/aiida/orm/implementation/computers.py +++ b/aiida/orm/implementation/computers.py @@ -37,14 +37,18 @@ class BackendComputer(BackendEntity): @property @abc.abstractmethod - def is_stored(self): + def is_stored(self) -> bool: """ Is the computer stored? :return: True if stored, False otherwise - :rtype: bool """ + @property + @abc.abstractmethod + def uuid(self) -> str: + pass + @property @abc.abstractmethod def label(self): @@ -120,6 +124,10 @@ def get_transport_type(self): def set_transport_type(self, transport_type): pass + @abc.abstractmethod + def copy(self) -> 'BackendComputer': + """Create an unstored clone of an already stored `Computer`.""" + class BackendComputerCollection(BackendCollection[BackendComputer]): """The collection of Computer entries.""" diff --git a/aiida/orm/implementation/entities.py b/aiida/orm/implementation/entities.py index 6b09d2587d..a20ef3e808 100644 --- a/aiida/orm/implementation/entities.py +++ b/aiida/orm/implementation/entities.py @@ -13,6 +13,9 @@ from aiida.orm.implementation.utils import clean_value, validate_attribute_extra_key +if typing.TYPE_CHECKING: + from aiida.orm.implementation import Backend + __all__ = ( 'BackendEntity', 'BackendCollection', 'EntityType', 'BackendEntityAttributesMixin', 'BackendEntityExtrasMixin' ) @@ -23,12 +26,12 @@ class BackendEntity(abc.ABC): """An first-class entity in the backend""" - def __init__(self, backend): + def __init__(self, backend: 'Backend'): self._backend = backend self._dbmodel = None @property - def backend(self): + def backend(self) -> 'Backend': """Return the backend this entity belongs to :return: the backend instance @@ -41,7 +44,7 @@ def dbmodel(self): @property @abc.abstractmethod - def id(self): # pylint: disable=invalid-name + def id(self) -> int: # pylint: disable=invalid-name """Return the id for this entity. This is unique only amongst entities of this type for a particular backend. @@ -50,7 +53,7 @@ def id(self): # pylint: disable=invalid-name """ @property - def pk(self): + def pk(self) -> int: """Return the id for this entity. This is unique only amongst entities of this type for a particular backend. @@ -68,11 +71,10 @@ def store(self): @property @abc.abstractmethod - def is_stored(self): + def is_stored(self) -> bool: """Return whether the entity is stored. :return: True if stored, False otherwise - :rtype: bool """ def _flush_if_stored(self, fields): @@ -85,10 +87,9 @@ class BackendCollection(typing.Generic[EntityType]): ENTITY_CLASS = None # type: EntityType - def __init__(self, backend): + def __init__(self, backend: 'Backend'): """ :param backend: the backend this collection belongs to - :type backend: :class:`aiida.orm.implementation.Backend` """ assert issubclass(self.ENTITY_CLASS, BackendEntity), 'Must set the ENTRY_CLASS class variable to an entity type' self._backend = backend @@ -103,12 +104,8 @@ def from_dbmodel(self, dbmodel): return self.ENTITY_CLASS.from_dbmodel(dbmodel, self.backend) @property - def backend(self): - """ - Return the backend. - - :rtype: :class:`aiida.orm.implementation.Backend` - """ + def backend(self) -> 'Backend': + """Return the backend.""" return self._backend def create(self, **kwargs): diff --git a/aiida/orm/implementation/nodes.py b/aiida/orm/implementation/nodes.py index 8ab2ccb9b2..f28b0307b1 100644 --- a/aiida/orm/implementation/nodes.py +++ b/aiida/orm/implementation/nodes.py @@ -8,7 +8,6 @@ # For further information please visit http://www.aiida.net # ########################################################################### """Abstract BackendNode and BackendNodeCollection implementation.""" - import abc from .entities import BackendCollection, BackendEntity, BackendEntityAttributesMixin, BackendEntityExtrasMixin @@ -29,16 +28,12 @@ def clone(self): """ @property - def uuid(self): + def uuid(self) -> str: """Return the node UUID. :return: the string representation of the UUID - :rtype: str or None """ - if self._dbmodel.uuid: - return str(self._dbmodel.uuid) - - return None + return str(self._dbmodel.uuid) @property def node_type(self): diff --git a/aiida/orm/implementation/users.py b/aiida/orm/implementation/users.py index 9b0df7fef0..8b052403d1 100644 --- a/aiida/orm/implementation/users.py +++ b/aiida/orm/implementation/users.py @@ -24,15 +24,6 @@ class BackendUser(BackendEntity): REQUIRED_FIELDS = ['first_name', 'last_name', 'institution'] - @property - def uuid(self): - """ - For now users do not have UUIDs so always return false - - :return: None - """ - return None - @property @abc.abstractmethod def email(self): diff --git a/aiida/orm/logs.py b/aiida/orm/logs.py index 36b6c20ba2..b15d63d32f 100644 --- a/aiida/orm/logs.py +++ b/aiida/orm/logs.py @@ -8,8 +8,9 @@ # For further information please visit http://www.aiida.net # ########################################################################### """Module for orm logging abstract classes""" +from datetime import datetime import logging -from typing import TYPE_CHECKING, List, Optional, Type +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type from aiida.common import timezone from aiida.common.lang import classproperty @@ -19,6 +20,7 @@ if TYPE_CHECKING: from aiida.orm import Node + from aiida.orm.implementation import Backend, BackendLog from aiida.orm.querybuilder import FilterType, OrderByType __all__ = ('Log', 'OrderSpecifier', 'ASCENDING', 'DESCENDING') @@ -41,14 +43,12 @@ class LogCollection(entities.Collection['Log']): def _entity_base_cls() -> Type['Log']: return Log - def create_entry_from_record(self, record: logging.LogRecord) -> 'Log': + def create_entry_from_record(self, record: logging.LogRecord) -> Optional['Log']: """Helper function to create a log entry from a record created as by the python logging library :param record: The record created by the logging module :return: A stored log instance """ - from datetime import datetime - dbnode_id = record.__dict__.get('dbnode_id', None) # Do not store if dbnode_id is not set @@ -123,7 +123,7 @@ def delete_many(self, filters: 'FilterType') -> List[int]: return self._backend.logs.delete_many(filters) -class Log(entities.Entity): +class Log(entities.Entity['BackendLog']): """ An AiiDA Log entity. Corresponds to a logged message against a particular AiiDA node. """ @@ -134,31 +134,25 @@ class Log(entities.Entity): def objects(cls) -> LogCollection: # pylint: disable=no-self-argument return LogCollection.get_cached(cls, get_manager().get_backend()) - def __init__(self, time, loggername, levelname, dbnode_id, message='', metadata=None, backend=None): # pylint: disable=too-many-arguments + def __init__( + self, + time: datetime, + loggername: str, + levelname: str, + dbnode_id: int, + message: str = '', + metadata: Optional[Dict[str, Any]] = None, + backend: Optional['Backend'] = None + ): # pylint: disable=too-many-arguments """Construct a new log :param time: time - :type time: :class:`!datetime.datetime` - :param loggername: name of logger - :type loggername: str - :param levelname: name of log level - :type levelname: str - :param dbnode_id: id of database node - :type dbnode_id: int - :param message: log message - :type message: str - :param metadata: metadata - :type metadata: dict - :param backend: database backend - :type backend: :class:`aiida.orm.implementation.Backend` - - """ from aiida.common import exceptions @@ -181,61 +175,65 @@ def __init__(self, time, loggername, levelname, dbnode_id, message='', metadata= self.store() # Logs are immutable and automatically stored @property - def time(self): + def uuid(self) -> str: + """Return the UUID for this log. + + This identifier is unique across all entities types and backend instances. + + :return: the entity uuid + """ + return self._backend_entity.uuid + + @property + def time(self) -> datetime: """ Get the time corresponding to the entry :return: The entry timestamp - :rtype: :class:`!datetime.datetime` """ return self._backend_entity.time @property - def loggername(self): + def loggername(self) -> str: """ The name of the logger that created this entry :return: The entry loggername - :rtype: str """ return self._backend_entity.loggername @property - def levelname(self): + def levelname(self) -> str: """ The name of the log level :return: The entry log level name - :rtype: str """ return self._backend_entity.levelname @property - def dbnode_id(self): + def dbnode_id(self) -> int: """ Get the id of the object that created the log entry :return: The id of the object that created the log entry - :rtype: int """ return self._backend_entity.dbnode_id @property - def message(self): + def message(self) -> str: """ Get the message corresponding to the entry :return: The entry message - :rtype: str """ return self._backend_entity.message @property - def metadata(self): + def metadata(self) -> Dict[str, Any]: """ Get the metadata corresponding to the entry :return: The entry metadata - :rtype: dict """ return self._backend_entity.metadata diff --git a/aiida/orm/nodes/node.py b/aiida/orm/nodes/node.py index 66ab7b14fa..723705f109 100644 --- a/aiida/orm/nodes/node.py +++ b/aiida/orm/nodes/node.py @@ -84,7 +84,9 @@ def delete(self, pk: int) -> None: self._backend.nodes.delete(pk) -class Node(Entity, NodeRepositoryMixin, EntityAttributesMixin, EntityExtrasMixin, metaclass=AbstractNodeMeta): +class Node( + Entity['BackendNode'], NodeRepositoryMixin, EntityAttributesMixin, EntityExtrasMixin, metaclass=AbstractNodeMeta +): """ Base class for all nodes in AiiDA. @@ -154,10 +156,6 @@ def __init__( ) super().__init__(backend_entity) - @property - def backend_entity(self) -> 'BackendNode': - return super().backend_entity - def __eq__(self, other: Any) -> bool: """Fallback equality comparison by uuid (can be overwritten by specific types)""" if isinstance(other, Node) and self.uuid == other.uuid: diff --git a/aiida/orm/querybuilder.py b/aiida/orm/querybuilder.py index d93965b1b6..82444e5d55 100644 --- a/aiida/orm/querybuilder.py +++ b/aiida/orm/querybuilder.py @@ -1198,7 +1198,9 @@ def _get_ormclass_from_cls(cls: EntityClsType) -> Tuple[EntityTypes, Classifier] classifiers = Classifier(cls.class_node_type) # type: ignore[union-attr] ormclass = EntityTypes.NODE elif issubclass(cls, groups.Group): - classifiers = Classifier(GROUP_ENTITY_TYPE_PREFIX + cls._type_string) # type: ignore[union-attr] + type_string = cls._type_string + assert type_string is not None, 'Group not registered as entry point' + classifiers = Classifier(GROUP_ENTITY_TYPE_PREFIX + type_string) ormclass = EntityTypes.GROUP elif issubclass(cls, computers.Computer): classifiers = Classifier('computer') diff --git a/aiida/orm/users.py b/aiida/orm/users.py index d00d889388..6fa11c7573 100644 --- a/aiida/orm/users.py +++ b/aiida/orm/users.py @@ -8,7 +8,7 @@ # For further information please visit http://www.aiida.net # ########################################################################### """Module for the ORM user class.""" -from typing import TYPE_CHECKING, Optional, Tuple, Type +from typing import TYPE_CHECKING, Optional, Tuple, Type, Union, cast from aiida.common import exceptions from aiida.common.lang import classproperty @@ -17,7 +17,7 @@ from . import entities if TYPE_CHECKING: - from aiida.orm.implementation import Backend + from aiida.orm.implementation import Backend, BackendUser __all__ = ('User',) @@ -26,7 +26,7 @@ class UserCollection(entities.Collection['User']): """The collection of users stored in a backend.""" UNDEFINED = 'UNDEFINED' - _default_user: Optional['User'] = None + _default_user: Union[None, str, 'User'] = None @staticmethod def _entity_base_cls() -> Type['User']: @@ -49,7 +49,7 @@ def get_or_create(self, email: str, **kwargs) -> Tuple[bool, 'User']: except exceptions.NotExistent: return True, User(backend=self.backend, email=email, **kwargs) - def get_default(self) -> 'User': + def get_default(self) -> Optional['User']: """Get the current default user""" if self._default_user is self.UNDEFINED: from aiida.manage.configuration import get_profile @@ -63,7 +63,7 @@ def get_default(self) -> 'User': except (exceptions.MultipleObjectsError, exceptions.NotExistent): self._default_user = None - return self._default_user + return cast(Optional['User'], self._default_user) def reset(self) -> None: """ @@ -72,7 +72,7 @@ def reset(self) -> None: self._default_user = self.UNDEFINED -class User(entities.Entity): +class User(entities.Entity['BackendUser']): """AiiDA User""" Collection = UserCollection @@ -83,7 +83,14 @@ def objects(cls) -> UserCollection: # pylint: disable=no-self-argument REQUIRED_FIELDS = ['first_name', 'last_name', 'institution'] - def __init__(self, email, first_name='', last_name='', institution='', backend=None): + def __init__( + self, + email: str, + first_name: str = '', + last_name: str = '', + institution: str = '', + backend: Optional['Backend'] = None + ): """Create a new `User`.""" # pylint: disable=too-many-arguments backend = backend or get_manager().get_backend() @@ -91,11 +98,11 @@ def __init__(self, email, first_name='', last_name='', institution='', backend=N backend_entity = backend.users.create(email, first_name, last_name, institution) super().__init__(backend_entity) - def __str__(self): + def __str__(self) -> str: return self.email @staticmethod - def normalize_email(email): + def normalize_email(email: str) -> str: """Normalize the address by lowercasing the domain part of the email address (taken from Django).""" email = email or '' try: @@ -107,38 +114,38 @@ def normalize_email(email): return email @property - def email(self): + def email(self) -> str: return self._backend_entity.email @email.setter - def email(self, email): + def email(self, email: str) -> None: self._backend_entity.email = email @property - def first_name(self): + def first_name(self) -> str: return self._backend_entity.first_name @first_name.setter - def first_name(self, first_name): + def first_name(self, first_name: str) -> None: self._backend_entity.first_name = first_name @property - def last_name(self): + def last_name(self) -> str: return self._backend_entity.last_name @last_name.setter - def last_name(self, last_name): + def last_name(self, last_name: str) -> None: self._backend_entity.last_name = last_name @property - def institution(self): + def institution(self) -> str: return self._backend_entity.institution @institution.setter - def institution(self, institution): + def institution(self, institution: str) -> None: self._backend_entity.institution = institution - def get_full_name(self): + def get_full_name(self) -> str: """ Return the user full name @@ -155,10 +162,17 @@ def get_full_name(self): return full_name - def get_short_name(self): + def get_short_name(self) -> str: """ Return the user short name (typically, this returns the email) :return: The short name """ return self.email + + @property + def uuid(self) -> None: + """ + For now users do not have UUIDs so always return None + """ + return None diff --git a/aiida/orm/utils/loaders.py b/aiida/orm/utils/loaders.py index d04be07713..b3981373da 100644 --- a/aiida/orm/utils/loaders.py +++ b/aiida/orm/utils/loaders.py @@ -10,11 +10,15 @@ """Module with `OrmEntityLoader` and its sub classes that simplify loading entities through their identifiers.""" from abc import abstractmethod from enum import Enum +from typing import TYPE_CHECKING from aiida.common.exceptions import MultipleObjectsError, NotExistent from aiida.common.lang import classproperty from aiida.orm.querybuilder import QueryBuilder +if TYPE_CHECKING: + from aiida.orm import Code, Computer, Group, Node + __all__ = ( 'load_code', 'load_computer', 'load_group', 'load_node', 'load_entity', 'get_loader', 'OrmEntityLoader', 'CalculationEntityLoader', 'CodeEntityLoader', 'ComputerEntityLoader', 'GroupEntityLoader', 'NodeEntityLoader' @@ -86,7 +90,7 @@ def load_entity( ) -def load_code(identifier=None, pk=None, uuid=None, label=None, sub_classes=None, query_with_dashes=True): +def load_code(identifier=None, pk=None, uuid=None, label=None, sub_classes=None, query_with_dashes=True) -> 'Code': """ Load a Code instance by one of its identifiers: pk, uuid or label @@ -117,7 +121,9 @@ def load_code(identifier=None, pk=None, uuid=None, label=None, sub_classes=None, ) -def load_computer(identifier=None, pk=None, uuid=None, label=None, sub_classes=None, query_with_dashes=True): +def load_computer( + identifier=None, pk=None, uuid=None, label=None, sub_classes=None, query_with_dashes=True +) -> 'Computer': """ Load a Computer instance by one of its identifiers: pk, uuid or label @@ -148,7 +154,7 @@ def load_computer(identifier=None, pk=None, uuid=None, label=None, sub_classes=N ) -def load_group(identifier=None, pk=None, uuid=None, label=None, sub_classes=None, query_with_dashes=True): +def load_group(identifier=None, pk=None, uuid=None, label=None, sub_classes=None, query_with_dashes=True) -> 'Group': """ Load a Group instance by one of its identifiers: pk, uuid or label @@ -179,7 +185,7 @@ def load_group(identifier=None, pk=None, uuid=None, label=None, sub_classes=None ) -def load_node(identifier=None, pk=None, uuid=None, label=None, sub_classes=None, query_with_dashes=True): +def load_node(identifier=None, pk=None, uuid=None, label=None, sub_classes=None, query_with_dashes=True) -> 'Node': """ Load a node by one of its identifiers: pk or uuid. If the type of the identifier is unknown simply pass it without a keyword and the loader will attempt to infer the type diff --git a/aiida/plugins/factories.py b/aiida/plugins/factories.py index 04a54392c0..079ab29c19 100644 --- a/aiida/plugins/factories.py +++ b/aiida/plugins/factories.py @@ -10,7 +10,7 @@ # pylint: disable=invalid-name,cyclic-import """Definition of factories to load classes from the various plugin groups.""" from inspect import isclass -from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Type, Union from importlib_metadata import EntryPoint @@ -264,12 +264,11 @@ def SchedulerFactory(entry_point_name: str, load: bool = True) -> Optional[Union raise_invalid_type_error(entry_point_name, entry_point_group, valid_classes) -def TransportFactory(entry_point_name: str, load: bool = True) -> Optional[Union[EntryPoint, 'Transport']]: +def TransportFactory(entry_point_name: str, load: bool = True) -> Optional[Union[EntryPoint, Type['Transport']]]: """Return the `Transport` sub class registered under the given entry point. :param entry_point_name: the entry point name. :param load: if True, load the matched entry point and return the loaded resource instead of the entry point itself. - :return: sub class of :py:class:`~aiida.transports.transport.Transport` :raises aiida.common.InvalidEntryPointTypeError: if the type of the loaded entry point is invalid. """ from aiida.transports import Transport diff --git a/docs/source/nitpick-exceptions b/docs/source/nitpick-exceptions index e03be84f3c..d7bfb15798 100644 --- a/docs/source/nitpick-exceptions +++ b/docs/source/nitpick-exceptions @@ -47,12 +47,15 @@ py:meth aiida.orm.groups.GroupCollection.delete py:class Backend py:class BackendEntity +py:class BackendEntityType py:class BackendNode py:class AuthInfo py:class CalcJob py:class CalcJobImporter py:class CalcJobNode +py:class Code py:class CollectionType +py:class Computer py:class Data py:class DbImporter py:class EntityType @@ -85,6 +88,7 @@ py:class SelfType py:class TransactionType py:class Transport py:class TransportQueue +py:class User py:class WorkChain py:class WorkChainSpec