-
Notifications
You must be signed in to change notification settings - Fork 192
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
Small changes to List
and Dict
data types API
#4495
Comments
Completely agree on the first point. For the In [1]: from aiida import orm
In [2]: x = orm.List(list=[1, 2, 3])
In [3]: x.value.append(4)
In [4]: x.store()
Out[4]: ...
In [5]: x.value.append(5)
In [6]: x
Out[6]: ... To be consistent with e.g.
Here's what currently happens with In [1]: from aiida import orm
In [2]: x = orm.List(list=[1, 2, 3])
In [3]: x.get_list().append(4)
In [4]: x.store()
Out[4]: <List: uuid: 412e131b-60c7-47fa-a76e-48812541a179 (pk: 61311) value: [1, 2, 3, 4]>
In [5]: x.get_list().append(5)
In [6]: x
Out[6]: <List: uuid: ac068ff6-4182-477c-82c3-59f034b539d2 (pk: 61312) value: [1, 2, 3, 4]> I think this could be a source of subtle bugs - maybe we should take the opportunity of introducing |
That's a good point, I hadn't considered this. I agree with your proposed behavior, i.e. make Initially I was inclined to suggest making Maybe for now we can just copy the |
Just another note regarding the Currently, when you try to pass a e.g. In [1]: f1 = Float(1)
In [2]: f2 = Float(f1) However, trying to do the same with a In [3]: d1 = Dict(dict={'a': 1})
In [4]: d2 = Dict(dict=d1)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-4-158c214430d4> in <module>
----> 1 d2 = Dict(dict=d1)
~/envs/aiida-dev/code/aiida-core/aiida/orm/nodes/data/dict.py in __init__(self, **kwargs)
60 super().__init__(**kwargs)
61 if dictionary:
---> 62 self.set_dict(dictionary)
63
64 def __getitem__(self, key):
~/envs/aiida-dev/code/aiida-core/aiida/orm/nodes/data/dict.py in set_dict(self, dictionary)
81 # Clear existing attributes and set the new dictionary
82 self.clear_attributes()
---> 83 self.update_dict(dictionary)
84 except exceptions.ModificationNotAllowed: # pylint: disable=try-except-raise
85 # I reraise here to avoid to go in the generic 'except' below that would raise the same exception again
~/envs/aiida-dev/code/aiida-core/aiida/orm/nodes/data/dict.py in update_dict(self, dictionary)
98 :param dictionary: a dictionary with the keys to substitute
99 """
--> 100 for key, value in dictionary.items():
101 self.set_attribute(key, value)
102
AttributeError: 'Dict' object has no attribute 'items' I'm inclined to allow this, creating a new Once again, the equality comparisons are relevant here. For the In [5]: f1 == f2
Out[5]: True
In [6]: f1 is f2
Out[6]: False I.e. their value is the same, but the node is different. Note that this doesn't correspond to the Python base type behaviour for In [9]: f3 = float(3)
In [10]: f4 = float(f3)
In [11]: f3 == f4
Out[11]: True
In [12]: f3 is f4
Out[12]: True
In [13]: id(f3)
Out[13]: 140577203382032
In [14]: id(f4)
Out[14]: 140577203382032 But this does correspond to the behaviour for In [15]: d3 = {'a': 1}
In [16]: d4 = dict(d3)
In [17]: d3 == d4
Out[17]: True
In [18]: d3 is d4
Out[18]: False
In [19]: id(d3)
Out[19]: 140577170248320
In [20]: id(d4)
Out[20]: 140577168878912 |
To move forward with this, I'd suggest three changes:
I think all of these can be achieved by making both |
Since this is backwards compatible and makes the interface easier, I am fully in favor of this.
Would be fine with this, but what do we do with the
I wouldn't touch this for now. There is a reason that Python base types also behave differently when it comes to equality, as you have noticed. Since what the "correct" behavior should be is not at all trivial (as evidenced by the long discussions in #1917 ) I wouldn't hastily change this in this PR. I would keep it limited to the changes listed above. |
I would keep it, having one more method/property (that actually returns something different) does not seem a big issue to me |
Also fine by me, but I am already anticipating the questions why there are two and they operate differently :) |
Marking this as critical-blocking as this should go in v2.0. Even if nothing is changed, than that decision should be documented such that we don't come back to this discussion everytime |
I've opened #5165 to adapt the constructor, but adding the In [1]: x = List(list=[1, 2, 3])
In [2]: x.store()
Out[2]: ...
In [3]: x.value.append(5)
--> This should raise! However, the In [1]: s = Str('test')
In [2]: s.store()
Out[2]: <Str: uuid: ad95e837-d158-48c1-af9a-f3b0b67e0377 (pk: 5) value: test>
In [3]: s.value.capitalize()
Out[3]: 'Test'
In [4]: s.value
Out[4]: 'test' So I'm not sure how we would fix this, and if this is even desirable. 😅 |
To me this is not surprising. That is to say, the exact same happens with node attributes. After all, the In [1]: d = Data()
In [2]: d.set_attribute('l', [1, 2])
In [3]: d.get_attribute('l')
Out[3]: [1, 2]
In [4]: d.get_attribute('l').append(3)
In [5]: d.get_attribute('l')
Out[5]: [1, 2, 3]
In [6]: d.store()
Out[6]: <Data: uuid: bebd05e6-0d3f-4328-8056-5bba433698ee (pk: 3)>
In [7]: d.get_attribute('l').append(4)
In [8]: d.get_attribute('l')
Out[8]: [1, 2, 3] This is on purpose though. The def get_attribute(self, key, default=_NO_DEFAULT):
"""Return the value of an attribute.
.. warning:: While the entity is unstored, this will return a reference of the attribute on the database model,
meaning that changes on the returned value (if they are mutable themselves, e.g. a list or dictionary) will
automatically be reflected on the database model as well. As soon as the entity is stored, the returned
attribute will be a deep copy and mutations of the database attributes will have to go through the
appropriate set methods.
:param key: name of the attribute
:param default: return this value instead of raising if the attribute does not exist
:return: the value of the attribute
:raises AttributeError: if the attribute does not exist and no default is specified
"""
try:
attribute = self.backend_entity.get_attribute(key)
except AttributeError:
if default is _NO_DEFAULT:
raise
attribute = default
if self.is_stored:
attribute = copy.deepcopy(attribute)
return attribute So the problematic behavior that @greschd describes is actually intentional behavior and has been part of AiiDA for a long time. I would say this is simply a question of documenting if it hasn't already been done. |
Wouldn't it then be more consistent to always return a deepcopy, regardless of store status? For modifying, it would make more sense to me to have methods (e.g. I definitely see @sphuber's point though that this has been the behavior for a long time now, so there's a significant risk associated with changing it. |
To move towards closing this issue: the constructor has been adapted in #5165, and the issue of comparing node equality by content for the
For v2.0, I would suggest implementing [1] so the API across the base data types is consistent. After v2.0 we can look into implementing [3], but the issue of inconsistent behaviour between nodes before and after being stored is a more general one that will require more extensive discussion to see the implications of changing this. |
I vote for approach 1. When in the future we go for 3, we should also make |
Since the bulk of this issue has been handled and only the |
I'd like to discuss two small changes to the API of the
List
andDict
classes:Initialization
Is your feature request related to a problem? Please describe
When initializing a
List
orDict
node, you have to passdict
orlist
as a keyword argument:If you try to instead just pass the list as a positional argument, you are faced with an error that doesn't clarify proper usage:
and the docstring doesn't explain that you have to use the
list
keyword argument either:Describe the solution you'd like
I think it would be more intuitive if the user could just pass the list/dict as the first positional argument. For example, we could change the current constructor:
to something like (thanks to @giovannipizzi for the suggestion to import the
builtins
module to avoid conflicts between thelist
argument and builtin, see this discussion):Similarly for
Dict
. If for some reason we cannot make this change, we should at least add a docstring that clearly explains the API to the user.Using
.value
Is your feature request related to a problem? Please describe
For mose data types in AiiDA derived from builtin Python types, you can use the
value
property to get the corresponding Python builtin. ForList
andDict
nodes, however, you have to use theget_list()
andget_dict()
methods. Adding thevalue
property to these classes would make the API between the "builtin" AiiDA data types more consistent.Describe the solution you'd like
Add the
value
property to both theList
andDict
classes.The text was updated successfully, but these errors were encountered: