From 1ebbdf77720878d0ac570cbad8b5bc5100993714 Mon Sep 17 00:00:00 2001 From: Jim Fulton Date: Wed, 16 Nov 2016 15:41:56 -0500 Subject: [PATCH] Document history, undoLog and undoInfo in IDatabase and have them return text for user_name and description --- src/ZODB/DB.py | 15 ++++- src/ZODB/interfaces.py | 117 ++++++++++++++++++++++++++++++++++++--- src/ZODB/tests/testDB.py | 34 ++++++++++++ 3 files changed, 155 insertions(+), 11 deletions(-) diff --git a/src/ZODB/DB.py b/src/ZODB/DB.py index 69d0c2408..7bb4fe072 100644 --- a/src/ZODB/DB.py +++ b/src/ZODB/DB.py @@ -901,7 +901,7 @@ def history(self, oid, size=1): See :meth:`ZODB.interfaces.IStorage.history`. """ - return self.storage.history(oid, size) + return text_transaction_info(self.storage.history(oid, size)) def supportsUndo(self): """Return whether the database supports undo. @@ -920,7 +920,7 @@ def undoLog(self, *args, **kw): if not self.supportsUndo(): return () - return self.storage.undoLog(*args, **kw) + return text_transaction_info(self.storage.undoLog(*args, **kw)) def undoInfo(self, *args, **kw): """Return a sequence of descriptions for transactions. @@ -929,7 +929,7 @@ def undoInfo(self, *args, **kw): """ if not self.supportsUndo(): return () - return self.storage.undoInfo(*args, **kw) + return text_transaction_info(self.storage.undoInfo(*args, **kw)) def undoMultiple(self, ids, txn=None): """Undo multiple transactions identified by ids. @@ -1064,3 +1064,12 @@ def connection(*args, **kw): managing a separate database object. """ return DB(*args, **kw).open_then_close_db_when_connection_closes() + +transaction_meta_data_text_variables = 'user_name', 'description' +def text_transaction_info(info): + for d in info: + for name in transaction_meta_data_text_variables: + if name in d: + d[name] = d[name].decode('utf-8') + + return info diff --git a/src/ZODB/interfaces.py b/src/ZODB/interfaces.py index 354723b68..fac54be37 100644 --- a/src/ZODB/interfaces.py +++ b/src/ZODB/interfaces.py @@ -383,7 +383,41 @@ def open(transaction_manager=None, serial=''): include useful objects. """ - # TODO: Should this method be moved into some subinterface? + def history(oid, size=1): + """Return a sequence of history information dictionaries. + + Up to size objects (including no objects) may be returned. + + The information provides a log of the changes made to the + object. Data are reported in reverse chronological order. + + Each dictionary has the following keys: + + time + UTC seconds since the epoch (as in time.time) that the + object revision was committed. + + tid + The transaction identifier of the transaction that + committed the version. + + user_name + The text (unicode) user identifier, if any (or an empty + string) of the user on whos behalf the revision was + committed. + + description + The text (unicode) transaction description for the + transaction that committed the revision. + + size + The size of the revision data record. + + If the transaction had extension items, then these items are + also included if they don't conflict with the keys above. + """ + + def pack(t=None, days=0): """Pack the storage, deleting unused object revisions. @@ -400,7 +434,76 @@ def pack(t=None, days=0): time if t is not specified. """ - # TODO: Should this method be moved into some subinterface? + def undoLog(first, last, filter=None): + """Return a sequence of descriptions for undoable transactions. + + Application code should call undoLog() on a DB instance instead of on + the storage directly. + + A transaction description is a mapping with at least these keys: + + "time": The time, as float seconds since the epoch, when + the transaction committed. + "user_name": The text value of the `.user` attribute on that + transaction. + "description": The text value of the `.description` attribute on + that transaction. + "id`" A bytes uniquely identifying the transaction to the + storage. If it's desired to undo this transaction, + this is the `transaction_id` to pass to `undo()`. + + In addition, if any name+value pairs were added to the transaction + by `setExtendedInfo()`, those may be added to the transaction + description mapping too (for example, FileStorage's `undoLog()` does + this). + + `filter` is a callable, taking one argument. A transaction + description mapping is passed to `filter` for each potentially + undoable transaction. The sequence returned by `undoLog()` excludes + descriptions for which `filter` returns a false value. By default, + `filter` always returns a true value. + + ZEO note: Arbitrary callables cannot be passed from a ZEO client + to a ZEO server, and a ZEO client's implementation of `undoLog()` + ignores any `filter` argument that may be passed. ZEO clients + should use the related `undoInfo()` method instead (if they want + to do filtering). + + Now picture a list containing descriptions of all undoable + transactions that pass the filter, most recent transaction first (at + index 0). The `first` and `last` arguments specify the slice of this + (conceptual) list to be returned: + + `first`: This is the index of the first transaction description + in the slice. It must be >= 0. + `last`: If >= 0, first:last acts like a Python slice, selecting + the descriptions at indices `first`, first+1, ..., up to + but not including index `last`. At most last-first + descriptions are in the slice, and `last` should be at + least as large as `first` in this case. If `last` is + less than 0, then abs(last) is taken to be the maximum + number of descriptions in the slice (which still begins + at index `first`). When `last` < 0, the same effect + could be gotten by passing the positive first-last for + `last` instead. + """ + + def undoInfo(first=0, last=-20, specification=None): + """Return a sequence of descriptions for undoable transactions. + + This is like `undoLog()`, except for the `specification` argument. + If given, `specification` is a dictionary, and `undoInfo()` + synthesizes a `filter` function `f` for `undoLog()` such that + `f(desc)` returns true for a transaction description mapping + `desc` if and only if `desc` maps each key in `specification` to + the same value `specification` maps that key to. In other words, + only extensions (or supersets) of `specification` match. + + ZEO note: `undoInfo()` passes the `specification` argument from a + ZEO client to its ZEO server (while a ZEO client ignores any `filter` + argument passed to `undoLog()`). + """ + def undo(id, txn=None): """Undo a transaction identified by id. @@ -940,10 +1043,10 @@ def undoLog(first, last, filter=None): "time": The time, as float seconds since the epoch, when the transaction committed. - "user_name": The value of the `.user` attribute on that - transaction, **bytes**. - "description": The value of the `.description` attribute on - that transaction, **bytes**. + "user_name": The bytes value of the `.user` attribute on that + transaction. + "description": The bytes value of the `.description` attribute on + that transaction. "id`" A bytes uniquely identifying the transaction to the storage. If it's desired to undo this transaction, this is the `transaction_id` to pass to `undo()`. @@ -983,7 +1086,6 @@ def undoLog(first, last, filter=None): could be gotten by passing the positive first-last for `last` instead. """ - # DB pass through def undoInfo(first=0, last=-20, specification=None): """Return a sequence of descriptions for undoable transactions. @@ -1000,7 +1102,6 @@ def undoInfo(first=0, last=-20, specification=None): ZEO client to its ZEO server (while a ZEO client ignores any `filter` argument passed to `undoLog()`). """ - # DB pass-through class IMVCCStorage(IStorage): diff --git a/src/ZODB/tests/testDB.py b/src/ZODB/tests/testDB.py index 262ab6bfd..7380ffee0 100644 --- a/src/ZODB/tests/testDB.py +++ b/src/ZODB/tests/testDB.py @@ -11,6 +11,7 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## +from six import PY2 from ZODB.tests.MinPO import MinPO import doctest @@ -75,6 +76,39 @@ def test_references(self): import ZODB.serialize self.assertTrue(self.db.references is ZODB.serialize.referencesf) + def test_history_and_undo_meta_data_text_handlinf(self): + db = self.db + conn = db.open() + for i in range(3): + with conn.transaction_manager as t: + t.note(u'work %s' % i) + t.setUser(u'user%s' % i) + conn.root()[i] = 42 + + conn.close() + + from ZODB.utils import z64 + + def check(info, text): + for i, h in enumerate(reversed(info)): + for (name, expect) in (('description', 'work %s'), + ('user_name', '/ user%s')): + expect = expect % i + if not text: + expect = expect.encode('ascii') + self.assertEqual(h[name], expect) + + if PY2: + expect = unicode if text else str + for name in 'description', 'user_name': + self.assertTrue(isinstance(h[name], expect)) + + check(db.storage.history(z64, 3), False) + check(db.storage.undoLog(0, 3) , False) + check(db.storage.undoInfo(0, 3) , False) + check(db.history(z64, 3), True) + check(db.undoLog(0, 3) , True) + check(db.undoInfo(0, 3) , True) def test_invalidateCache(): """The invalidateCache method invalidates a connection caches for all of