Skip to content

Commit

Permalink
Document history, undoLog and undoInfo in IDatabase and have them ret…
Browse files Browse the repository at this point in the history
…urn text

for user_name and description
  • Loading branch information
Jim Fulton committed Nov 16, 2016
1 parent 539e5f8 commit 1ebbdf7
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 11 deletions.
15 changes: 12 additions & 3 deletions src/ZODB/DB.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
117 changes: 109 additions & 8 deletions src/ZODB/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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()`.
Expand Down Expand Up @@ -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.
Expand All @@ -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):
Expand Down
34 changes: 34 additions & 0 deletions src/ZODB/tests/testDB.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
from six import PY2

from ZODB.tests.MinPO import MinPO
import doctest
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 1ebbdf7

Please sign in to comment.