Changeset 762 for branches/4.x


Ignore:
Timestamp:
Jan 17, 2016, 11:32:18 AM (4 years ago)
Author:
cito
Message:

Docs and 100% test coverage for NotificationHandler?

Location:
branches/4.x
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • branches/4.x/docs/contents/changelog.rst

    r748 r762  
    1818- Fix notification handler (Thanks Patrick TJ McPhee).
    1919- Fix a small issue with large objects.
     20- Minor improvements in the NotificationHandler.
     21- Converted documentation to Sphinx and added many missing parts.
    2022- The tutorial files have become a chapter in the documentation.
    21 - Greatly improve unit testing, tests run with Python 2.4 to 2.7 again.
     23- Greatly improved unit testing, tests run with Python 2.4 to 2.7 again.
    2224
    2325Version 4.1.1 (2013-01-08)
    2426--------------------------
    25 - Add WhenNotified class and method.  Replaces need for third party pgnotify.
     27- Add NotificationHandler class and method.  Replaces need for pgnotify.
    2628- Sharpen test for inserting current_timestamp.
    2729- Add more quote tests.  False and 0 should evaluate to NULL.
  • branches/4.x/docs/contents/pg/db_wrapper.rst

    r749 r762  
    530530
    531531.. versionadded:: 4.1
     532
     533notification_handler -- create a notification handler
     534-----------------------------------------------------
     535
     536.. class:: DB.notification_handler(event, callback, [arg_dict], [timeout], [stop_event])
     537
     538    Create a notification handler instance
     539
     540    :param str event: the name of an event to listen for
     541    :param callback: a callback function
     542    :param dict arg_dict: an optional dictionary for passing arguments
     543    :param timeout: the time-out when waiting for notifications
     544    :type timeout: int, float or None
     545    :param str stop_event: an optional different name to be used as stop event
     546
     547This method creates a :class:`pg.NotificationHandler` object using the
     548:class:`DB` connection as explained under :doc:`notification`.
     549
     550.. versionadded:: 4.1.1
  • branches/4.x/docs/contents/pg/index.rst

    r695 r762  
    1515    query
    1616    large_objects
     17    notification
  • branches/4.x/pg.py

    r748 r762  
    144144    """A PostgreSQL client-side asynchronous notification handler."""
    145145
    146     def __init__(self, db, event, callback, arg_dict=None, timeout=None):
     146    def __init__(self, db, event, callback=None,
     147            arg_dict=None, timeout=None, stop_event=None):
    147148        """Initialize the notification handler.
    148149
    149         db       - PostgreSQL connection object.
    150         event    - Event (notification channel) to LISTEN for.
    151         callback - Event callback function.
    152         arg_dict - A dictionary passed as the argument to the callback.
    153         timeout  - Timeout in seconds; a floating point number denotes
    154                    fractions of seconds. If it is absent or None, the
    155                    callers will never time out.
    156 
    157         """
    158         if isinstance(db, DB):
    159             db = db.db
     150        You must pass a PyGreSQL database connection, the name of an
     151        event (notification channel) to listen for and a callback function.
     152
     153        You can also specify a dictionary arg_dict that will be passed as
     154        the single argument to the callback function, and a timeout value
     155        in seconds (a floating point number denotes fractions of seconds).
     156        If it is absent or None, the callers will never time out.  If the
     157        timeout is reached, the callback function will be called with a
     158        single argument that is None.  If you set the timeout to zero,
     159        the handler will poll notifications synchronously and return.
     160
     161        You can specify the name of the event that will be used to signal
     162        the handler to stop listening as stop_event. By default, it will
     163        be the event name prefixed with 'stop_'.
     164        """
    160165        self.db = db
    161166        self.event = event
    162         self.stop_event = 'stop_%s' % event
     167        self.stop_event = stop_event or 'stop_%s' % event
    163168        self.listening = False
    164169        self.callback = callback
     
    169174
    170175    def __del__(self):
    171         self.close()
     176        self.unlisten()
    172177
    173178    def close(self):
     
    195200        """Generate a notification.
    196201
    197         Note: If the main loop is running in another thread, you must pass
    198         a different database connection to avoid a collision.
    199 
    200         The payload parameter is only supported in PostgreSQL >= 9.0.
    201 
    202         """
    203         if not db:
    204             db = self.db
     202        Optionally, you can pass a payload with the notification.
     203
     204        If you set the stop flag, a stop notification will be sent that
     205        will cause the handler to stop listening.
     206
     207        Note: If the notification handler is running in another thread, you
     208        must pass a different database connection since PyGreSQL database
     209        connections are not thread-safe.
     210        """
    205211        if self.listening:
     212            if not db:
     213                db = self.db
    206214            q = 'notify "%s"' % (stop and self.stop_event or self.event)
    207215            if payload:
     
    209217            return db.query(q)
    210218
    211     def __call__(self, close=False):
     219    def __call__(self):
    212220        """Invoke the notification handler.
    213221
    214         The handler is a loop that actually LISTENs for two NOTIFY messages:
    215 
    216         <event> and stop_<event>.
    217 
    218         When either of these NOTIFY messages are received, its associated
    219         'pid' and 'event' are inserted into <arg_dict>, and the callback is
    220         invoked with <arg_dict>. If the NOTIFY message is stop_<event>, the
    221         handler UNLISTENs both <event> and stop_<event> and exits.
     222        The handler is a loop that listens for notifications on the event
     223        and stop event channels.  When either of these notifications are
     224        received, its associated 'pid', 'event' and 'extra' (the payload
     225        passed with the notification) are inserted into its arg_dict
     226        dictionary and the callback is invoked with this dictionary as
     227        a single argument.  When the handler receives a stop event, it
     228        stops listening to both events and return.
     229
     230        In the special case that the timeout of the handler has been set
     231        to zero, the handler will poll all events synchronously and return.
     232        If will keep listening until it receives a stop event.
    222233
    223234        Note: If you run this loop in another thread, don't use the same
    224235        database connection for database operations in the main thread.
    225 
    226236        """
    227237        self.listen()
    228         _ilist = [self.db.fileno()]
    229 
     238        poll = self.timeout == 0
     239        if not poll:
     240            rlist = [self.db.fileno()]
    230241        while self.listening:
    231             ilist, _olist, _elist = select.select(_ilist, [], [], self.timeout)
    232             if ilist:
     242            if poll or select.select(rlist, [], [], self.timeout)[0]:
    233243                while self.listening:
    234244                    notice = self.db.getnotify()
     
    239249                        self.unlisten()
    240250                        raise _db_error(
    241                             'listening for "%s" and "%s", but notified of "%s"'
     251                            'Listening for "%s" and "%s", but notified of "%s"'
    242252                            % (self.event, self.stop_event, event))
    243253                    if event == self.stop_event:
    244254                        self.unlisten()
    245                     self.arg_dict['pid'] = pid
    246                     self.arg_dict['event'] = event
    247                     self.arg_dict['extra'] = extra
     255                    self.arg_dict.update(pid=pid, event=event, extra=extra)
    248256                    self.callback(self.arg_dict)
     257                if poll:
     258                    break
    249259            else:   # we timed out
    250260                self.unlisten()
     
    11561166        return self.query(q)
    11571167
    1158     def notification_handler(self, event, callback, arg_dict={}, timeout=None):
     1168    def notification_handler(self,
     1169            event, callback, arg_dict=None, timeout=None, stop_event=None):
    11591170        """Get notification handler that will run the given callback."""
    1160         return NotificationHandler(self.db, event, callback, arg_dict, timeout)
     1171        return NotificationHandler(self,
     1172            event, callback, arg_dict, timeout, stop_event)
    11611173
    11621174
  • branches/4.x/tests/test_classic_dbwrapper.py

    r748 r762  
    225225        self.assertRaises(pg.InternalError, self.db.close)
    226226        self.assertRaises(pg.InternalError, self.db.query, 'select 1')
     227
     228    def testMethodReset(self):
     229        con = self.db.db
     230        self.db.reset()
     231        self.assertIs(self.db.db, con)
     232        self.db.query("select 1+1")
     233        self.db.close()
     234        self.assertRaises(pg.InternalError, self.db.reset)
     235
     236    def testMethodReopen(self):
     237        con = self.db.db
     238        self.db.reopen()
     239        self.assertIsNot(self.db.db, con)
     240        con = self.db.db
     241        self.db.query("select 1+1")
     242        self.db.close()
     243        self.db.reopen()
     244        self.assertIsNot(self.db.db, con)
     245        self.db.query("select 1+1")
     246        self.db.close()
    227247
    228248    def testExistingConnection(self):
     
    14341454        query('drop table bytea_test')
    14351455
     1456    def testNotificationHandler(self):
     1457        # the notification handler itself is tested separately
     1458        f = self.db.notification_handler
     1459        callback = lambda arg_dict: None
     1460        handler = f('test', callback)
     1461        self.assertIsInstance(handler, pg.NotificationHandler)
     1462        self.assertIs(handler.db, self.db)
     1463        self.assertEqual(handler.event, 'test')
     1464        self.assertEqual(handler.stop_event, 'stop_test')
     1465        self.assertIs(handler.callback, callback)
     1466        self.assertIsInstance(handler.arg_dict, dict)
     1467        self.assertEqual(handler.arg_dict, {})
     1468        self.assertIsNone(handler.timeout)
     1469        self.assertFalse(handler.listening)
     1470        handler.close()
     1471        self.assertIsNone(handler.db)
     1472        self.db.reopen()
     1473        self.assertIsNone(handler.db)
     1474        handler = f('test2', callback, timeout=2)
     1475        self.assertIsInstance(handler, pg.NotificationHandler)
     1476        self.assertIs(handler.db, self.db)
     1477        self.assertEqual(handler.event, 'test2')
     1478        self.assertEqual(handler.stop_event, 'stop_test2')
     1479        self.assertIs(handler.callback, callback)
     1480        self.assertIsInstance(handler.arg_dict, dict)
     1481        self.assertEqual(handler.arg_dict, {})
     1482        self.assertEqual(handler.timeout, 2)
     1483        self.assertFalse(handler.listening)
     1484        handler.close()
     1485        self.assertIsNone(handler.db)
     1486        self.db.reopen()
     1487        self.assertIsNone(handler.db)
     1488        arg_dict = {'testing': 3}
     1489        handler = f('test3', callback, arg_dict=arg_dict)
     1490        self.assertIsInstance(handler, pg.NotificationHandler)
     1491        self.assertIs(handler.db, self.db)
     1492        self.assertEqual(handler.event, 'test3')
     1493        self.assertEqual(handler.stop_event, 'stop_test3')
     1494        self.assertIs(handler.callback, callback)
     1495        self.assertIs(handler.arg_dict, arg_dict)
     1496        self.assertEqual(arg_dict['testing'], 3)
     1497        self.assertIsNone(handler.timeout)
     1498        self.assertFalse(handler.listening)
     1499        handler.close()
     1500        self.assertIsNone(handler.db)
     1501        self.db.reopen()
     1502        self.assertIsNone(handler.db)
     1503        handler = f('test4', callback, stop_event='stop4')
     1504        self.assertIsInstance(handler, pg.NotificationHandler)
     1505        self.assertIs(handler.db, self.db)
     1506        self.assertEqual(handler.event, 'test4')
     1507        self.assertEqual(handler.stop_event, 'stop4')
     1508        self.assertIs(handler.callback, callback)
     1509        self.assertIsInstance(handler.arg_dict, dict)
     1510        self.assertEqual(handler.arg_dict, {})
     1511        self.assertIsNone(handler.timeout)
     1512        self.assertFalse(handler.listening)
     1513        handler.close()
     1514        self.assertIsNone(handler.db)
     1515        self.db.reopen()
     1516        self.assertIsNone(handler.db)
     1517        arg_dict = {'testing': 5}
     1518        handler = f('test5', callback, arg_dict, 1.5, 'stop5')
     1519        self.assertIsInstance(handler, pg.NotificationHandler)
     1520        self.assertIs(handler.db, self.db)
     1521        self.assertEqual(handler.event, 'test5')
     1522        self.assertEqual(handler.stop_event, 'stop5')
     1523        self.assertIs(handler.callback, callback)
     1524        self.assertIs(handler.arg_dict, arg_dict)
     1525        self.assertEqual(arg_dict['testing'], 5)
     1526        self.assertEqual(handler.timeout, 1.5)
     1527        self.assertFalse(handler.listening)
     1528        handler.close()
     1529        self.assertIsNone(handler.db)
     1530        self.db.reopen()
     1531        self.assertIsNone(handler.db)
     1532
    14361533    def testDebugWithCallable(self):
    14371534        if debug:
     
    14561553        db = DB()
    14571554        query = db.query
    1458         query("set client_min_messages=warning")
    14591555        for num_schema in range(5):
    14601556            if num_schema:
     
    14811577        db = DB()
    14821578        query = db.query
    1483         query("set client_min_messages=warning")
    14841579        for num_schema in range(5):
    14851580            if num_schema:
     
    14941589    def setUp(self):
    14951590        self.db = DB()
    1496         self.db.query("set client_min_messages=warning")
    14971591
    14981592    def tearDown(self):
Note: See TracChangeset for help on using the changeset viewer.