-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Apply defaults to fields with None value at 'set' time. #349
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
Conversation
If a field has a default, and you explicitly set it to None, the behaviour before this patch was very confusing: class Person(Document): created = DateTimeField(default=datetime.datetime.utcnow) >>> p = Person(created=None) >>> p.created datetime.datetime(2013, 5, 30, 0, 18, 20, 242628) >>> p.created datetime.datetime(2013, 5, 30, 0, 18, 20, 995248) >>> p.created datetime.datetime(2013, 5, 30, 0, 18, 21, 370578) It would be stored as None, and then at 'get' time, the default would be applied. As you can see, if the default is a generator, this leads to some crazy behaviour. There's an argument that if I asked it to be set to None, why not respect that? But I don't think that's how the rest of mongoengine seems to work (for example, setting a field to None seems to mean it doesn't even get set in mongo - as opposed to being set but with a 'null' value). Besides, as the code shows above, you'd expect p.created to return None. So clearly, mongoengine is already expecting None to mean 'default' where a default is available. This bug also interacts nastily with required=True - if you're forcibly setting the field to None, then at validation time, the None will fail validation despite a perfectly valid default being available. With this patch, when the field is set, the default is immediately applied. This means any generation happens once, the getter always returns the same value, and 'required' validation always respects the default. Note: this breakage seems to be new since mongoengine 0.8.
The difficulty here is having a non- My suggestion here is that when you Sure Edited to add: this follows with my concept of document instance initialization. If I do not pass the keyword value, the value is missing, not |
You've said " The reason I advance this patch, is because currently, mongoengine does not distinguish between When I say it does not distinguish between This script helps demonstrate this behaviour - after it's run, in mongo you end up with these records, and as you can see, there's no distinction between setting The problem with trying to treat Another problem is when you come to read the attribute value. Currently, if you never set it, the attribute getter will return It seems to me like there's only two ways this can work sensibly:
Number 2 sounds like the greater of two evils - all existing databases using mongoengine would have to somehow have their explicit |
A clarification: above, I said this: "when you set a value to What actually happens depends on your mongoengine version. In 0.7, when you set a field with a default to In 0.8, when you set a field with a default to This is the behaviour change that lead me to write this patch in the first place, as we have been using mongoengine since 0.7.5, and thus have much code that relies on the " |
There are two issues here. Setting of a default value and setting a value to None. I think when the callable is called eg it should probably set the value explicitly in _data - to stop it being called all the time. I think if you set something to be None - it should be set to None - even if it wont validate later - I just dont know how much work that is... |
@rozza if you treat "setting to I can see how you could store them differently in mongo itself - an explicit And if you do decide to do it this way, there's all the existing code out there that assumes that FYI, one example of where I've found the
In 0.7, this worked nicely, and was quite concise. If there's no ID, we technically did a "set ID to And an FYI for others working around this issue, we changed the line above to this as a way to get back 0.7 behaviour:
|
Aargh, I just experienced this! It took me a lot of time to find this issue and understand what the behavior is. I consider it to be very confusing. |
Interestingly I'm not seeing a difference in behavour for 0.7 and 0.8 with this test - eg RecapAs theres lots of discussion in this thread - lets clarify the broken behaviours with a recap.
However this succeeds - so this approach sounds broken to me:
What is Broken?The storage of data:
Compare that to:
Solutions?
Have I missed anything? I thinking reverting back to 0.7 behaviour (A) is the best choice and your solution @nigelmcnie looks good. I wonder if we should pass the responsibility to the field itself on how to handle being set to None? |
|
Things are even stranger than they seem. Apologies for the massive wall of text and code that follows. Test ClassThe following is my test case's model. The fields are named logically based on the field definition itself. from random import randint
from mongoengine import Document, StringField
class Test(Document):
class Test(Document):
meta = dict(allow_inheritance=False)
srd = StringField(required=True, default='d') # string required default
src = StringField(required=True, default=lambda: str(randint(1000000, 9999999))) # string required callable-default
snd = StringField(required=False, default='d') # string not-required default
snc = StringField(required=False, default=lambda: str(randint(1000000, 9999999))) # string not-required callable-default Object CreationThis works as expected: >>> a = Test()
>>> a._data # Omission looks correct; callable defaults are immediately evaluated.
{'id': None, 'srd': u'd', 'src': u'6495825', 'snd': u'd', 'snc': u'3364130'}
>>> a.src == a.src and a.snc == a.snc # Because these have values, the defaults shouldn't regenerate.
True Explicit ReassignmentThis illustrates one of the problems: if >>> a.src = None # Explicitly set.
>>> a._data['src'] # None
>>> a.src == a.src # Generates a new value on each access!
False
>>> a._data['src'] # None; new values never recorded Additionally, even though attribute access generates a new value, if the field is required validation fails early as the value is Explicit DeleteThis is simply unfortunate. By defining this behaviour to mean "regenerate the default" we can still allow >>> del a.snc # Does nothing!
>>> 'snc' in a._data and bool(a._data['snc'])
True Explicit ConstructionThe problem outlined above is consistent between reassignment and initial construction: >>> b = Test(srd=None, src=None, snd=None, snc=None)
>>> b.src == b.src
False Non-Required Defaults and SavingThis is where things go from "well, that's odd" into bizarro land. ;) >>> c = Test(snc=None) # Set the non-required defaulted field.
>>> c._data['snc'] # None
>>> c.save()
>>> c._data['snc'] # Still None!
>>> c.snc == c.snc # As above, generates a new value…
False Now, what was actually saved to the database? >>> d = Test.objects.get(id=c.id)
>>> d._data['snc'] # NOT NONE! WUT?!
'6047255'
>>> d.snc == d.snc
True
>>> c.snc == d.snc # NOT THE SAME (c.snc regenerates on each access)
False If a non-required field with a default value is set to I've also verified that |
@amcgregor I ran through your testing, I noticed one difference from what you wrote.
However, this doesn't matter too much - I think we (and anyone reading this pull request in future) have a reasonable handle on the problems now, which is good. I have tried your series of tests with my patch applied, and it seems that the behaviour quirks and outright craziness you found are all resolved:
So, regarding solutions... I suggest that we pick (A) (of course I do ;)). I also think there could be some merit in the idea of allowing the field itself to define how it wants to treat |
Now field defaults are king, unsetting or setting to None on a field with a default means the default is reapplied.
Ok - I've cleaned up a little and added those testcases! Thanks for all your help @nigelmcnie and @amcgregor :) |
If a field has a default, and you explicitly set it to None, the behaviour before this patch was very confusing:
It would be stored as None, and then at 'get' time, the default would be applied. As you can see, if the default is a generator, this leads to some crazy behaviour.
There's an argument that if I asked it to be set to None, why not respect that? But I don't think that's how the rest of mongoengine seems to work (for example, setting a field to None seems to mean it doesn't even get set in mongo - as opposed to being set but with a 'null' value). Besides, as the code shows above, you'd expect p.created to return None. So clearly, mongoengine is already expecting None to mean 'default' where a default is available.
This bug also interacts nastily with required=True - if you're forcibly setting the field to None, then at validation time, the None will fail validation despite a perfectly valid default being available.
With this patch, when the field is set, the default is immediately applied. This means any generation happens once, the getter always returns the same value, and 'required' validation always respects the default.
Note: this breakage seems to be new since mongoengine 0.8.