Merge branch 'feature/blinker_signals' into dev
This commit is contained in:
		| @@ -2,6 +2,8 @@ from queryset import QuerySet, QuerySetManager | |||||||
| from queryset import DoesNotExist, MultipleObjectsReturned | from queryset import DoesNotExist, MultipleObjectsReturned | ||||||
| from queryset import DO_NOTHING | from queryset import DO_NOTHING | ||||||
|  |  | ||||||
|  | from mongoengine import signals | ||||||
|  |  | ||||||
| import sys | import sys | ||||||
| import pymongo | import pymongo | ||||||
| import pymongo.objectid | import pymongo.objectid | ||||||
| @@ -465,6 +467,8 @@ class TopLevelDocumentMetaclass(DocumentMetaclass): | |||||||
| class BaseDocument(object): | class BaseDocument(object): | ||||||
|  |  | ||||||
|     def __init__(self, **values): |     def __init__(self, **values): | ||||||
|  |         signals.pre_init.send(self, values=values) | ||||||
|  |  | ||||||
|         self._data = {} |         self._data = {} | ||||||
|         # Assign default values to instance |         # Assign default values to instance | ||||||
|         for attr_name in self._fields.keys(): |         for attr_name in self._fields.keys(): | ||||||
| @@ -478,6 +482,8 @@ class BaseDocument(object): | |||||||
|             except AttributeError: |             except AttributeError: | ||||||
|                 pass |                 pass | ||||||
|  |  | ||||||
|  |         signals.post_init.send(self) | ||||||
|  |  | ||||||
|     def validate(self): |     def validate(self): | ||||||
|         """Ensure that all fields' values are valid and that required fields |         """Ensure that all fields' values are valid and that required fields | ||||||
|         are present. |         are present. | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | from mongoengine import signals | ||||||
| from base import (DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument, | from base import (DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument, | ||||||
|                   ValidationError) |                   ValidationError) | ||||||
| from queryset import OperationError | from queryset import OperationError | ||||||
| @@ -75,6 +76,8 @@ class Document(BaseDocument): | |||||||
|                 For example, ``save(..., w=2, fsync=True)`` will wait until at least two servers |                 For example, ``save(..., w=2, fsync=True)`` will wait until at least two servers | ||||||
|                 have recorded the write and will force an fsync on each server being written to. |                 have recorded the write and will force an fsync on each server being written to. | ||||||
|         """ |         """ | ||||||
|  |         signals.pre_save.send(self) | ||||||
|  |  | ||||||
|         if validate: |         if validate: | ||||||
|             self.validate() |             self.validate() | ||||||
|  |  | ||||||
| @@ -82,6 +85,7 @@ class Document(BaseDocument): | |||||||
|             write_options = {} |             write_options = {} | ||||||
|  |  | ||||||
|         doc = self.to_mongo() |         doc = self.to_mongo() | ||||||
|  |         created = '_id' not in doc | ||||||
|         try: |         try: | ||||||
|             collection = self.__class__.objects._collection |             collection = self.__class__.objects._collection | ||||||
|             if force_insert: |             if force_insert: | ||||||
| @@ -96,12 +100,16 @@ class Document(BaseDocument): | |||||||
|         id_field = self._meta['id_field'] |         id_field = self._meta['id_field'] | ||||||
|         self[id_field] = self._fields[id_field].to_python(object_id) |         self[id_field] = self._fields[id_field].to_python(object_id) | ||||||
|  |  | ||||||
|  |         signals.post_save.send(self, created=created) | ||||||
|  |  | ||||||
|     def delete(self, safe=False): |     def delete(self, safe=False): | ||||||
|         """Delete the :class:`~mongoengine.Document` from the database. This |         """Delete the :class:`~mongoengine.Document` from the database. This | ||||||
|         will only take effect if the document has been previously saved. |         will only take effect if the document has been previously saved. | ||||||
|  |  | ||||||
|         :param safe: check if the operation succeeded before returning |         :param safe: check if the operation succeeded before returning | ||||||
|         """ |         """ | ||||||
|  |         signals.pre_delete.send(self) | ||||||
|  |  | ||||||
|         id_field = self._meta['id_field'] |         id_field = self._meta['id_field'] | ||||||
|         object_id = self._fields[id_field].to_mongo(self[id_field]) |         object_id = self._fields[id_field].to_mongo(self[id_field]) | ||||||
|         try: |         try: | ||||||
| @@ -110,6 +118,8 @@ class Document(BaseDocument): | |||||||
|             message = u'Could not delete document (%s)' % err.message |             message = u'Could not delete document (%s)' % err.message | ||||||
|             raise OperationError(message) |             raise OperationError(message) | ||||||
|  |  | ||||||
|  |         signals.post_delete.send(self) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def register_delete_rule(cls, document_cls, field_name, rule): |     def register_delete_rule(cls, document_cls, field_name, rule): | ||||||
|         """This method registers the delete rules to apply when removing this |         """This method registers the delete rules to apply when removing this | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								mongoengine/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								mongoengine/signals.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  |  | ||||||
|  | signals_available = False | ||||||
|  | try: | ||||||
|  |     from blinker import Namespace | ||||||
|  |     signals_available = True | ||||||
|  | except ImportError: | ||||||
|  |     class Namespace(object): | ||||||
|  |         def signal(self, name, doc=None): | ||||||
|  |             return _FakeSignal(name, doc) | ||||||
|  |  | ||||||
|  |     class _FakeSignal(object): | ||||||
|  |         """If blinker is unavailable, create a fake class with the same | ||||||
|  |         interface that allows sending of signals but will fail with an | ||||||
|  |         error on anything else.  Instead of doing anything on send, it | ||||||
|  |         will just ignore the arguments and do nothing instead. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         def __init__(self, name, doc=None): | ||||||
|  |             self.name = name | ||||||
|  |             self.__doc__ = doc | ||||||
|  |  | ||||||
|  |         def _fail(self, *args, **kwargs): | ||||||
|  |             raise RuntimeError('signalling support is unavailable ' | ||||||
|  |                                'because the blinker library is ' | ||||||
|  |                                'not installed.') | ||||||
|  |         send = lambda *a, **kw: None | ||||||
|  |         connect = disconnect = has_receivers_for = receivers_for = \ | ||||||
|  |             temporarily_connected_to = _fail | ||||||
|  |         del _fail | ||||||
|  |  | ||||||
|  | # the namespace for code signals.  If you are not mongoengine code, do | ||||||
|  | # not put signals in here.  Create your own namespace instead. | ||||||
|  | _signals = Namespace() | ||||||
|  |  | ||||||
|  | pre_init = _signals.signal('pre_init') | ||||||
|  | post_init = _signals.signal('post_init') | ||||||
|  | pre_save = _signals.signal('pre_save') | ||||||
|  | post_save = _signals.signal('post_save') | ||||||
|  | pre_delete = _signals.signal('pre_delete') | ||||||
|  | post_delete = _signals.signal('post_delete') | ||||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
									
									
									
									
								
							| @@ -45,6 +45,6 @@ setup(name='mongoengine', | |||||||
|       long_description=LONG_DESCRIPTION, |       long_description=LONG_DESCRIPTION, | ||||||
|       platforms=['any'], |       platforms=['any'], | ||||||
|       classifiers=CLASSIFIERS, |       classifiers=CLASSIFIERS, | ||||||
|       install_requires=['pymongo'], |       install_requires=['pymongo', 'blinker'], | ||||||
|       test_suite='tests', |       test_suite='tests', | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										130
									
								
								tests/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								tests/signals.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | import unittest | ||||||
|  |  | ||||||
|  | from mongoengine import * | ||||||
|  | from mongoengine import signals | ||||||
|  |  | ||||||
|  | signal_output = [] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SignalTests(unittest.TestCase): | ||||||
|  |     """ | ||||||
|  |     Testing signals before/after saving and deleting. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def get_signal_output(self, fn, *args, **kwargs): | ||||||
|  |         # Flush any existing signal output | ||||||
|  |         global signal_output | ||||||
|  |         signal_output = [] | ||||||
|  |         fn(*args, **kwargs) | ||||||
|  |         return signal_output | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         connect(db='mongoenginetest') | ||||||
|  |         class Author(Document): | ||||||
|  |             name = StringField() | ||||||
|  |  | ||||||
|  |             def __unicode__(self): | ||||||
|  |                 return self.name | ||||||
|  |  | ||||||
|  |             @classmethod | ||||||
|  |             def pre_init(cls, instance, **kwargs): | ||||||
|  |                 signal_output.append('pre_init signal, %s' % cls.__name__) | ||||||
|  |                 signal_output.append(str(kwargs['values'])) | ||||||
|  |  | ||||||
|  |             @classmethod | ||||||
|  |             def post_init(cls, instance, **kwargs): | ||||||
|  |                 signal_output.append('post_init signal, %s' % instance) | ||||||
|  |  | ||||||
|  |             @classmethod | ||||||
|  |             def pre_save(cls, instance, **kwargs): | ||||||
|  |                 signal_output.append('pre_save signal, %s' % instance) | ||||||
|  |  | ||||||
|  |             @classmethod | ||||||
|  |             def post_save(cls, instance, **kwargs): | ||||||
|  |                 signal_output.append('post_save signal, %s' % instance) | ||||||
|  |                 if 'created' in kwargs: | ||||||
|  |                     if kwargs['created']: | ||||||
|  |                         signal_output.append('Is created') | ||||||
|  |                     else: | ||||||
|  |                         signal_output.append('Is updated') | ||||||
|  |  | ||||||
|  |             @classmethod | ||||||
|  |             def pre_delete(cls, instance, **kwargs): | ||||||
|  |                 signal_output.append('pre_delete signal, %s' % instance) | ||||||
|  |  | ||||||
|  |             @classmethod | ||||||
|  |             def post_delete(cls, instance, **kwargs): | ||||||
|  |                 signal_output.append('post_delete signal, %s' % instance) | ||||||
|  |  | ||||||
|  |         self.Author = Author | ||||||
|  |  | ||||||
|  |         # Save up the number of connected signals so that we can check at the end | ||||||
|  |         # that all the signals we register get properly unregistered | ||||||
|  |         self.pre_signals = ( | ||||||
|  |             len(signals.pre_init.receivers), | ||||||
|  |             len(signals.post_init.receivers), | ||||||
|  |             len(signals.pre_save.receivers), | ||||||
|  |             len(signals.post_save.receivers), | ||||||
|  |             len(signals.pre_delete.receivers), | ||||||
|  |             len(signals.post_delete.receivers) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         signals.pre_init.connect(Author.pre_init) | ||||||
|  |         signals.post_init.connect(Author.post_init) | ||||||
|  |         signals.pre_save.connect(Author.pre_save) | ||||||
|  |         signals.post_save.connect(Author.post_save) | ||||||
|  |         signals.pre_delete.connect(Author.pre_delete) | ||||||
|  |         signals.post_delete.connect(Author.post_delete) | ||||||
|  |  | ||||||
|  |     def tearDown(self): | ||||||
|  |         signals.pre_init.disconnect(self.Author.pre_init) | ||||||
|  |         signals.post_init.disconnect(self.Author.post_init) | ||||||
|  |         signals.post_delete.disconnect(self.Author.post_delete) | ||||||
|  |         signals.pre_delete.disconnect(self.Author.pre_delete) | ||||||
|  |         signals.post_save.disconnect(self.Author.post_save) | ||||||
|  |         signals.pre_save.disconnect(self.Author.pre_save) | ||||||
|  |  | ||||||
|  |         # Check that all our signals got disconnected properly. | ||||||
|  |         post_signals = ( | ||||||
|  |             len(signals.pre_init.receivers), | ||||||
|  |             len(signals.post_init.receivers), | ||||||
|  |             len(signals.pre_save.receivers), | ||||||
|  |             len(signals.post_save.receivers), | ||||||
|  |             len(signals.pre_delete.receivers), | ||||||
|  |             len(signals.post_delete.receivers) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertEqual(self.pre_signals, post_signals) | ||||||
|  |  | ||||||
|  |     def test_model_signals(self): | ||||||
|  |         """ Model saves should throw some signals. """ | ||||||
|  |  | ||||||
|  |         def create_author(): | ||||||
|  |             a1 = self.Author(name='Bill Shakespeare') | ||||||
|  |  | ||||||
|  |         self.assertEqual(self.get_signal_output(create_author), [ | ||||||
|  |             "pre_init signal, Author", | ||||||
|  |             "{'name': 'Bill Shakespeare'}", | ||||||
|  |             "post_init signal, Bill Shakespeare", | ||||||
|  |         ]) | ||||||
|  |  | ||||||
|  |         a1 = self.Author(name='Bill Shakespeare') | ||||||
|  |         self.assertEqual(self.get_signal_output(a1.save), [ | ||||||
|  |             "pre_save signal, Bill Shakespeare", | ||||||
|  |             "post_save signal, Bill Shakespeare", | ||||||
|  |             "Is created" | ||||||
|  |         ]) | ||||||
|  |  | ||||||
|  |         a1.reload() | ||||||
|  |         a1.name='William Shakespeare' | ||||||
|  |         self.assertEqual(self.get_signal_output(a1.save), [ | ||||||
|  |             "pre_save signal, William Shakespeare", | ||||||
|  |             "post_save signal, William Shakespeare", | ||||||
|  |             "Is updated" | ||||||
|  |         ]) | ||||||
|  |  | ||||||
|  |         self.assertEqual(self.get_signal_output(a1.delete), [ | ||||||
|  |             'pre_delete signal, William Shakespeare', | ||||||
|  |             'post_delete signal, William Shakespeare', | ||||||
|  |         ]) | ||||||
		Reference in New Issue
	
	Block a user