Commit d2a26bcc authored by Jim Fulton's avatar Jim Fulton

- Databases have a new method, transaction, that can be used with the

  Python (2.5 and later) with statement::

db = ZODB.DB(...)
     with db.transaction() as conn:
          # ... do stuff with conn

This uses a private transaction manager for the connection.
  If control exists the block without an error, the transaction is
  committed, otherwise, it is aborted.

- Connections now have a public ``opened`` attribute that is true when
  the connection is open, and false otherwise.  When true, it is the
  seconds since the epoch (time.time()) when the connection was
  opened. This is a renaming of the previous ``_opened`` private
  variable.
parent 27c2fc38
...@@ -14,6 +14,17 @@ New Features ...@@ -14,6 +14,17 @@ New Features
New Features New Features
------------ ------------
- Databases have a new method, transaction, that can be used with the
Python (2.5 and later) with statement::
db = ZODB.DB(...)
with db.transaction() as conn:
# ... do stuff with conn
This uses a private transaction manager for the connection.
If control exists the block without an error, the transaction is
committed, otherwise, it is aborted.
- Convenience methods ZODB.DB.open and ZEO.DB.open provide a - Convenience methods ZODB.DB.open and ZEO.DB.open provide a
convenient way to open a connection to a database. They open a convenient way to open a connection to a database. They open a
database and return a connection to it. When the connection is database and return a connection to it. When the connection is
...@@ -48,6 +59,13 @@ New Features ...@@ -48,6 +59,13 @@ New Features
There are corresponding methods to read and set the new configuration There are corresponding methods to read and set the new configuration
parameters. parameters.
- Connections now have a public ``opened`` attribute that is true when
the connection is open, and false otherwise. When true, it is the
seconds since the epoch (time.time()) when the connection was
opened. This is a renaming of the previous ``_opened`` private
variable.
Bugs Fixed Bugs Fixed
---------- ----------
......
...@@ -101,7 +101,7 @@ class Connection(ExportImport, object): ...@@ -101,7 +101,7 @@ class Connection(ExportImport, object):
# Do we need to join a txn manager? # Do we need to join a txn manager?
self._needs_to_join = True self._needs_to_join = True
self.transaction_manager = None self.transaction_manager = None
self._opened = None # time.time() when DB.open() opened us self.opened = None # time.time() when DB.open() opened us
self._reset_counter = global_reset_counter self._reset_counter = global_reset_counter
self._load_count = 0 # Number of objects unghosted self._load_count = 0 # Number of objects unghosted
...@@ -196,7 +196,7 @@ class Connection(ExportImport, object): ...@@ -196,7 +196,7 @@ class Connection(ExportImport, object):
def add(self, obj): def add(self, obj):
"""Add a new object 'obj' to the database and assign it an oid.""" """Add a new object 'obj' to the database and assign it an oid."""
if self._opened is None: if self.opened is None:
raise ConnectionStateError("The database connection is closed") raise ConnectionStateError("The database connection is closed")
marker = object() marker = object()
...@@ -220,7 +220,7 @@ class Connection(ExportImport, object): ...@@ -220,7 +220,7 @@ class Connection(ExportImport, object):
def get(self, oid): def get(self, oid):
"""Return the persistent object with oid 'oid'.""" """Return the persistent object with oid 'oid'."""
if self._opened is None: if self.opened is None:
raise ConnectionStateError("The database connection is closed") raise ConnectionStateError("The database connection is closed")
obj = self._cache.get(oid, None) obj = self._cache.get(oid, None)
...@@ -292,7 +292,7 @@ class Connection(ExportImport, object): ...@@ -292,7 +292,7 @@ class Connection(ExportImport, object):
self._debug_info = () self._debug_info = ()
if self._opened: if self.opened:
self.transaction_manager.unregisterSynch(self) self.transaction_manager.unregisterSynch(self)
if primary: if primary:
...@@ -301,15 +301,15 @@ class Connection(ExportImport, object): ...@@ -301,15 +301,15 @@ class Connection(ExportImport, object):
connection.close(False) connection.close(False)
# Return the connection to the pool. # Return the connection to the pool.
if self._opened is not None: if self.opened is not None:
self._db._returnToPool(self) self._db._returnToPool(self)
# _returnToPool() set self._opened to None. # _returnToPool() set self.opened to None.
# However, we can't assert that here, because self may # However, we can't assert that here, because self may
# have been reused (by another thread) by the time we # have been reused (by another thread) by the time we
# get back here. # get back here.
else: else:
self._opened = None self.opened = None
def db(self): def db(self):
"""Returns a handle to the database this connection belongs to.""" """Returns a handle to the database this connection belongs to."""
...@@ -317,7 +317,7 @@ class Connection(ExportImport, object): ...@@ -317,7 +317,7 @@ class Connection(ExportImport, object):
def isReadOnly(self): def isReadOnly(self):
"""Returns True if this connection is read only.""" """Returns True if this connection is read only."""
if self._opened is None: if self.opened is None:
raise ConnectionStateError("The database connection is closed") raise ConnectionStateError("The database connection is closed")
return self.before is not None or self._storage.isReadOnly() return self.before is not None or self._storage.isReadOnly()
...@@ -798,7 +798,7 @@ class Connection(ExportImport, object): ...@@ -798,7 +798,7 @@ class Connection(ExportImport, object):
the database.""" the database."""
oid = obj._p_oid oid = obj._p_oid
if self._opened is None: if self.opened is None:
msg = ("Shouldn't load state for %s " msg = ("Shouldn't load state for %s "
"when the connection is closed" % oid_repr(oid)) "when the connection is closed" % oid_repr(oid))
self._log.error(msg) self._log.error(msg)
...@@ -1010,7 +1010,7 @@ class Connection(ExportImport, object): ...@@ -1010,7 +1010,7 @@ class Connection(ExportImport, object):
register for afterCompletion() calls. register for afterCompletion() calls.
""" """
self._opened = time.time() self.opened = time.time()
if transaction_manager is None: if transaction_manager is None:
transaction_manager = transaction.manager transaction_manager = transaction.manager
......
...@@ -515,7 +515,7 @@ class DB(object): ...@@ -515,7 +515,7 @@ class DB(object):
self._a() self._a()
try: try:
assert connection._db is self assert connection._db is self
connection._opened = None connection.opened = None
am = self._activity_monitor am = self._activity_monitor
if am is not None: if am is not None:
...@@ -783,7 +783,7 @@ class DB(object): ...@@ -783,7 +783,7 @@ class DB(object):
def get_info(c): def get_info(c):
# `result`, `time` and `before` are lexically inherited. # `result`, `time` and `before` are lexically inherited.
o = c._opened o = c.opened
d = c.getDebugInfo() d = c.getDebugInfo()
if d: if d:
if len(d) == 1: if len(d) == 1:
...@@ -920,6 +920,28 @@ class DB(object): ...@@ -920,6 +920,28 @@ class DB(object):
txn = transaction.get() txn = transaction.get()
txn.register(TransactionalUndo(self, id)) txn.register(TransactionalUndo(self, id))
def transaction(self):
return ContextManager(self)
class ContextManager:
"""PEP 343 context manager
"""
def __init__(self, db):
self.db = db
def __enter__(self):
self.tm = transaction.TransactionManager()
self.conn = self.db.open(self.tm)
return self.conn
def __exit__(self, t, v, tb):
if t is None:
self.tm.commit()
else:
self.tm.abort()
self.conn.close()
resource_counter_lock = threading.Lock() resource_counter_lock = threading.Lock()
resource_counter = 0 resource_counter = 0
......
...@@ -16,6 +16,7 @@ from ZODB.tests.MinPO import MinPO ...@@ -16,6 +16,7 @@ from ZODB.tests.MinPO import MinPO
from zope.testing import doctest from zope.testing import doctest
import datetime import datetime
import os import os
import sys
import time import time
import transaction import transaction
import unittest import unittest
...@@ -181,6 +182,62 @@ def open_convenience(): ...@@ -181,6 +182,62 @@ def open_convenience():
>>> db.close() >>> db.close()
""" """
if sys.version_info >= (2, 6):
def db_with_transaction():
"""Using databases with with
The transaction method returns a context manager that when entered
starts a transaction with a private transaction manager. To
illustrate this, we start a trasnaction using a regular connection
and see that it isn't automatically committed or aborted as we use
the transaction context manager.
>>> db = ZODB.DB('data.fs')
>>> conn = db.open()
>>> conn.root()['x'] = conn.root().__class__()
>>> transaction.commit()
>>> conn.root()['x']['x'] = 1
>>> with db.transaction() as conn2:
... conn2.root()['y'] = 1
>>> conn2.opened
Now, we'll open a 3rd connection a verify that
>>> conn3 = db.open()
>>> conn3.root()['x']
{}
>>> conn3.root()['y']
1
>>> conn3.close()
Let's try again, but this time, we'll have an exception:
>>> with db.transaction() as conn2:
... conn2.root()['y'] = 2
... XXX
Traceback (most recent call last):
...
NameError: name 'XXX' is not defined
>>> conn2.opened
>>> conn3 = db.open()
>>> conn3.root()['x']
{}
>>> conn3.root()['y']
1
>>> conn3.close()
>>> transaction.commit()
>>> conn3 = db.open()
>>> conn3.root()['x']
{'x': 1}
"""
def test_suite(): def test_suite():
s = unittest.makeSuite(DBTests) s = unittest.makeSuite(DBTests)
s.addTest(doctest.DocTestSuite( s.addTest(doctest.DocTestSuite(
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment