Mongoengine: How to call __init__ for Document to init values.

Created on 9 Jun 2013  Â·  15Comments  Â·  Source: MongoEngine/mongoengine

Hello, in SQLAlchemy there is possible to do like that:

'''python
class MyModel(Base):
...
def init(self, name=name, value=value, kwargs):
self.name = name
self.value = some_func(value)
# call constructor
super(MyModel, self).
init**()
'''

That's very nice if you need to to calculate some fields on create.

But, using Mongoengine, i was surprised that i not able to do like that.

Even if i create init function like above, i'll got error:
''''
fields.py", line 94, in set
instance._data[self.name] = value
AttributeError: 'MyModel' object has no attribute '_data'
'''

If i pass empty constructor
super(MyModel, self).init()

i got error also:
'''
document.py", line 586, in _from_son
obj = cls(__auto_convert=False, **data)
TypeError: __init__() takes at least 3 arguments (4 given)
''''

I checked code of base/document.py and looks like there is no way to do something like above. Is it bug or feature?

Most helpful comment

@seglberg @rozza

def __init__(self, *args, **kwargs):
    super(Article, self).__init__(*args, **kwargs)
    self.whatever = ...
def save(self, *args, **kwargs):
    if not self.slug:
        self.slug = make_slug(self.title)
    return super(Article, self).save(*args, **kwargs)

is the overriding of model methods like __init__ and save an accepted practice assuming super is called "appropriately"?

it seems as though for the __init__ method to work, super is called before modifying self while for save, super is called after modifying self, which makes sense as there is preinitialization logic that needs to be run and post save logic that needs to be run

is this a supported way of keeping models more pythonic or will signals be the only supported method?

All 15 comments

The task is simple, i want to calculate password hash with salt, and i want to do it on model level, since there is also method to check if passwords match. I saw this code https://github.com/hmarr/mongoengine/blob/master/mongoengine/django/auth.py with same problem, but author solve it by define new method called set_password(password).

I currently dont have plans to support this - there are hooks in initialisation that you'd have to mimic.

Obviously, if you are overriding core methods you'd need to mimic the signatures but doing pre initialisation logic isn't going to work as the _data dictionary is created in init.

Finally, why would you want to calculate a password salt on initialisation of a class from a database? Surely, its salted in the database?! I think adding a create_password method would be better suited.

Don't want to store password like it entered by user to database, i want only to store password hash with salt and have a function (in same model) to check is there this user and if password match to hash saved in model. I think that is ORM/MVC technologies about to separate data methods and storage algorithms from everything else.

I solve this tasks by overriding save method + it's possible to do it using signals (pre_save).

@istinspring thats my point you should never ever store the password in plaintext.

You could hash it via a pre save signal or override save - but the down side is you have to know if the Document you are saving already exists or not. The good thing about a generate_password_hash method is it can be used explicitly to create the users password at any point: User creation, user change password and user forgotten password.

I have another use case: Say I have an Article model with a title attribute and a slug one. On create, I set only the title, and I would like the slug to default to slugify(self.title). How to accomplish that ?

Besides that, I need other computations to be "triggered" after article creation, such as fetching contents and images from internet, etc. These computations should really be started automatically.

For now, I will fake this with a default callable on a self.inited field. The callable will return False, then will trigger a celery task with some delay (to be sure the object exists in DB) that will run all the heavy computations in the background and set self.inited = True in the end. But I would really have liked to have a cleaner machanism to do that, eg. a def init(), or better, a def post_init().

regards,

Have you tried signals?

Not yet, but it's indeed a very good suggestion ! Thanks :-)
If it works like expected, this will totally void my needs for any kind of init().

Signals works. Also you can override save() method:

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = make_slug(self.title)
        return super(Article, self).save(*args, **kwargs)

But anyway would be cool to do this kind of tasks w/o using ugly signals and overriding methods. Why not use classical approach like sqlalchemy? because mongoengine looks very similar to sqlalchemy for end programmer and this is great because if you used one you can quickly switch to another.

Here is my signal-based approach, which may be incomplete in terms of checked subclasses, but can be easily extended. [update 2013-07-03: fixed the connect_mongoengine_signals() helper]

in models.py:

class MyModel(Document):
    name       = StringField()
    url        = URLField()

    @classmethod
    def signal_post_init_handler(cls, sender, document, **kwargs):
        LOGGER.warning('POST INIT for %s', document)

… #whatever code …

connect_mongoengine_signals(globals())

And in my utils.py:

def connect_mongoengine_signals(module_globals):
    """ Automatically iterate classes of a given module and connect handlers
        to signals, given they follow the name pattern
        ``signal_<signal_name>_handler()``.

        See https://mongoengine-odm.readthedocs.org/en/latest/guide/signals.html#overview # NOQA
        for a list of valid signal names.
    """

    if blinker is None:
        LOGGER.error('blinker module is not installed, '
                     'cannot connect mongoengine signals!')
        return

    for key, potential_class in module_globals.items():

        # TODO: use ReferenceDocument and other Mongo classes if appropriate?
        try:
            look_for_handlers = issubclass(potential_class, Document)

        except:
            # potential_class is definitely not a class/document ;-)
            continue

        if look_for_handlers:
            for signal_name in ('pre_init', 'post_init', 'pre_save',
                                'pre_save_post_validation', 'post_save',
                                'pre_delete', 'post_delete',
                                'pre_bulk_insert', 'post_bulk_insert'):
                handler_name = 'signal_{0}_handler'.format(signal_name)
                if hasattr(potential_class, handler_name):
                    getattr(signals, signal_name).connect(
                        getattr(potential_class, handler_name),
                        sender=potential_class)
                    LOGGER.info('Connected %s.%s to signal %s of sender %s.',
                                potential_class.__name__,
                                handler_name, signal_name,
                                potential_class.__name__)

The calling of this function could be easily moved to some startup parts of django, to avoid the need of calling it in each models.py. This would also avoid passing the globals() argument, which can be thought of _strange_ by newcomers.

I had the same problem, getting AttributeError: 'User' object has no attribute '_data' while trying to overload the init method on a model that inherits from Document.
My solution is to call the Document class initializer first, then override/auto-generate fields, as needed:

def __init__(self, *args, **kwargs):
        Document.__init__(self, *args, **kwargs)
        if not self.some_field:
            self.some_field = function_to_create()
        if not self.created_at:
            self.created_at = datetime.datetime.now()

What if you won't inherit from Document directly?
Try super(YourClassName, self).__init__(self, *args, **kwargs) instead

Issue Backlog Cleanup: If your question or issue has not been resolved please reopen the issue. Thanks!

@seglberg @rozza

def __init__(self, *args, **kwargs):
    super(Article, self).__init__(*args, **kwargs)
    self.whatever = ...
def save(self, *args, **kwargs):
    if not self.slug:
        self.slug = make_slug(self.title)
    return super(Article, self).save(*args, **kwargs)

is the overriding of model methods like __init__ and save an accepted practice assuming super is called "appropriately"?

it seems as though for the __init__ method to work, super is called before modifying self while for save, super is called after modifying self, which makes sense as there is preinitialization logic that needs to be run and post save logic that needs to be run

is this a supported way of keeping models more pythonic or will signals be the only supported method?

Was this page helpful?
0 / 5 - 0 ratings