-
Notifications
You must be signed in to change notification settings - Fork 238
Add typing to aiida/common/{utils,extendeddicts}.py #6706
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
eb22567
e9cec1f
2e2b667
6669344
4f6de68
7ff9f59
17aeeeb
270852b
c757fec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,12 @@ | |
| ########################################################################### | ||
| """Various dictionary types with extended functionality.""" | ||
|
|
||
| from collections.abc import Mapping | ||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Hashable, KeysView, Mapping | ||
| from typing import Any | ||
|
|
||
| from aiida.common.typing import Self | ||
|
|
||
| from . import exceptions | ||
|
|
||
|
|
@@ -25,7 +30,7 @@ class AttributeDict(dict): | |
| used. | ||
| """ | ||
|
|
||
| def __init__(self, dictionary=None): | ||
| def __init__(self, dictionary: Mapping | None = None): | ||
| """Recursively turn the `dict` and all its nested dictionaries into `AttributeDict` instance.""" | ||
| super().__init__() | ||
| if dictionary is None: | ||
|
|
@@ -37,11 +42,11 @@ def __init__(self, dictionary=None): | |
| else: | ||
| self[key] = value | ||
|
|
||
| def __repr__(self): | ||
| def __repr__(self) -> str: | ||
| """Representation of the object.""" | ||
| return f'{self.__class__.__name__}({dict.__repr__(self)})' | ||
|
|
||
| def __getattr__(self, attr): | ||
| def __getattr__(self, attr: Hashable) -> Any: | ||
| """Read a key as an attribute. | ||
|
|
||
| :raises AttributeError: if the attribute does not correspond to an existing key. | ||
|
|
@@ -52,7 +57,7 @@ def __getattr__(self, attr): | |
| errmsg = f"'{self.__class__.__name__}' object has no attribute '{attr}'" | ||
| raise AttributeError(errmsg) | ||
|
|
||
| def __setattr__(self, attr, value): | ||
| def __setattr__(self, attr: str, value: Any) -> None: | ||
| """Set a key as an attribute.""" | ||
| try: | ||
| self[attr] = value | ||
|
|
@@ -61,7 +66,7 @@ def __setattr__(self, attr, value): | |
| f"AttributeError: '{attr}' is not a valid attribute of the object '{self.__class__.__name__}'" | ||
| ) | ||
|
|
||
| def __delattr__(self, attr): | ||
| def __delattr__(self, attr: Hashable) -> None: | ||
| """Delete a key as an attribute. | ||
|
|
||
| :raises AttributeError: if the attribute does not correspond to an existing key. | ||
|
|
@@ -72,7 +77,7 @@ def __delattr__(self, attr): | |
| errmsg = f"'{self.__class__.__name__}' object has no attribute '{attr}'" | ||
| raise AttributeError(errmsg) | ||
|
|
||
| def __deepcopy__(self, memo=None): | ||
| def __deepcopy__(self, memo: Mapping | None = None) -> Self: | ||
| """Deep copy.""" | ||
| from copy import deepcopy | ||
|
|
||
|
|
@@ -81,15 +86,15 @@ def __deepcopy__(self, memo=None): | |
| retval = deepcopy(dict(self)) | ||
| return self.__class__(retval) | ||
|
|
||
| def __getstate__(self): | ||
| def __getstate__(self) -> dict[str, Any]: | ||
| """Needed for pickling this class.""" | ||
| return self.__dict__.copy() | ||
|
|
||
| def __setstate__(self, dictionary): | ||
| def __setstate__(self, dictionary: Mapping) -> None: | ||
| """Needed for pickling this class.""" | ||
| self.__dict__.update(dictionary) | ||
|
|
||
| def __dir__(self): | ||
| def __dir__(self) -> KeysView[Any]: | ||
| return self.keys() | ||
|
|
||
|
|
||
|
|
@@ -104,9 +109,9 @@ class TestExample(FixedFieldsAttributeDict): | |
| _valid_fields = ('a','b','c') | ||
| """ | ||
|
|
||
| _valid_fields = tuple() | ||
| _valid_fields: tuple[Any, ...] = tuple() | ||
|
|
||
| def __init__(self, init=None): | ||
| def __init__(self, init: Mapping | None = None): | ||
| if init is None: | ||
| init = {} | ||
|
|
||
|
|
@@ -116,26 +121,28 @@ def __init__(self, init=None): | |
| raise KeyError(errmsg) | ||
| super().__init__(init) | ||
|
|
||
| def __setitem__(self, item, value): | ||
| def __setitem__(self, item: Hashable, value: Any) -> None: | ||
| """Set a key as an attribute.""" | ||
| if item not in self._valid_fields: | ||
| errmsg = f"'{item}' is not a valid key for object '{self.__class__.__name__}'" | ||
| raise KeyError(errmsg) | ||
| super().__setitem__(item, value) | ||
|
|
||
| def __setattr__(self, attr, value): | ||
| def __setattr__(self, attr: str, value: Any) -> None: | ||
| """Overridden to allow direct access to fields with underscore.""" | ||
| if attr.startswith('_'): | ||
| object.__setattr__(self, attr, value) | ||
| else: | ||
| super().__setattr__(attr, value) | ||
|
|
||
| @classmethod | ||
| def get_valid_fields(cls): | ||
| def get_valid_fields(cls) -> tuple: | ||
| """Return the list of valid fields.""" | ||
| return cls._valid_fields | ||
|
|
||
| def __dir__(self): | ||
| # TODO: We're in violation of the `dict` interface here, | ||
| # we should be returning collections.abc.KeysView[Any] | ||
| def __dir__(self) -> list[Any]: # type: ignore[override] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the ignore?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're actually in violation of the dict interface here; I added a TODO comment |
||
| return list(self._valid_fields) | ||
|
|
||
|
|
||
|
|
@@ -192,9 +199,9 @@ class TestExample(DefaultFieldsAttributeDict): | |
| See if we want that setting a default field to None means deleting it. | ||
| """ | ||
|
|
||
| _default_fields = tuple() | ||
| _default_fields: tuple = tuple() | ||
|
|
||
| def validate(self): | ||
| def validate(self) -> None: | ||
| """Validate the keys, if any ``validate_*`` method is available.""" | ||
| for key in self.get_default_fields(): | ||
| # I get the attribute starting with validate_ and containing the name of the key | ||
|
|
@@ -206,14 +213,14 @@ def validate(self): | |
| except Exception as exc: | ||
| raise exceptions.ValidationError(f"Invalid value for key '{key}' [{exc.__class__.__name__}]: {exc}") | ||
|
|
||
| def __setattr__(self, attr, value): | ||
| def __setattr__(self, attr: str, value: Any) -> None: | ||
| """Overridden to allow direct access to fields with underscore.""" | ||
| if attr.startswith('_'): | ||
| object.__setattr__(self, attr, value) | ||
| else: | ||
| super().__setattr__(attr, value) | ||
|
|
||
| def __getitem__(self, key): | ||
| def __getitem__(self, key: Hashable) -> Any | None: | ||
| """Return None instead of raising an exception if the key does not exist | ||
| but is in the list of default fields. | ||
| """ | ||
|
|
@@ -225,14 +232,14 @@ def __getitem__(self, key): | |
| raise | ||
|
|
||
| @classmethod | ||
| def get_default_fields(cls): | ||
| def get_default_fields(cls) -> list: | ||
| """Return the list of default fields, either defined in the instance or not.""" | ||
| return list(cls._default_fields) | ||
|
|
||
| def defaultkeys(self): | ||
| def defaultkeys(self) -> list: | ||
| """Return the default keys defined in the instance.""" | ||
| return [_ for _ in self.keys() if _ in self._default_fields] | ||
|
|
||
| def extrakeys(self): | ||
| def extrakeys(self) -> list: | ||
| """Return the extra keys defined in the instance.""" | ||
| return [_ for _ in self.keys() if _ not in self._default_fields] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,12 @@ | |
| import pathlib | ||
| from typing import Union | ||
|
|
||
| __all__ = ('FilePath',) | ||
| try: | ||
| from typing import Self | ||
| except ImportError: | ||
| from typing_extensions import Self | ||
|
Comment on lines
+16
to
+19
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I'm finding this less friendly to the IDE's static analysis (pyright on my end, via pylance in VS Code) - neither |
||
|
|
||
| __all__ = ('FilePath', 'Self') | ||
|
|
||
|
|
||
| FilePath = Union[str, pathlib.PurePath] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made a comment on this below, but going back. Same comment though. This file only exposes a few things from typing. Let's import explicitly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I am not sure. I'd rather for us to move to a direction of using the
import typing as t. It's not clear what counts as "few", and more importantly when adding more typing to a file or adding more code easily leads to "more than a few".And also on a practical level, it's annoying when adding typing you need a new type which is not yet imported and you have to go to the top and type it out (unless you have an LSP that does it for you).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import typing as t, as long as we're consistent. But maybe we involve 1-2 more people from the core team on this.