[Checkins] SVN: zope.transaction/trunk/ First cut; one test still fails.

Chris McDonough chrism at plope.com
Thu Nov 8 19:58:55 EST 2007


Log message for revision 81628:
  First cut; one test still fails.
  

Changed:
  _U  zope.transaction/trunk/
  A   zope.transaction/trunk/CHANGES.txt
  A   zope.transaction/trunk/COPYRIGHT.txt
  A   zope.transaction/trunk/LICENSE.txt
  A   zope.transaction/trunk/README.txt
  A   zope.transaction/trunk/ez_setup.py
  A   zope.transaction/trunk/setup.py
  A   zope.transaction/trunk/zope/__init__.py
  _U  zope.transaction/trunk/zope/transaction/
  U   zope.transaction/trunk/zope/transaction/DEPENDENCIES.cfg
  A   zope.transaction/trunk/zope/transaction/TimeStamp.c
  U   zope.transaction/trunk/zope/transaction/__init__.py
  U   zope.transaction/trunk/zope/transaction/_manager.py
  U   zope.transaction/trunk/zope/transaction/_transaction.py
  U   zope.transaction/trunk/zope/transaction/interfaces.py
  D   zope.transaction/trunk/zope/transaction/savepoint.txt
  D   zope.transaction/trunk/zope/transaction/tests/abstestIDataManager.py
  U   zope.transaction/trunk/zope/transaction/tests/doom.txt
  A   zope.transaction/trunk/zope/transaction/tests/sampledm.py
  A   zope.transaction/trunk/zope/transaction/tests/savepoint.txt
  U   zope.transaction/trunk/zope/transaction/tests/savepointsample.py
  U   zope.transaction/trunk/zope/transaction/tests/test_SampleDataManager.py
  U   zope.transaction/trunk/zope/transaction/tests/test_SampleResourceManager.py
  U   zope.transaction/trunk/zope/transaction/tests/test_register_compat.py
  U   zope.transaction/trunk/zope/transaction/tests/test_savepoint.py
  A   zope.transaction/trunk/zope/transaction/tests/test_timestamp.py
  U   zope.transaction/trunk/zope/transaction/tests/test_transaction.py
  A   zope.transaction/trunk/zope/transaction/tests/test_weakset.py
  A   zope.transaction/trunk/zope/transaction/tests/warnhook.py
  A   zope.transaction/trunk/zope/transaction/weakset.py

-=-

Property changes on: zope.transaction/trunk
___________________________________________________________________
Name: svn:ignore
   + *.egg
*.egg-info
build
dist



Added: zope.transaction/trunk/CHANGES.txt
===================================================================
--- zope.transaction/trunk/CHANGES.txt	                        (rev 0)
+++ zope.transaction/trunk/CHANGES.txt	2007-11-09 00:58:54 UTC (rev 81628)
@@ -0,0 +1,12 @@
+1.0 
+
+  Initial release.
+
+  Remove (deprecated) support for beforeCommitHook alias to
+  addBeforeCommitHook.
+
+  Add weakset tests.
+
+  Add TimeStamp tests.
+
+

Added: zope.transaction/trunk/COPYRIGHT.txt
===================================================================
--- zope.transaction/trunk/COPYRIGHT.txt	                        (rev 0)
+++ zope.transaction/trunk/COPYRIGHT.txt	2007-11-09 00:58:54 UTC (rev 81628)
@@ -0,0 +1,2 @@
+Copyright (c) 2007 Zope Corporation and Contributors.
+All Rights Reserved.

Added: zope.transaction/trunk/LICENSE.txt
===================================================================
--- zope.transaction/trunk/LICENSE.txt	                        (rev 0)
+++ zope.transaction/trunk/LICENSE.txt	2007-11-09 00:58:54 UTC (rev 81628)
@@ -0,0 +1,54 @@
+Zope Public License (ZPL) Version 2.1
+-------------------------------------
+
+A copyright notice accompanies this license document that
+identifies the copyright holders.
+
+This license has been certified as open source. It has also
+been designated as GPL compatible by the Free Software
+Foundation (FSF).
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the
+following conditions are met:
+
+1. Redistributions in source code must retain the
+   accompanying copyright notice, this list of conditions,
+   and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the accompanying
+   copyright notice, this list of conditions, and the
+   following disclaimer in the documentation and/or other
+   materials provided with the distribution.
+
+3. Names of the copyright holders must not be used to
+   endorse or promote products derived from this software
+   without prior written permission from the copyright
+   holders.
+
+4. The right to distribute this software or to use it for
+   any purpose does not give you the right to use
+   Servicemarks (sm) or Trademarks (tm) of the copyright
+   holders. Use of them is covered by separate agreement
+   with the copyright holders.
+
+5. If any files are modified, you must cause the modified
+   files to carry prominent notices stating that you changed
+   the files and the date of any change.
+
+Disclaimer
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS''
+  AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+  NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+  AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN
+  NO EVENT SHALL THE COPYRIGHT HOLDERS BE
+  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+  OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+  DAMAGE.

Added: zope.transaction/trunk/README.txt
===================================================================
--- zope.transaction/trunk/README.txt	                        (rev 0)
+++ zope.transaction/trunk/README.txt	2007-11-09 00:58:54 UTC (rev 81628)
@@ -0,0 +1,12 @@
+============
+Transactions
+============
+
+This package contains a generic transaction implementation for Python. It is
+mainly used by the ZODB, though.
+
+Note that the data manager API, ``transaction.interfaces.IDataManager``,
+is syntactically simple, but semantically complex.  The semantics
+were not easy to express in the interface. This could probably use
+more work.  The semantics are presented in detail through examples of
+a sample data manager in ``transaction.tests.test_SampleDataManager``.

Added: zope.transaction/trunk/ez_setup.py
===================================================================
--- zope.transaction/trunk/ez_setup.py	                        (rev 0)
+++ zope.transaction/trunk/ez_setup.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -0,0 +1,234 @@
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+    from ez_setup import use_setuptools
+    use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c7"
+DEFAULT_URL     = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+    'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+    'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+    'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+    'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+    'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+    'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+    'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+    'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+    'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+    'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+    'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+    'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+    'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+    'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+    'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+    'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
+    'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
+    'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
+    'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
+    'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
+    'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
+    'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
+    'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
+    'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
+    'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
+    'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
+    'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
+}
+
+import sys, os
+
+def _validate_md5(egg_name, data):
+    if egg_name in md5_data:
+        from md5 import md5
+        digest = md5(data).hexdigest()
+        if digest != md5_data[egg_name]:
+            print >>sys.stderr, (
+                "md5 validation of %s failed!  (Possible download problem?)"
+                % egg_name
+            )
+            sys.exit(2)
+    return data
+
+
+def use_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    download_delay=15
+):
+    """Automatically find/download setuptools and make it available on sys.path
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end with
+    a '/').  `to_dir` is the directory where setuptools will be downloaded, if
+    it is not already available.  If `download_delay` is specified, it should
+    be the number of seconds that will be paused before initiating a download,
+    should one be required.  If an older version of setuptools is installed,
+    this routine will print a message to ``sys.stderr`` and raise SystemExit in
+    an attempt to abort the calling script.
+    """
+    try:
+        import setuptools
+        if setuptools.__version__ == '0.0.1':
+            print >>sys.stderr, (
+            "You have an obsolete version of setuptools installed.  Please\n"
+            "remove it from your system entirely before rerunning this script."
+            )
+            sys.exit(2)
+    except ImportError:
+        egg = download_setuptools(version, download_base, to_dir, download_delay)
+        sys.path.insert(0, egg)
+        import setuptools; setuptools.bootstrap_install_from = egg
+
+    import pkg_resources
+    try:
+        pkg_resources.require("setuptools>="+version)
+
+    except pkg_resources.VersionConflict, e:
+        # XXX could we install in a subprocess here?
+        print >>sys.stderr, (
+            "The required version of setuptools (>=%s) is not available, and\n"
+            "can't be installed while this script is running. Please install\n"
+            " a more recent version first.\n\n(Currently using %r)"
+        ) % (version, e.args[0])
+        sys.exit(2)
+
+def download_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    delay = 15
+):
+    """Download setuptools from a specified location and return its filename
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end
+    with a '/'). `to_dir` is the directory where the egg will be downloaded.
+    `delay` is the number of seconds to pause before an actual download attempt.
+    """
+    import urllib2, shutil
+    egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+    url = download_base + egg_name
+    saveto = os.path.join(to_dir, egg_name)
+    src = dst = None
+    if not os.path.exists(saveto):  # Avoid repeated downloads
+        try:
+            from distutils import log
+            if delay:
+                log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help).  I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+   %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+                    version, download_base, delay, url
+                ); from time import sleep; sleep(delay)
+            log.warn("Downloading %s", url)
+            src = urllib2.urlopen(url)
+            # Read/write all in one block, so we don't create a corrupt file
+            # if the download is interrupted.
+            data = _validate_md5(egg_name, src.read())
+            dst = open(saveto,"wb"); dst.write(data)
+        finally:
+            if src: src.close()
+            if dst: dst.close()
+    return os.path.realpath(saveto)
+
+def main(argv, version=DEFAULT_VERSION):
+    """Install or upgrade setuptools and EasyInstall"""
+
+    try:
+        import setuptools
+    except ImportError:
+        egg = None
+        try:
+            egg = download_setuptools(version, delay=0)
+            sys.path.insert(0,egg)
+            from setuptools.command.easy_install import main
+            return main(list(argv)+[egg])   # we're done here
+        finally:
+            if egg and os.path.exists(egg):
+                os.unlink(egg)
+    else:
+        if setuptools.__version__ == '0.0.1':
+            # tell the user to uninstall obsolete version
+            use_setuptools(version)
+
+    req = "setuptools>="+version
+    import pkg_resources
+    try:
+        pkg_resources.require(req)
+    except pkg_resources.VersionConflict:
+        try:
+            from setuptools.command.easy_install import main
+        except ImportError:
+            from easy_install import main
+        main(list(argv)+[download_setuptools(delay=0)])
+        sys.exit(0) # try to force an exit
+    else:
+        if argv:
+            from setuptools.command.easy_install import main
+            main(argv)
+        else:
+            print "Setuptools version",version,"or greater has been installed."
+            print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+
+
+def update_md5(filenames):
+    """Update our built-in md5 registry"""
+
+    import re
+    from md5 import md5
+
+    for name in filenames:
+        base = os.path.basename(name)
+        f = open(name,'rb')
+        md5_data[base] = md5(f.read()).hexdigest()
+        f.close()
+
+    data = ["    %r: %r,\n" % it for it in md5_data.items()]
+    data.sort()
+    repl = "".join(data)
+
+    import inspect
+    srcfile = inspect.getsourcefile(sys.modules[__name__])
+    f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+    match = re.search("\nmd5_data = {\n([^}]+)}", src)
+    if not match:
+        print >>sys.stderr, "Internal error!"
+        sys.exit(2)
+
+    src = src[:match.start(1)] + repl + src[match.end(1):]
+    f = open(srcfile,'w')
+    f.write(src)
+    f.close()
+
+
+if __name__=='__main__':
+    if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+        update_md5(sys.argv[2:])
+    else:
+        main(sys.argv[1:])
+
+
+
+
+


Property changes on: zope.transaction/trunk/ez_setup.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zope.transaction/trunk/setup.py
===================================================================
--- zope.transaction/trunk/setup.py	                        (rev 0)
+++ zope.transaction/trunk/setup.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -0,0 +1,62 @@
+##############################################################################
+#
+# Copyright (c) 2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+__version__ = '1.0dev'
+
+import os
+
+from ez_setup import use_setuptools
+use_setuptools()
+
+from setuptools import setup, find_packages, Extension
+
+here = os.path.abspath(os.path.dirname(__file__))
+README = open(os.path.join(here, 'README.txt')).read()
+
+setup(name='zope.transaction',
+      version=__version__,
+      description='Transaction management for Python',
+      long_description=README,
+      classifiers=[
+        "Development Status :: 6 - Mature",
+        "License :: OSI Approved :: Zope Public License"
+        "Programming Language :: Python"
+        "Topic :: Database"
+        "Topic :: Software Development :: Libraries :: Python Modules"
+        "Operating System :: Microsoft :: Windows"
+        "Operating System :: Unix"
+        ],
+      author="Zope Corporation",
+      author_email="zodb-dev at zope.org",
+      url="http://www.zope.org/Products/ZODB",
+      license="ZPL 2.1",
+      platforms=["any"],
+      packages=find_packages(),
+      ext_modules = [
+            Extension('zope.transaction.TimeStamp',
+                      ['zope/transaction/TimeStamp.c']),
+            ],
+      include_package_data=True,
+      zip_safe=False,
+      test_suite="zope.transaction.tests",
+      tests_require = ['zope.interface',
+                       'zope.testing',
+                       'ZODB3'],
+      install_requires=[
+        'zope.interface',
+        ],
+      entry_points = """\
+      """
+      )
+


Property changes on: zope.transaction/trunk/setup.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zope.transaction/trunk/zope/__init__.py
===================================================================
--- zope.transaction/trunk/zope/__init__.py	                        (rev 0)
+++ zope.transaction/trunk/zope/__init__.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -0,0 +1,2 @@
+# namespace package
+__import__('pkg_resources').declare_namespace(__name__)


Property changes on: zope.transaction/trunk/zope/__init__.py
___________________________________________________________________
Name: svn:eol-style
   + native


Property changes on: zope.transaction/trunk/zope/transaction
___________________________________________________________________
Name: svn:ignore
   + *.so


Modified: zope.transaction/trunk/zope/transaction/DEPENDENCIES.cfg
===================================================================
--- zope.transaction/trunk/zope/transaction/DEPENDENCIES.cfg	2007-11-08 23:11:28 UTC (rev 81627)
+++ zope.transaction/trunk/zope/transaction/DEPENDENCIES.cfg	2007-11-09 00:58:54 UTC (rev 81628)
@@ -1,2 +1 @@
-ZODB
 zope.interface

Added: zope.transaction/trunk/zope/transaction/TimeStamp.c
===================================================================
--- zope.transaction/trunk/zope/transaction/TimeStamp.c	                        (rev 0)
+++ zope.transaction/trunk/zope/transaction/TimeStamp.c	2007-11-09 00:58:54 UTC (rev 81628)
@@ -0,0 +1,437 @@
+/*****************************************************************************
+
+  Copyright (c) 2001, 2004 Zope Corporation and Contributors.
+  All Rights Reserved.
+
+  This software is subject to the provisions of the Zope Public License,
+  Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+  THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+  WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+  WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+  FOR A PARTICULAR PURPOSE
+
+ ****************************************************************************/
+
+#include "Python.h"
+#include <time.h>
+
+PyObject *TimeStamp_FromDate(int, int, int, int, int, double);
+PyObject *TimeStamp_FromString(const char *);
+
+static char TimeStampModule_doc[] =
+"A 64-bit TimeStamp used as a ZODB serial number.\n"
+"\n"
+"$Id: TimeStamp.c 41599 2006-02-11 21:33:49Z tseaver $\n";
+
+
+typedef struct {
+    PyObject_HEAD
+    unsigned char data[8];
+} TimeStamp;
+
+/* The first dimension of the arrays below is non-leapyear / leapyear */
+
+static char month_len[2][12]={
+  {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
+  {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
+};
+
+static short joff[2][12] = {
+  {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334},
+  {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335}
+};
+
+static double gmoff=0;
+
+/* TODO:  May be better (faster) to store in a file static. */
+#define SCONV ((double)60) / ((double)(1<<16)) / ((double)(1<<16))
+
+static int
+leap(int year)
+{
+    return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
+}
+
+static int
+days_in_month(int year, int month)
+{
+    return month_len[leap(year)][month];
+}
+
+static double
+TimeStamp_yad(int y)
+{
+    double d, s;
+
+    y -= 1900;
+
+    d = (y - 1) * 365;
+    if (y > 0) {
+        s = 1.0;
+	y -= 1;
+    } else {
+	s = -1.0;
+	y = -y;
+    }
+    return d + s * (y / 4 - y / 100 + (y + 300) / 400);
+}
+
+static double
+TimeStamp_abst(int y, int mo, int d, int m, int s)
+{
+    return (TimeStamp_yad(y) + joff[leap(y)][mo] + d) * 86400 + m * 60 + s;
+}
+
+static int
+TimeStamp_init_gmoff(void)
+{
+    struct tm *t;
+    time_t z=0;
+
+    t = gmtime(&z);
+    if (t == NULL) {
+	PyErr_SetString(PyExc_SystemError, "gmtime failed");
+	return -1;
+    }
+
+    gmoff = TimeStamp_abst(t->tm_year+1900, t->tm_mon, t->tm_mday - 1,
+			   t->tm_hour * 60 + t->tm_min, t->tm_sec);
+
+    return 0;
+}
+
+static void
+TimeStamp_dealloc(TimeStamp *ts)
+{
+    PyObject_Del(ts);
+}
+
+static int
+TimeStamp_compare(TimeStamp *v, TimeStamp *w)
+{
+    int cmp = memcmp(v->data, w->data, 8);
+    if (cmp < 0) return -1;
+    if (cmp > 0) return 1;
+    return 0;
+}
+
+static long
+TimeStamp_hash(TimeStamp *self)
+{
+    register unsigned char *p = (unsigned char *)self->data;
+    register int len = 8;
+    register long x = *p << 7;
+    while (--len >= 0)
+	x = (1000003*x) ^ *p++;
+    x ^= 8;
+    if (x == -1)
+	x = -2;
+    return x;
+}
+
+typedef struct {
+    /* TODO:  reverse-engineer what's in these things and comment them */
+    int y;
+    int m;
+    int d;
+    int mi;
+} TimeStampParts;
+
+static void
+TimeStamp_unpack(TimeStamp *self, TimeStampParts *p)
+{
+    unsigned long v;
+
+    v = (self->data[0] * 16777216 + self->data[1] * 65536
+	 + self->data[2] * 256 + self->data[3]);
+    p->y = v / 535680 + 1900;
+    p->m = (v % 535680) / 44640 + 1;
+    p->d = (v % 44640) / 1440 + 1;
+    p->mi = v % 1440;
+}
+
+static double
+TimeStamp_sec(TimeStamp *self)
+{
+    unsigned int v;
+
+    v = (self->data[4] * 16777216 + self->data[5] * 65536
+	 + self->data[6] * 256 + self->data[7]);
+    return SCONV * v;
+}
+
+static PyObject *
+TimeStamp_year(TimeStamp *self)
+{
+    TimeStampParts p;
+    TimeStamp_unpack(self, &p);
+    return PyInt_FromLong(p.y);
+}
+
+static PyObject *
+TimeStamp_month(TimeStamp *self)
+{
+    TimeStampParts p;
+    TimeStamp_unpack(self, &p);
+    return PyInt_FromLong(p.m);
+}
+
+static PyObject *
+TimeStamp_day(TimeStamp *self)
+{
+    TimeStampParts p;
+    TimeStamp_unpack(self, &p);
+    return PyInt_FromLong(p.d);
+}
+
+static PyObject *
+TimeStamp_hour(TimeStamp *self)
+{
+    TimeStampParts p;
+    TimeStamp_unpack(self, &p);
+    return PyInt_FromLong(p.mi / 60);
+}
+
+static PyObject *
+TimeStamp_minute(TimeStamp *self)
+{
+    TimeStampParts p;
+    TimeStamp_unpack(self, &p);
+    return PyInt_FromLong(p.mi % 60);
+}
+
+static PyObject *
+TimeStamp_second(TimeStamp *self)
+{
+    return PyFloat_FromDouble(TimeStamp_sec(self));
+}
+
+static PyObject *
+TimeStamp_timeTime(TimeStamp *self)
+{
+    TimeStampParts p;
+    TimeStamp_unpack(self, &p);
+    return PyFloat_FromDouble(TimeStamp_abst(p.y, p.m - 1, p.d - 1, p.mi, 0)
+			      + TimeStamp_sec(self) - gmoff);
+}
+
+static PyObject *
+TimeStamp_raw(TimeStamp *self)
+{
+    return PyString_FromStringAndSize((const char*)self->data, 8);
+}
+
+static PyObject *
+TimeStamp_str(TimeStamp *self)
+{
+    char buf[128];
+    TimeStampParts p;
+    int len;
+
+    TimeStamp_unpack(self, &p);
+    len =sprintf(buf, "%4.4d-%2.2d-%2.2d %2.2d:%2.2d:%09.6f",
+	         p.y, p.m, p.d, p.mi / 60, p.mi % 60,
+	         TimeStamp_sec(self));
+
+    return PyString_FromStringAndSize(buf, len);
+}
+
+
+static PyObject *
+TimeStamp_laterThan(TimeStamp *self, PyObject *obj)
+{
+    TimeStamp *o = NULL;
+    TimeStampParts p;
+    unsigned char new[8];
+    int i;
+
+    if (obj->ob_type != self->ob_type) {
+	PyErr_SetString(PyExc_TypeError, "expected TimeStamp object");
+	return NULL;
+    }
+    o = (TimeStamp *)obj;
+    if (memcmp(self->data, o->data, 8) > 0) {
+	Py_INCREF(self);
+	return (PyObject *)self;
+    }
+
+    memcpy(new, o->data, 8);
+    for (i = 7; i > 3; i--) {
+	if (new[i] == 255)
+	    new[i] = 0;
+	else {
+	    new[i]++;
+	    return TimeStamp_FromString((const char*)new);
+	}
+    }
+
+    /* All but the first two bytes are the same.  Need to increment
+       the year, month, and day explicitly. */
+    TimeStamp_unpack(o, &p);
+    if (p.mi >= 1439) {
+	p.mi = 0;
+	if (p.d == month_len[leap(p.y)][p.m - 1]) {
+	    p.d = 1;
+	    if (p.m == 12) {
+		p.m = 1;
+		p.y++;
+	    } else
+		p.m++;
+	} else
+	    p.d++;
+    } else
+	p.mi++;
+
+    return TimeStamp_FromDate(p.y, p.m, p.d, p.mi / 60, p.mi % 60, 0);
+}
+
+static struct PyMethodDef TimeStamp_methods[] = {
+    {"year", 	(PyCFunction)TimeStamp_year, 	METH_NOARGS},
+    {"minute", 	(PyCFunction)TimeStamp_minute, 	METH_NOARGS},
+    {"month", 	(PyCFunction)TimeStamp_month, 	METH_NOARGS},
+    {"day", 	(PyCFunction)TimeStamp_day,	METH_NOARGS},
+    {"hour", 	(PyCFunction)TimeStamp_hour, 	METH_NOARGS},
+    {"second", 	(PyCFunction)TimeStamp_second, 	METH_NOARGS},
+    {"timeTime",(PyCFunction)TimeStamp_timeTime, 	METH_NOARGS},
+    {"laterThan", (PyCFunction)TimeStamp_laterThan, 	METH_O},
+    {"raw",	(PyCFunction)TimeStamp_raw,	METH_NOARGS},
+    {NULL,	NULL},
+};
+
+static PyTypeObject TimeStamp_type = {
+    PyObject_HEAD_INIT(NULL)
+    0,
+    "persistent.TimeStamp",
+    sizeof(TimeStamp),
+    0,
+    (destructor)TimeStamp_dealloc,	/* tp_dealloc */
+    0,					/* tp_print */
+    0,					/* tp_getattr */
+    0,					/* tp_setattr */
+    (cmpfunc)TimeStamp_compare,		/* tp_compare */
+    (reprfunc)TimeStamp_raw,		/* tp_repr */
+    0,					/* tp_as_number */
+    0,					/* tp_as_sequence */
+    0,					/* tp_as_mapping */
+    (hashfunc)TimeStamp_hash,		/* tp_hash */
+    0,					/* tp_call */
+    (reprfunc)TimeStamp_str,		/* tp_str */
+    0,					/* tp_getattro */
+    0,					/* tp_setattro */
+    0,					/* tp_as_buffer */
+    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
+    0,					/* tp_doc */
+    0,					/* tp_traverse */
+    0,					/* tp_clear */
+    0,					/* tp_richcompare */
+    0,					/* tp_weaklistoffset */
+    0,					/* tp_iter */
+    0,					/* tp_iternext */
+    TimeStamp_methods,			/* tp_methods */
+    0,					/* tp_members */
+    0,					/* tp_getset */
+    0,					/* tp_base */
+    0,					/* tp_dict */
+    0,					/* tp_descr_get */
+    0,					/* tp_descr_set */
+};
+
+PyObject *
+TimeStamp_FromString(const char *buf)
+{
+    /* buf must be exactly 8 characters */
+    TimeStamp *ts = (TimeStamp *)PyObject_New(TimeStamp, &TimeStamp_type);
+    memcpy(ts->data, buf, 8);
+    return (PyObject *)ts;
+}
+
+#define CHECK_RANGE(VAR, LO, HI) if ((VAR) < (LO) || (VAR) > (HI)) { \
+     return PyErr_Format(PyExc_ValueError, \
+			 # VAR " must be between %d and %d: %d", \
+			 (LO), (HI), (VAR)); \
+    }
+
+PyObject *
+TimeStamp_FromDate(int year, int month, int day, int hour, int min,
+		   double sec)
+{
+    TimeStamp *ts = NULL;
+    int d;
+    unsigned int v;
+
+    if (year < 1900)
+	return PyErr_Format(PyExc_ValueError,
+			    "year must be greater than 1900: %d", year);
+    CHECK_RANGE(month, 1, 12);
+    d = days_in_month(year, month - 1);
+    if (day < 1 || day > d)
+	return PyErr_Format(PyExc_ValueError,
+			    "day must be between 1 and %d: %d", d, day);
+    CHECK_RANGE(hour, 0, 23);
+    CHECK_RANGE(min, 0, 59);
+    /* Seconds are allowed to be anything, so chill
+       If we did want to be pickly, 60 would be a better choice.
+    if (sec < 0 || sec > 59)
+	return PyErr_Format(PyExc_ValueError,
+			    "second must be between 0 and 59: %f", sec);
+    */
+    ts = (TimeStamp *)PyObject_New(TimeStamp, &TimeStamp_type);
+    v = (((year - 1900) * 12 + month - 1) * 31 + day - 1);
+    v = (v * 24 + hour) * 60 + min;
+    ts->data[0] = v / 16777216;
+    ts->data[1] = (v % 16777216) / 65536;
+    ts->data[2] = (v % 65536) / 256;
+    ts->data[3] = v % 256;
+    sec /= SCONV;
+    v = (unsigned int)sec;
+    ts->data[4] = v / 16777216;
+    ts->data[5] = (v % 16777216) / 65536;
+    ts->data[6] = (v % 65536) / 256;
+    ts->data[7] = v % 256;
+
+    return (PyObject *)ts;
+}
+
+PyObject *
+TimeStamp_TimeStamp(PyObject *obj, PyObject *args)
+{
+    char *buf = NULL;
+    int len = 0, y, mo, d, h = 0, m = 0;
+    double sec = 0;
+
+    if (PyArg_ParseTuple(args, "s#:TimeStamp", &buf, &len)) {
+	if (len != 8) {
+	    PyErr_SetString(PyExc_ValueError, "8-character string expected");
+	    return NULL;
+	}
+	return TimeStamp_FromString(buf);
+    }
+    PyErr_Clear();
+
+    if (!PyArg_ParseTuple(args, "iii|iid", &y, &mo, &d, &h, &m, &sec))
+	return NULL;
+    return TimeStamp_FromDate(y, mo, d, h, m, sec);
+}
+
+static PyMethodDef TimeStampModule_functions[] = {
+    {"TimeStamp",	TimeStamp_TimeStamp,	METH_VARARGS},
+    {NULL,		NULL},
+};
+
+
+void
+initTimeStamp(void)
+{
+    PyObject *m;
+
+    if (TimeStamp_init_gmoff() < 0)
+	return;
+
+    m = Py_InitModule4("TimeStamp", TimeStampModule_functions,
+		       TimeStampModule_doc, NULL, PYTHON_API_VERSION);
+    if (m == NULL)
+	return;
+
+    TimeStamp_type.ob_type = &PyType_Type;
+    TimeStamp_type.tp_getattro = PyObject_GenericGetAttr;
+}

Modified: zope.transaction/trunk/zope/transaction/__init__.py
===================================================================
--- zope.transaction/trunk/zope/transaction/__init__.py	2007-11-08 23:11:28 UTC (rev 81627)
+++ zope.transaction/trunk/zope/transaction/__init__.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -16,8 +16,9 @@
 $Id$
 """
 
-from transaction._transaction import Transaction
-from transaction._manager import TransactionManager, ThreadTransactionManager
+from zope.transaction._transaction import Transaction
+from zope.transaction._manager import TransactionManager
+from zope.transaction._manager import ThreadTransactionManager
 
 manager = ThreadTransactionManager()
 get = manager.get

Modified: zope.transaction/trunk/zope/transaction/_manager.py
===================================================================
--- zope.transaction/trunk/zope/transaction/_manager.py	2007-11-08 23:11:28 UTC (rev 81627)
+++ zope.transaction/trunk/zope/transaction/_manager.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -19,9 +19,9 @@
 
 import thread
 
-from ZODB.utils import WeakSet, deprecated37
+from zope.transaction.weakset import WeakSet
 
-from transaction._transaction import Transaction
+from zope.transaction._transaction import Transaction
 
 # Used for deprecated arguments.  ZODB.utils.DEPRECATED_ARGUMENT was
 # too hard to use here, due to the convoluted import dance across

Modified: zope.transaction/trunk/zope/transaction/_transaction.py
===================================================================
--- zope.transaction/trunk/zope/transaction/_transaction.py	2007-11-08 23:11:28 UTC (rev 81627)
+++ zope.transaction/trunk/zope/transaction/_transaction.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -106,14 +106,11 @@
 from cStringIO import StringIO
 
 from zope import interface
-from ZODB.utils import WeakSet
-from ZODB.utils import deprecated37, deprecated38
-from ZODB.POSException import TransactionFailedError
-from ZODB.utils import oid_repr
 
+from zope.transaction.weakset import WeakSet
+from zope.transaction.interfaces import TransactionFailedError
+from zope.transaction import interfaces
 
-from transaction import interfaces
-
 _marker = object()
 
 # The point of this is to avoid hiding exceptions (which the builtin
@@ -360,11 +357,6 @@
             kws = {}
         self._before_commit.append((hook, tuple(args), kws))
 
-    def beforeCommitHook(self, hook, *args, **kws):
-
-        deprecated38("Use addBeforeCommitHook instead of beforeCommitHook.")
-        self.addBeforeCommitHook(hook, args, kws)
-
     def _callBeforeCommitHooks(self):
         # Call all hooks registered, allowing further registrations
         # during processing.  Note that calls to addBeforeCommitHook() may
@@ -574,6 +566,19 @@
         oid = oid_repr(oid)
     return "%s oid=%s" % (klass, oid)
 
+def oid_repr(oid):
+    if isinstance(oid, str) and len(oid) == 8:
+        # Convert to hex and strip leading zeroes.
+        as_hex = hexlify(oid).lstrip('0')
+        # Ensure two characters per input byte.
+        if len(as_hex) & 1:
+            as_hex = '0' + as_hex
+        elif as_hex == '':
+            as_hex = '00'
+        return '0x' + as_hex
+    else:
+        return repr(oid)
+
 # TODO: deprecate for 3.6.
 class DataManagerAdapter(object):
     """Adapt zodb 4-style data managers to zodb3 style

Modified: zope.transaction/trunk/zope/transaction/interfaces.py
===================================================================
--- zope.transaction/trunk/zope/transaction/interfaces.py	2007-11-08 23:11:28 UTC (rev 81627)
+++ zope.transaction/trunk/zope/transaction/interfaces.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -472,5 +472,17 @@
         begin() method is called explictly.
         """
 
-class DoomedTransaction(Exception):
+class TransactionError(Exception):
+    """An error occurred due to normal transaction processing."""
+
+class TransactionFailedError(TransactionError):
+    """Cannot perform an operation on a transaction that previously failed.
+
+    An attempt was made to commit a transaction, or to join a transaction,
+    but this transaction previously raised an exception during an attempt
+    to commit it.  The transaction must be explicitly aborted, either by
+    invoking abort() on the transaction, or begin() on its transaction
+    manager.
+    """
+class DoomedTransaction(TransactionError):
     """A commit was attempted on a transaction that was doomed."""

Deleted: zope.transaction/trunk/zope/transaction/savepoint.txt
===================================================================
--- zope.transaction/trunk/zope/transaction/savepoint.txt	2007-11-08 23:11:28 UTC (rev 81627)
+++ zope.transaction/trunk/zope/transaction/savepoint.txt	2007-11-09 00:58:54 UTC (rev 81628)
@@ -1,289 +0,0 @@
-Savepoints
-==========
-
-Savepoints provide a way to save to disk intermediate work done during
-a transaction allowing:
-
-- partial transaction (subtransaction) rollback (abort)
-
-- state of saved objects to be freed, freeing on-line memory for other
-  uses
-
-Savepoints make it possible to write atomic subroutines that don't
-make top-level transaction commitments.
-
-
-Applications
-------------
-
-To demonstrate how savepoints work with transactions, we've provided a sample
-data manager implementation that provides savepoint support.  The primary
-purpose of this data manager is to provide code that can be read to understand
-how savepoints work.  The secondary purpose is to provide support for
-demonstrating the correct operation of savepoint support within the
-transaction system.  This data manager is very simple.  It provides flat
-storage of named immutable values, like strings and numbers.
-
-    >>> import transaction.tests.savepointsample
-    >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
-    >>> dm['name'] = 'bob'
-
-As with other data managers, we can commit changes:
-
-    >>> transaction.commit()
-    >>> dm['name']
-    'bob'
-
-and abort changes:
-
-    >>> dm['name'] = 'sally'
-    >>> dm['name']
-    'sally'
-    >>> transaction.abort()
-    >>> dm['name']
-    'bob'
-
-Now, let's look at an application that manages funds for people.  It allows
-deposits and debits to be entered for multiple people.  It accepts a sequence
-of entries and generates a sequence of status messages.  For each entry, it
-applies the change and then validates the user's account.  If the user's
-account is invalid, we roll back the change for that entry.  The success or
-failure of an entry is indicated in the output status.  First we'll initialize
-some accounts:
-
-    >>> dm['bob-balance'] = 0.0
-    >>> dm['bob-credit'] = 0.0
-    >>> dm['sally-balance'] = 0.0
-    >>> dm['sally-credit'] = 100.0
-    >>> transaction.commit()
-
-Now, we'll define a validation function to validate an account:
-
-    >>> def validate_account(name):
-    ...     if dm[name+'-balance'] + dm[name+'-credit'] < 0:
-    ...         raise ValueError('Overdrawn', name)
-
-And a function to apply entries.  If the function fails in some unexpected
-way, it rolls back all of its changes and prints the error:
-
-    >>> def apply_entries(entries):
-    ...     savepoint = transaction.savepoint()
-    ...     try:
-    ...         for name, amount in entries:
-    ...             entry_savepoint = transaction.savepoint()
-    ...             try:
-    ...                 dm[name+'-balance'] += amount
-    ...                 validate_account(name)
-    ...             except ValueError, error:
-    ...                 entry_savepoint.rollback()
-    ...                 print 'Error', str(error)
-    ...             else:
-    ...                 print 'Updated', name
-    ...     except Exception, error:
-    ...         savepoint.rollback()
-    ...         print 'Unexpected exception', error
-
-Now let's try applying some entries:
-
-    >>> apply_entries([
-    ...     ('bob',   10.0),
-    ...     ('sally', 10.0),
-    ...     ('bob',   20.0),
-    ...     ('sally', 10.0),
-    ...     ('bob',   -100.0),
-    ...     ('sally', -100.0),
-    ...     ])
-    Updated bob
-    Updated sally
-    Updated bob
-    Updated sally
-    Error ('Overdrawn', 'bob')
-    Updated sally
-
-    >>> dm['bob-balance']
-    30.0
-
-    >>> dm['sally-balance']
-    -80.0
-
-If we provide entries that cause an unexpected error:
-
-    >>> apply_entries([
-    ...     ('bob',   10.0),
-    ...     ('sally', 10.0),
-    ...     ('bob',   '20.0'),
-    ...     ('sally', 10.0),
-    ...     ])
-    Updated bob
-    Updated sally
-    Unexpected exception unsupported operand type(s) for +=: 'float' and 'str'
-
-Because the apply_entries used a savepoint for the entire function, it was
-able to rollback the partial changes without rolling back changes made in the
-previous call to ``apply_entries``:
-
-    >>> dm['bob-balance']
-    30.0
-
-    >>> dm['sally-balance']
-    -80.0
-
-If we now abort the outer transactions, the earlier changes will go
-away:
-
-    >>> transaction.abort()
-
-    >>> dm['bob-balance']
-    0.0
-
-    >>> dm['sally-balance']
-    0.0
-
-Savepoint invalidation
-----------------------
-
-A savepoint can be used any number of times:
-
-    >>> dm['bob-balance'] = 100.0
-    >>> dm['bob-balance']
-    100.0
-    >>> savepoint = transaction.savepoint()
-
-    >>> dm['bob-balance'] = 200.0
-    >>> dm['bob-balance']
-    200.0
-    >>> savepoint.rollback()
-    >>> dm['bob-balance']
-    100.0
-
-    >>> savepoint.rollback()  # redundant, but should be harmless
-    >>> dm['bob-balance']
-    100.0
-
-    >>> dm['bob-balance'] = 300.0
-    >>> dm['bob-balance']
-    300.0
-    >>> savepoint.rollback()
-    >>> dm['bob-balance']
-    100.0
-
-However, using a savepoint invalidates any savepoints that come after it:
-
-    >>> dm['bob-balance'] = 200.0
-    >>> dm['bob-balance']
-    200.0
-    >>> savepoint1 = transaction.savepoint()
-
-    >>> dm['bob-balance'] = 300.0
-    >>> dm['bob-balance']
-    300.0
-    >>> savepoint2 = transaction.savepoint()
-
-    >>> savepoint.rollback()
-    >>> dm['bob-balance']
-    100.0
-
-    >>> savepoint2.rollback()
-    Traceback (most recent call last):
-    ...
-    InvalidSavepointRollbackError
-
-    >>> savepoint1.rollback()
-    Traceback (most recent call last):
-    ...
-    InvalidSavepointRollbackError
-
-    >>> transaction.abort()
-
-
-Databases without savepoint support
------------------------------------
-
-Normally it's an error to use savepoints with databases that don't support
-savepoints:
-
-    >>> dm_no_sp = transaction.tests.savepointsample.SampleDataManager()
-    >>> dm_no_sp['name'] = 'bob'
-    >>> transaction.commit()
-    >>> dm_no_sp['name'] = 'sally'
-    >>> savepoint = transaction.savepoint()
-    Traceback (most recent call last):
-    ...
-    TypeError: ('Savepoints unsupported', {'name': 'bob'})
-
-    >>> transaction.abort()
-
-However, a flag can be passed to the transaction savepoint method to indicate
-that databases without savepoint support should be tolerated until a savepoint
-is rolled back.  This allows transactions to proceed if there are no reasons
-to roll back:
-
-    >>> dm_no_sp['name'] = 'sally'
-    >>> savepoint = transaction.savepoint(1)
-    >>> dm_no_sp['name'] = 'sue'
-    >>> transaction.commit()
-    >>> dm_no_sp['name']
-    'sue'
-
-    >>> dm_no_sp['name'] = 'sam'
-    >>> savepoint = transaction.savepoint(1)
-    >>> savepoint.rollback()
-    Traceback (most recent call last):
-    ...
-    TypeError: ('Savepoints unsupported', {'name': 'sam'})
-
-
-Failures
---------
-
-If a failure occurs when creating or rolling back a savepoint, the transaction
-state will be uncertain and the transaction will become uncommitable.  From
-that point on, most transaction operations, including commit, will fail until
-the transaction is aborted.
-
-In the previous example, we got an error when we tried to rollback the
-savepoint.  If we try to commit the transaction, the commit will fail:
-
-    >>> transaction.commit() # doctest: +ELLIPSIS
-    Traceback (most recent call last):
-    ...
-    TransactionFailedError: An operation previously failed, with traceback:
-    ...
-    TypeError: ('Savepoints unsupported', {'name': 'sam'})
-    <BLANKLINE>
-
-We have to abort it to make any progress:
-
-    >>> transaction.abort()
-
-Similarly, in our earlier example, where we tried to take a savepoint with a
-data manager that didn't support savepoints:
-
-    >>> dm_no_sp['name'] = 'sally'
-    >>> dm['name'] = 'sally'
-    >>> savepoint = transaction.savepoint()
-    Traceback (most recent call last):
-    ...
-    TypeError: ('Savepoints unsupported', {'name': 'sue'})
-
-    >>> transaction.commit() # doctest: +ELLIPSIS
-    Traceback (most recent call last):
-    ...
-    TransactionFailedError: An operation previously failed, with traceback:
-    ...
-    TypeError: ('Savepoints unsupported', {'name': 'sue'})
-    <BLANKLINE>
-
-    >>> transaction.abort()
-
-After clearing the transaction with an abort, we can get on with new
-transactions:
-
-    >>> dm_no_sp['name'] = 'sally'
-    >>> dm['name'] = 'sally'
-    >>> transaction.commit()
-    >>> dm_no_sp['name']
-    'sally'
-    >>> dm['name']
-    'sally'
-

Deleted: zope.transaction/trunk/zope/transaction/tests/abstestIDataManager.py
===================================================================
--- zope.transaction/trunk/zope/transaction/tests/abstestIDataManager.py	2007-11-08 23:11:28 UTC (rev 81627)
+++ zope.transaction/trunk/zope/transaction/tests/abstestIDataManager.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -1,57 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-"""Test cases for objects implementing IDataManager.
-
-This is a combo test between Connection and DB, since the two are
-rather incestuous and the DB Interface is not defined that I was
-able to find.
-
-To do a full test suite one would probably want to write a dummy
-storage that will raise errors as needed for testing.
-
-I started this test suite to reproduce a very simple error (tpc_abort
-had an error and wouldn't even run if called).  So it is *very*
-incomplete, and even the tests that exist do not make sure that
-the data actually gets written/not written to the storge.
-
-Obviously this test suite should be expanded.
-
-$Id$
-"""
-
-from unittest import TestCase
-
-class IDataManagerTests(TestCase, object):
-
-    def setUp(self):
-        self.datamgr = None # subclass should override
-        self.obj = None # subclass should define Persistent object
-        self.txn_factory = None
-
-    def get_transaction(self):
-        return self.txn_factory()
-
-    ################################
-    # IDataManager interface tests #
-    ################################
-
-    def testCommitObj(self):
-        tran = self.get_transaction()
-        self.datamgr.prepare(tran)
-        self.datamgr.commit(tran)
-
-    def testAbortTran(self):
-        tran = self.get_transaction()
-        self.datamgr.prepare(tran)
-        self.datamgr.abort(tran)

Modified: zope.transaction/trunk/zope/transaction/tests/doom.txt
===================================================================
--- zope.transaction/trunk/zope/transaction/tests/doom.txt	2007-11-08 23:11:28 UTC (rev 81627)
+++ zope.transaction/trunk/zope/transaction/tests/doom.txt	2007-11-09 00:58:54 UTC (rev 81628)
@@ -25,7 +25,7 @@
 
 To see how it works we first need to create a stub data manager:
 
-    >>> from transaction.interfaces import IDataManager
+    >>> from zope.transaction.interfaces import IDataManager
     >>> from zope.interface import implements
     >>> class DataManager:
     ...     implements(IDataManager)
@@ -45,7 +45,7 @@
 
 Start a new transaction:
 
-    >>> import transaction
+    >>> from zope import transaction
     >>> txn = transaction.begin()
     >>> dm = DataManager()
     >>> txn.join(dm)

Added: zope.transaction/trunk/zope/transaction/tests/sampledm.py
===================================================================
--- zope.transaction/trunk/zope/transaction/tests/sampledm.py	                        (rev 0)
+++ zope.transaction/trunk/zope/transaction/tests/sampledm.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -0,0 +1,412 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Sample objects for use in tests
+
+$Id: sampledm.py 29896 2005-04-07 04:48:06Z tim_one $
+"""
+
+class DataManager(object):
+    """Sample data manager
+
+       This class provides a trivial data-manager implementation and doc
+       strings to illustrate the the protocol and to provide a tool for
+       writing tests.
+
+       Our sample data manager has state that is updated through an inc
+       method and through transaction operations.
+
+       When we create a sample data manager:
+
+       >>> dm = DataManager()
+
+       It has two bits of state, state:
+
+       >>> dm.state
+       0
+
+       and delta:
+
+       >>> dm.delta
+       0
+
+       Both of which are initialized to 0.  state is meant to model
+       committed state, while delta represents tentative changes within a
+       transaction.  We change the state by calling inc:
+
+       >>> dm.inc()
+
+       which updates delta:
+
+       >>> dm.delta
+       1
+
+       but state isn't changed until we commit the transaction:
+
+       >>> dm.state
+       0
+
+       To commit the changes, we use 2-phase commit. We execute the first
+       stage by calling prepare.  We need to pass a transation. Our
+       sample data managers don't really use the transactions for much,
+       so we'll be lazy and use strings for transactions:
+
+       >>> t1 = '1'
+       >>> dm.prepare(t1)
+
+       The sample data manager updates the state when we call prepare:
+
+       >>> dm.state
+       1
+       >>> dm.delta
+       1
+
+       This is mainly so we can detect some affect of calling the methods.
+
+       Now if we call commit:
+
+       >>> dm.commit(t1)
+
+       Our changes are"permanent".  The state reflects the changes and the
+       delta has been reset to 0.
+
+       >>> dm.state
+       1
+       >>> dm.delta
+       0
+       """
+
+    def __init__(self):
+        self.state = 0
+        self.sp = 0
+        self.transaction = None
+        self.delta = 0
+        self.prepared = False
+
+    def inc(self, n=1):
+        self.delta += n
+
+    def prepare(self, transaction):
+        """Prepare to commit data
+
+        >>> dm = DataManager()
+        >>> dm.inc()
+        >>> t1 = '1'
+        >>> dm.prepare(t1)
+        >>> dm.commit(t1)
+        >>> dm.state
+        1
+        >>> dm.inc()
+        >>> t2 = '2'
+        >>> dm.prepare(t2)
+        >>> dm.abort(t2)
+        >>> dm.state
+        1
+
+        It is en error to call prepare more than once without an intervening
+        commit or abort:
+
+        >>> dm.prepare(t1)
+
+        >>> dm.prepare(t1)
+        Traceback (most recent call last):
+        ...
+        TypeError: Already prepared
+
+        >>> dm.prepare(t2)
+        Traceback (most recent call last):
+        ...
+        TypeError: Already prepared
+
+        >>> dm.abort(t1)
+
+        If there was a preceeding savepoint, the transaction must match:
+
+        >>> rollback = dm.savepoint(t1)
+        >>> dm.prepare(t2)
+        Traceback (most recent call last):
+        ,,,
+        TypeError: ('Transaction missmatch', '2', '1')
+
+        >>> dm.prepare(t1)
+
+        """
+        if self.prepared:
+            raise TypeError('Already prepared')
+        self._checkTransaction(transaction)
+        self.prepared = True
+        self.transaction = transaction
+        self.state += self.delta
+
+    def _checkTransaction(self, transaction):
+        if (transaction is not self.transaction
+            and self.transaction is not None):
+            raise TypeError("Transaction missmatch",
+                            transaction, self.transaction)
+
+    def abort(self, transaction):
+        """Abort a transaction
+
+        The abort method can be called before two-phase commit to
+        throw away work done in the transaction:
+
+        >>> dm = DataManager()
+        >>> dm.inc()
+        >>> dm.state, dm.delta
+        (0, 1)
+        >>> t1 = '1'
+        >>> dm.abort(t1)
+        >>> dm.state, dm.delta
+        (0, 0)
+
+        The abort method also throws away work done in savepoints:
+
+        >>> dm.inc()
+        >>> r = dm.savepoint(t1)
+        >>> dm.inc()
+        >>> r = dm.savepoint(t1)
+        >>> dm.state, dm.delta
+        (0, 2)
+        >>> dm.abort(t1)
+        >>> dm.state, dm.delta
+        (0, 0)
+
+        If savepoints are used, abort must be passed the same
+        transaction:
+
+        >>> dm.inc()
+        >>> r = dm.savepoint(t1)
+        >>> t2 = '2'
+        >>> dm.abort(t2)
+        Traceback (most recent call last):
+        ...
+        TypeError: ('Transaction missmatch', '2', '1')
+
+        >>> dm.abort(t1)
+
+        The abort method is also used to abort a two-phase commit:
+
+        >>> dm.inc()
+        >>> dm.state, dm.delta
+        (0, 1)
+        >>> dm.prepare(t1)
+        >>> dm.state, dm.delta
+        (1, 1)
+        >>> dm.abort(t1)
+        >>> dm.state, dm.delta
+        (0, 0)
+
+        Of course, the transactions passed to prepare and abort must
+        match:
+
+        >>> dm.prepare(t1)
+        >>> dm.abort(t2)
+        Traceback (most recent call last):
+        ...
+        TypeError: ('Transaction missmatch', '2', '1')
+
+        >>> dm.abort(t1)
+
+
+        """
+        self._checkTransaction(transaction)
+        if self.transaction is not None:
+            self.transaction = None
+
+        if self.prepared:
+            self.state -= self.delta
+            self.prepared = False
+
+        self.delta = 0
+
+    def commit(self, transaction):
+        """Complete two-phase commit
+
+        >>> dm = DataManager()
+        >>> dm.state
+        0
+        >>> dm.inc()
+
+        We start two-phase commit by calling prepare:
+
+        >>> t1 = '1'
+        >>> dm.prepare(t1)
+
+        We complete it by calling commit:
+
+        >>> dm.commit(t1)
+        >>> dm.state
+        1
+
+        It is an error ro call commit without calling prepare first:
+
+        >>> dm.inc()
+        >>> t2 = '2'
+        >>> dm.commit(t2)
+        Traceback (most recent call last):
+        ...
+        TypeError: Not prepared to commit
+
+        >>> dm.prepare(t2)
+        >>> dm.commit(t2)
+
+        If course, the transactions given to prepare and commit must
+        be the same:
+
+        >>> dm.inc()
+        >>> t3 = '3'
+        >>> dm.prepare(t3)
+        >>> dm.commit(t2)
+        Traceback (most recent call last):
+        ...
+        TypeError: ('Transaction missmatch', '2', '3')
+
+        """
+        if not self.prepared:
+            raise TypeError('Not prepared to commit')
+        self._checkTransaction(transaction)
+        self.delta = 0
+        self.transaction = None
+        self.prepared = False
+
+    def savepoint(self, transaction):
+        """Provide the ability to rollback transaction state
+
+        Savepoints provide a way to:
+
+        - Save partial transaction work. For some data managers, this
+          could allow resources to be used more efficiently.
+
+        - Provide the ability to revert state to a point in a
+          transaction without aborting the entire transaction.  In
+          other words, savepoints support partial aborts.
+
+        Savepoints don't use two-phase commit. If there are errors in
+        setting or rolling back to savepoints, the application should
+        abort the containing transaction.  This is *not* the
+        responsibility of the data manager.
+
+        Savepoints are always associated with a transaction. Any work
+        done in a savepoint's transaction is tentative until the
+        transaction is committed using two-phase commit.
+
+        >>> dm = DataManager()
+        >>> dm.inc()
+        >>> t1 = '1'
+        >>> r = dm.savepoint(t1)
+        >>> dm.state, dm.delta
+        (0, 1)
+        >>> dm.inc()
+        >>> dm.state, dm.delta
+        (0, 2)
+        >>> r.rollback()
+        >>> dm.state, dm.delta
+        (0, 1)
+        >>> dm.prepare(t1)
+        >>> dm.commit(t1)
+        >>> dm.state, dm.delta
+        (1, 0)
+
+        Savepoints must have the same transaction:
+
+        >>> r1 = dm.savepoint(t1)
+        >>> dm.state, dm.delta
+        (1, 0)
+        >>> dm.inc()
+        >>> dm.state, dm.delta
+        (1, 1)
+        >>> t2 = '2'
+        >>> r2 = dm.savepoint(t2)
+        Traceback (most recent call last):
+        ...
+        TypeError: ('Transaction missmatch', '2', '1')
+
+        >>> r2 = dm.savepoint(t1)
+        >>> dm.inc()
+        >>> dm.state, dm.delta
+        (1, 2)
+
+        If we rollback to an earlier savepoint, we discard all work
+        done later:
+
+        >>> r1.rollback()
+        >>> dm.state, dm.delta
+        (1, 0)
+
+        and we can no longer rollback to the later savepoint:
+
+        >>> r2.rollback()
+        Traceback (most recent call last):
+        ...
+        TypeError: ('Attempt to roll back to invalid save point', 3, 2)
+
+        We can roll back to a savepoint as often as we like:
+
+        >>> r1.rollback()
+        >>> r1.rollback()
+        >>> r1.rollback()
+        >>> dm.state, dm.delta
+        (1, 0)
+
+        >>> dm.inc()
+        >>> dm.inc()
+        >>> dm.inc()
+        >>> dm.state, dm.delta
+        (1, 3)
+        >>> r1.rollback()
+        >>> dm.state, dm.delta
+        (1, 0)
+
+        But we can't rollback to a savepoint after it has been
+        committed:
+
+        >>> dm.prepare(t1)
+        >>> dm.commit(t1)
+
+        >>> r1.rollback()
+        Traceback (most recent call last):
+        ...
+        TypeError: Attempt to rollback stale rollback
+
+        """
+        if self.prepared:
+            raise TypeError("Can't get savepoint during two-phase commit")
+        self._checkTransaction(transaction)
+        self.transaction = transaction
+        self.sp += 1
+        return Rollback(self)
+
+class Rollback(object):
+
+    def __init__(self, dm):
+        self.dm = dm
+        self.sp = dm.sp
+        self.delta = dm.delta
+        self.transaction = dm.transaction
+
+    def rollback(self):
+        if self.transaction is not self.dm.transaction:
+            raise TypeError("Attempt to rollback stale rollback")
+        if self.dm.sp < self.sp:
+            raise TypeError("Attempt to roll back to invalid save point",
+                            self.sp, self.dm.sp)
+        self.dm.sp = self.sp
+        self.dm.delta = self.delta
+
+
+def test_suite():
+    from zope.testing.doctest import DocTestSuite
+    return DocTestSuite()
+
+if __name__ == '__main__':
+    unittest.main()


Property changes on: zope.transaction/trunk/zope/transaction/tests/sampledm.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zope.transaction/trunk/zope/transaction/tests/savepoint.txt
===================================================================
--- zope.transaction/trunk/zope/transaction/tests/savepoint.txt	                        (rev 0)
+++ zope.transaction/trunk/zope/transaction/tests/savepoint.txt	2007-11-09 00:58:54 UTC (rev 81628)
@@ -0,0 +1,290 @@
+Savepoints
+==========
+
+Savepoints provide a way to save to disk intermediate work done during
+a transaction allowing:
+
+- partial transaction (subtransaction) rollback (abort)
+
+- state of saved objects to be freed, freeing on-line memory for other
+  uses
+
+Savepoints make it possible to write atomic subroutines that don't
+make top-level transaction commitments.
+
+
+Applications
+------------
+
+To demonstrate how savepoints work with transactions, we've provided a sample
+data manager implementation that provides savepoint support.  The primary
+purpose of this data manager is to provide code that can be read to understand
+how savepoints work.  The secondary purpose is to provide support for
+demonstrating the correct operation of savepoint support within the
+transaction system.  This data manager is very simple.  It provides flat
+storage of named immutable values, like strings and numbers.
+
+    >>> from zope import transaction
+    >>> from zope.transaction.tests import savepointsample
+    >>> dm = savepointsample.SampleSavepointDataManager()
+    >>> dm['name'] = 'bob'
+
+As with other data managers, we can commit changes:
+
+    >>> transaction.commit()
+    >>> dm['name']
+    'bob'
+
+and abort changes:
+
+    >>> dm['name'] = 'sally'
+    >>> dm['name']
+    'sally'
+    >>> transaction.abort()
+    >>> dm['name']
+    'bob'
+
+Now, let's look at an application that manages funds for people.  It allows
+deposits and debits to be entered for multiple people.  It accepts a sequence
+of entries and generates a sequence of status messages.  For each entry, it
+applies the change and then validates the user's account.  If the user's
+account is invalid, we roll back the change for that entry.  The success or
+failure of an entry is indicated in the output status.  First we'll initialize
+some accounts:
+
+    >>> dm['bob-balance'] = 0.0
+    >>> dm['bob-credit'] = 0.0
+    >>> dm['sally-balance'] = 0.0
+    >>> dm['sally-credit'] = 100.0
+    >>> transaction.commit()
+
+Now, we'll define a validation function to validate an account:
+
+    >>> def validate_account(name):
+    ...     if dm[name+'-balance'] + dm[name+'-credit'] < 0:
+    ...         raise ValueError('Overdrawn', name)
+
+And a function to apply entries.  If the function fails in some unexpected
+way, it rolls back all of its changes and prints the error:
+
+    >>> def apply_entries(entries):
+    ...     savepoint = transaction.savepoint()
+    ...     try:
+    ...         for name, amount in entries:
+    ...             entry_savepoint = transaction.savepoint()
+    ...             try:
+    ...                 dm[name+'-balance'] += amount
+    ...                 validate_account(name)
+    ...             except ValueError, error:
+    ...                 entry_savepoint.rollback()
+    ...                 print 'Error', str(error)
+    ...             else:
+    ...                 print 'Updated', name
+    ...     except Exception, error:
+    ...         savepoint.rollback()
+    ...         print 'Unexpected exception', error
+
+Now let's try applying some entries:
+
+    >>> apply_entries([
+    ...     ('bob',   10.0),
+    ...     ('sally', 10.0),
+    ...     ('bob',   20.0),
+    ...     ('sally', 10.0),
+    ...     ('bob',   -100.0),
+    ...     ('sally', -100.0),
+    ...     ])
+    Updated bob
+    Updated sally
+    Updated bob
+    Updated sally
+    Error ('Overdrawn', 'bob')
+    Updated sally
+
+    >>> dm['bob-balance']
+    30.0
+
+    >>> dm['sally-balance']
+    -80.0
+
+If we provide entries that cause an unexpected error:
+
+    >>> apply_entries([
+    ...     ('bob',   10.0),
+    ...     ('sally', 10.0),
+    ...     ('bob',   '20.0'),
+    ...     ('sally', 10.0),
+    ...     ])
+    Updated bob
+    Updated sally
+    Unexpected exception unsupported operand type(s) for +=: 'float' and 'str'
+
+Because the apply_entries used a savepoint for the entire function, it was
+able to rollback the partial changes without rolling back changes made in the
+previous call to ``apply_entries``:
+
+    >>> dm['bob-balance']
+    30.0
+
+    >>> dm['sally-balance']
+    -80.0
+
+If we now abort the outer transactions, the earlier changes will go
+away:
+
+    >>> transaction.abort()
+
+    >>> dm['bob-balance']
+    0.0
+
+    >>> dm['sally-balance']
+    0.0
+
+Savepoint invalidation
+----------------------
+
+A savepoint can be used any number of times:
+
+    >>> dm['bob-balance'] = 100.0
+    >>> dm['bob-balance']
+    100.0
+    >>> savepoint = transaction.savepoint()
+
+    >>> dm['bob-balance'] = 200.0
+    >>> dm['bob-balance']
+    200.0
+    >>> savepoint.rollback()
+    >>> dm['bob-balance']
+    100.0
+
+    >>> savepoint.rollback()  # redundant, but should be harmless
+    >>> dm['bob-balance']
+    100.0
+
+    >>> dm['bob-balance'] = 300.0
+    >>> dm['bob-balance']
+    300.0
+    >>> savepoint.rollback()
+    >>> dm['bob-balance']
+    100.0
+
+However, using a savepoint invalidates any savepoints that come after it:
+
+    >>> dm['bob-balance'] = 200.0
+    >>> dm['bob-balance']
+    200.0
+    >>> savepoint1 = transaction.savepoint()
+
+    >>> dm['bob-balance'] = 300.0
+    >>> dm['bob-balance']
+    300.0
+    >>> savepoint2 = transaction.savepoint()
+
+    >>> savepoint.rollback()
+    >>> dm['bob-balance']
+    100.0
+
+    >>> savepoint2.rollback()
+    Traceback (most recent call last):
+    ...
+    InvalidSavepointRollbackError
+
+    >>> savepoint1.rollback()
+    Traceback (most recent call last):
+    ...
+    InvalidSavepointRollbackError
+
+    >>> transaction.abort()
+
+
+Databases without savepoint support
+-----------------------------------
+
+Normally it's an error to use savepoints with databases that don't support
+savepoints:
+
+    >>> dm_no_sp = savepointsample.SampleDataManager()
+    >>> dm_no_sp['name'] = 'bob'
+    >>> transaction.commit()
+    >>> dm_no_sp['name'] = 'sally'
+    >>> savepoint = transaction.savepoint()
+    Traceback (most recent call last):
+    ...
+    TypeError: ('Savepoints unsupported', {'name': 'bob'})
+
+    >>> transaction.abort()
+
+However, a flag can be passed to the transaction savepoint method to indicate
+that databases without savepoint support should be tolerated until a savepoint
+is rolled back.  This allows transactions to proceed if there are no reasons
+to roll back:
+
+    >>> dm_no_sp['name'] = 'sally'
+    >>> savepoint = transaction.savepoint(1)
+    >>> dm_no_sp['name'] = 'sue'
+    >>> transaction.commit()
+    >>> dm_no_sp['name']
+    'sue'
+
+    >>> dm_no_sp['name'] = 'sam'
+    >>> savepoint = transaction.savepoint(1)
+    >>> savepoint.rollback()
+    Traceback (most recent call last):
+    ...
+    TypeError: ('Savepoints unsupported', {'name': 'sam'})
+
+
+Failures
+--------
+
+If a failure occurs when creating or rolling back a savepoint, the transaction
+state will be uncertain and the transaction will become uncommitable.  From
+that point on, most transaction operations, including commit, will fail until
+the transaction is aborted.
+
+In the previous example, we got an error when we tried to rollback the
+savepoint.  If we try to commit the transaction, the commit will fail:
+
+    >>> transaction.commit() # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    TransactionFailedError: An operation previously failed, with traceback:
+    ...
+    TypeError: ('Savepoints unsupported', {'name': 'sam'})
+    <BLANKLINE>
+
+We have to abort it to make any progress:
+
+    >>> transaction.abort()
+
+Similarly, in our earlier example, where we tried to take a savepoint with a
+data manager that didn't support savepoints:
+
+    >>> dm_no_sp['name'] = 'sally'
+    >>> dm['name'] = 'sally'
+    >>> savepoint = transaction.savepoint()
+    Traceback (most recent call last):
+    ...
+    TypeError: ('Savepoints unsupported', {'name': 'sue'})
+
+    >>> transaction.commit() # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    TransactionFailedError: An operation previously failed, with traceback:
+    ...
+    TypeError: ('Savepoints unsupported', {'name': 'sue'})
+    <BLANKLINE>
+
+    >>> transaction.abort()
+
+After clearing the transaction with an abort, we can get on with new
+transactions:
+
+    >>> dm_no_sp['name'] = 'sally'
+    >>> dm['name'] = 'sally'
+    >>> transaction.commit()
+    >>> dm_no_sp['name']
+    'sally'
+    >>> dm['name']
+    'sally'
+

Modified: zope.transaction/trunk/zope/transaction/tests/savepointsample.py
===================================================================
--- zope.transaction/trunk/zope/transaction/tests/savepointsample.py	2007-11-08 23:11:28 UTC (rev 81627)
+++ zope.transaction/trunk/zope/transaction/tests/savepointsample.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -23,7 +23,7 @@
 
 import UserDict
 from zope import interface
-import transaction.interfaces
+import zope.transaction.interfaces
 
 class SampleDataManager(UserDict.DictMixin):
     """Sample implementation of data manager that doesn't support savepoints
@@ -31,11 +31,12 @@
     This data manager stores named simple values, like strings and numbers.
     """
 
-    interface.implements(transaction.interfaces.IDataManager)
+    interface.implements(zope.transaction.interfaces.IDataManager)
 
     def __init__(self, transaction_manager=None):
         if transaction_manager is None:
             # Use the thread-local transaction manager if none is provided:
+            from zope import transaction
             transaction_manager = transaction.manager
         self.transaction_manager = transaction_manager
 
@@ -157,7 +158,7 @@
     This extends the basic data manager with savepoint support.
     """
 
-    interface.implements(transaction.interfaces.ISavepointDataManager)
+    interface.implements(zope.transaction.interfaces.ISavepointDataManager)
 
     def savepoint(self):
         # When we create the savepoint, we save the existing database state.
@@ -174,7 +175,7 @@
 
 class SampleSavepoint:
 
-    interface.implements(transaction.interfaces.IDataManagerSavepoint)
+    interface.implements(zope.transaction.interfaces.IDataManagerSavepoint)
 
     def __init__(self, data_manager, data):
         self.data_manager = data_manager

Modified: zope.transaction/trunk/zope/transaction/tests/test_SampleDataManager.py
===================================================================
--- zope.transaction/trunk/zope/transaction/tests/test_SampleDataManager.py	2007-11-08 23:11:28 UTC (rev 81627)
+++ zope.transaction/trunk/zope/transaction/tests/test_SampleDataManager.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -408,5 +408,8 @@
     from zope.testing.doctest import DocTestSuite
     return DocTestSuite()
 
+# additional_tests is for setuptools "setup.py test" support
+additional_tests = test_suite
+
 if __name__ == '__main__':
     unittest.main()

Modified: zope.transaction/trunk/zope/transaction/tests/test_SampleResourceManager.py
===================================================================
--- zope.transaction/trunk/zope/transaction/tests/test_SampleResourceManager.py	2007-11-08 23:11:28 UTC (rev 81627)
+++ zope.transaction/trunk/zope/transaction/tests/test_SampleResourceManager.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -431,5 +431,8 @@
     from doctest import DocTestSuite
     return DocTestSuite()
 
+# additional_tests is for setuptools "setup.py test" support
+additional_tests = test_suite
+
 if __name__ == '__main__':
     unittest.main()

Modified: zope.transaction/trunk/zope/transaction/tests/test_register_compat.py
===================================================================
--- zope.transaction/trunk/zope/transaction/tests/test_register_compat.py	2007-11-08 23:11:28 UTC (rev 81627)
+++ zope.transaction/trunk/zope/transaction/tests/test_register_compat.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -91,7 +91,7 @@
 >>> transaction.abort()
 """
 
-import transaction
+from zope import transaction
 
 class Object(object):
 
@@ -152,3 +152,6 @@
 
 def test_suite():
     return doctest.DocTestSuite()
+
+# additional_tests is for setuptools "setup.py test" support
+additional_tests = test_suite

Modified: zope.transaction/trunk/zope/transaction/tests/test_savepoint.py
===================================================================
--- zope.transaction/trunk/zope/transaction/tests/test_savepoint.py	2007-11-08 23:11:28 UTC (rev 81627)
+++ zope.transaction/trunk/zope/transaction/tests/test_savepoint.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -26,14 +26,15 @@
 rollback savepoints for data managers that joined savepoints after the
 savepoint:
 
-    >>> import transaction.tests.savepointsample
-    >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
+    >>> from zope import transaction
+    >>> from zope.transaction.tests import savepointsample
+    >>> dm = savepointsample.SampleSavepointDataManager()
     >>> dm['name'] = 'bob'
     >>> sp1 = transaction.savepoint()
     >>> dm['job'] = 'geek'
     >>> sp2 = transaction.savepoint()
     >>> dm['salary'] = 'fun'    
-    >>> dm2 = transaction.tests.savepointsample.SampleSavepointDataManager()
+    >>> dm2 = savepointsample.SampleSavepointDataManager()
     >>> dm2['name'] = 'sally'
 
     >>> 'name' in dm
@@ -60,10 +61,13 @@
 
 def test_suite():
     return unittest.TestSuite((
-        doctest.DocFileSuite('../savepoint.txt'),
+        doctest.DocFileSuite('savepoint.txt'),
         doctest.DocTestSuite(),
         ))
 
+# additional_tests is for setuptools "setup.py test" support
+additional_tests = test_suite
+
 if __name__ == '__main__':
     unittest.main(defaultTest='test_suite')
 

Added: zope.transaction/trunk/zope/transaction/tests/test_timestamp.py
===================================================================
--- zope.transaction/trunk/zope/transaction/tests/test_timestamp.py	                        (rev 0)
+++ zope.transaction/trunk/zope/transaction/tests/test_timestamp.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -0,0 +1,181 @@
+#############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+import unittest
+import time
+from zope.transaction.TimeStamp import TimeStamp
+
+EPSILON = 0.000001
+
+class TimeStampTests(unittest.TestCase):
+    def testStringInput(self):
+        ts = TimeStamp('00000000')
+        self.assertEqual(repr(ts), '00000000')
+        ts = TimeStamp('11111111')
+        self.assertEqual(repr(ts), '11111111')
+
+    def testTupleInput(self):
+        t = int(time.time())
+        args = time.gmtime(t)[:6]
+        ts = TimeStamp(*args)
+        self.assertEqual(ts.year(), args[0])
+        self.assertEqual(ts.month(), args[1])
+        self.assertEqual(ts.day(), args[2])
+        self.assertEqual(ts.hour(), args[3])
+        self.assertEqual(ts.minute(), args[4])
+        self.assertEqual(int(round(ts.second())), args[5])
+
+    def testRaw(self):
+        ts = TimeStamp('00000000')
+        self.assertEqual(repr(ts), ts.raw())
+        ts = TimeStamp('11111111')
+        self.assertEqual(repr(ts), ts.raw())
+
+    def testStr(self):
+        t1 = 1141445984
+        args1 = time.gmtime(t1)[:6]
+        ts1 = TimeStamp(*args1)
+        self.assertEqual(str(ts1), '2006-03-04 04:19:44.000000')
+
+    def testTimeTime(self):
+        t = int(time.time())
+        args = time.gmtime(t)[:6]
+        ts = TimeStamp(*args)
+        tt = ts.timeTime()
+        self.assertEqual(tt, t)
+
+    def testYMDTimeStamp(self):
+        self._check_ymd(2001, 6, 3)
+
+    def _check_ymd(self, yr, mo, dy):
+        ts = TimeStamp(yr, mo, dy)
+        self.assertEqual(ts.year(), yr)
+        self.assertEqual(ts.month(), mo)
+        self.assertEqual(ts.day(), dy)
+
+        self.assertEquals(ts.hour(), 0)
+        self.assertEquals(ts.minute(), 0)
+        self.assertEquals(ts.second(), 0)
+
+        t = time.gmtime(ts.timeTime())
+        self.assertEquals(yr, t[0])
+        self.assertEquals(mo, t[1])
+        self.assertEquals(dy, t[2])
+
+    def testFullTimeStamp(self):
+        native_ts = int(time.time()) # fractional seconds get in the way
+        t = time.gmtime(native_ts)   # the corresponding GMT struct tm
+        ts = TimeStamp(*t[:6])
+
+        # Seconds are stored internally via (conceptually) multiplying by
+        # 2**32 then dividing by 60, ending up with a 32-bit integer.
+        # While this gives a lot of room for cramming many distinct
+        # TimeStamps into a second, it's not good at roundtrip accuracy.
+        # For example, 1 second is stored as int(2**32/60) == 71582788.
+        # Converting back gives 71582788*60.0/2**32 == 0.9999999962747097.
+        # In general, we can lose up to 0.999... to truncation during
+        # storing, creating an absolute error up to about 1*60.0/2**32 ==
+        # 0.000000014 on the seconds value we get back.  This is so even
+        # when we have an exact integral second value going in (as we
+        # do in this test), so we can't expect equality in any comparison
+        # involving seconds.  Minutes (etc) are stored exactly, so we
+        # can expect equality for those.
+
+        self.assert_(abs(ts.timeTime() - native_ts) < EPSILON)
+        self.assertEqual(ts.year(), t[0])
+        self.assertEqual(ts.month(), t[1])
+        self.assertEqual(ts.day(), t[2])
+        self.assertEquals(ts.hour(), t[3])
+        self.assertEquals(ts.minute(), t[4])
+        self.assert_(abs(ts.second() - t[5]) < EPSILON)
+
+    def testRawTimestamp(self):
+        t = time.gmtime()
+        ts1 = TimeStamp(*t[:6])
+        ts2 = TimeStamp(`ts1`)
+
+        self.assertEquals(ts1, ts2)
+        self.assertEquals(ts1.timeTime(), ts2.timeTime())
+        self.assertEqual(ts1.year(), ts2.year())
+        self.assertEqual(ts1.month(), ts2.month())
+        self.assertEqual(ts1.day(), ts2.day())
+        self.assertEquals(ts1.hour(), ts2.hour())
+        self.assertEquals(ts1.minute(), ts2.minute())
+        self.assert_(abs(ts1.second() - ts2.second()) < EPSILON)
+
+    def testDictKey(self):
+        t = time.gmtime()
+        ts1 = TimeStamp(*t[:6])
+        ts2 = TimeStamp(2000, *t[1:6])
+
+        d = {}
+        d[ts1] = 1
+        d[ts2] = 2
+
+        self.assertEquals(len(d), 2)
+
+    def testCompare(self):
+        ts1 = TimeStamp(1972, 6, 27)
+        ts2 = TimeStamp(1971, 12, 12)
+        self.assert_(ts1 > ts2)
+        self.assert_(ts2 <= ts1)
+
+    def testLaterThan(self):
+        t = time.gmtime()
+        ts = TimeStamp(*t[:6])
+        ts2 = ts.laterThan(ts)
+        self.assert_(ts2 > ts)
+
+    # TODO:  should test for bogus inputs to TimeStamp constructor
+
+    def testTimeStamp(self):
+        # Alternate test suite
+        t = TimeStamp(2002, 1, 23, 10, 48, 5) # GMT
+        self.assertEquals(str(t), '2002-01-23 10:48:05.000000')
+        self.assertEquals(repr(t), '\x03B9H\x15UUU')
+        self.assertEquals(TimeStamp('\x03B9H\x15UUU'), t)
+        self.assertEquals(t.year(), 2002)
+        self.assertEquals(t.month(), 1)
+        self.assertEquals(t.day(), 23)
+        self.assertEquals(t.hour(), 10)
+        self.assertEquals(t.minute(), 48)
+        self.assertEquals(round(t.second()), 5)
+        self.assertEquals(t.timeTime(), 1011782885)
+        t1 = TimeStamp(2002, 1, 23, 10, 48, 10)
+        self.assertEquals(str(t1), '2002-01-23 10:48:10.000000')
+        self.assert_(t == t)
+        self.assert_(t != t1)
+        self.assert_(t < t1)
+        self.assert_(t <= t1)
+        self.assert_(t1 >= t)
+        self.assert_(t1 > t)
+        self.failIf(t == t1)
+        self.failIf(t != t)
+        self.failIf(t > t1)
+        self.failIf(t >= t1)
+        self.failIf(t1 < t)
+        self.failIf(t1 <= t)
+        self.assertEquals(cmp(t, t), 0)
+        self.assertEquals(cmp(t, t1), -1)
+        self.assertEquals(cmp(t1, t), 1)
+        self.assertEquals(t1.laterThan(t), t1)
+        self.assert_(t.laterThan(t1) > t1)
+        self.assertEquals(TimeStamp(2002,1,23), TimeStamp(2002,1,23,0,0,0))
+
+
+def test_suite():
+    return unittest.makeSuite(TimeStampTests)
+
+if __name__ == '__main__':
+    unittest.main()


Property changes on: zope.transaction/trunk/zope/transaction/tests/test_timestamp.py
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: zope.transaction/trunk/zope/transaction/tests/test_transaction.py
===================================================================
--- zope.transaction/trunk/zope/transaction/tests/test_transaction.py	2007-11-08 23:11:28 UTC (rev 81627)
+++ zope.transaction/trunk/zope/transaction/tests/test_transaction.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -39,13 +39,23 @@
 $Id$
 """
 
+import struct
 import unittest
 import warnings
 
-import transaction
-from ZODB.utils import positive_id
-from ZODB.tests.warnhook import WarningsHook
+from zope import transaction
+from zope.transaction.tests.warnhook import WarningsHook
 
+_ADDRESS_MASK = 256 ** struct.calcsize('P')
+def positive_id(obj):
+    """Return id(obj) as a non-negative integer."""
+
+    result = id(obj)
+    if result < 0:
+        result += _ADDRESS_MASK
+        assert result > 0
+    return result
+
 class TransactionTests(unittest.TestCase):
 
     def setUp(self):
@@ -343,8 +353,8 @@
     The argument to join must be a zodb4 data manager,
     transaction.interfaces.IDataManager.
 
-    >>> from ZODB.tests.sampledm import DataManager
-    >>> from transaction._transaction import DataManagerAdapter
+    >>> from zope.transaction.tests.sampledm import DataManager
+    >>> from zope.transaction._transaction import DataManagerAdapter
     >>> t = transaction.Transaction()
     >>> dm = DataManager()
     >>> t.join(dm)
@@ -362,178 +372,6 @@
 def hook():
     pass
 
-# deprecated38; remove this then
-def test_beforeCommitHook():
-    """Test beforeCommitHook.
-
-    Let's define a hook to call, and a way to see that it was called.
-
-      >>> log = []
-      >>> def reset_log():
-      ...     del log[:]
-
-      >>> def hook(arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
-      ...     log.append("arg %r kw1 %r kw2 %r" % (arg, kw1, kw2))
-
-    beforeCommitHook is deprecated, so we need cruft to suppress the
-    warnings.
-
-      >>> whook = WarningsHook()
-      >>> whook.install()
-
-    Fool the warnings module into delivering the warnings despite that
-    they've been seen before; this is needed in case this test is run
-    more than once.
-
-      >>> import warnings
-      >>> warnings.filterwarnings("always", category=DeprecationWarning)
-
-    Now register the hook with a transaction.
-
-      >>> import transaction
-      >>> t = transaction.begin()
-      >>> t.beforeCommitHook(hook, '1')
-
-    Make sure it triggered a deprecation warning:
-
-      >>> len(whook.warnings)
-      1
-      >>> message, category, filename, lineno = whook.warnings[0]
-      >>> print message
-      This will be removed in ZODB 3.8:
-      Use addBeforeCommitHook instead of beforeCommitHook.
-      >>> category.__name__
-      'DeprecationWarning'
-      >>> whook.clear()
-
-    We can see that the hook is indeed registered.
-
-      >>> [(hook.func_name, args, kws)
-      ...  for hook, args, kws in t.getBeforeCommitHooks()]
-      [('hook', ('1',), {})]
-
-    When transaction commit starts, the hook is called, with its
-    arguments.
-
-      >>> log
-      []
-      >>> t.commit()
-      >>> log
-      ["arg '1' kw1 'no_kw1' kw2 'no_kw2'"]
-      >>> reset_log()
-
-    A hook's registration is consumed whenever the hook is called.  Since
-    the hook above was called, it's no longer registered:
-
-      >>> len(list(t.getBeforeCommitHooks()))
-      0
-      >>> transaction.commit()
-      >>> log
-      []
-
-    The hook is only called for a full commit, not for a savepoint.
-
-      >>> t = transaction.begin()
-      >>> t.beforeCommitHook(hook, 'A', kw1='B')
-      >>> dummy = t.savepoint()
-      >>> log
-      []
-      >>> t.commit()
-      >>> log
-      ["arg 'A' kw1 'B' kw2 'no_kw2'"]
-      >>> reset_log()
-
-    If a transaction is aborted, no hook is called.
-
-      >>> t = transaction.begin()
-      >>> t.beforeCommitHook(hook, "OOPS!")
-      >>> transaction.abort()
-      >>> log
-      []
-      >>> transaction.commit()
-      >>> log
-      []
-
-    The hook is called before the commit does anything, so even if the
-    commit fails the hook will have been called.  To provoke failures in
-    commit, we'll add failing resource manager to the transaction.
-
-      >>> class CommitFailure(Exception):
-      ...     pass
-      >>> class FailingDataManager:
-      ...     def tpc_begin(self, txn, sub=False):
-      ...         raise CommitFailure
-      ...     def abort(self, txn):
-      ...         pass
-
-      >>> t = transaction.begin()
-      >>> t.join(FailingDataManager())
-
-      >>> t.beforeCommitHook(hook, '2')
-      >>> t.commit()
-      Traceback (most recent call last):
-      ...
-      CommitFailure
-      >>> log
-      ["arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
-      >>> reset_log()
-
-    Let's register several hooks.
-
-      >>> t = transaction.begin()
-      >>> t.beforeCommitHook(hook, '4', kw1='4.1')
-      >>> t.beforeCommitHook(hook, '5', kw2='5.2')
-
-    They are returned in the same order by getBeforeCommitHooks.
-
-      >>> [(hook.func_name, args, kws)     #doctest: +NORMALIZE_WHITESPACE
-      ...  for hook, args, kws in t.getBeforeCommitHooks()]
-      [('hook', ('4',), {'kw1': '4.1'}),
-       ('hook', ('5',), {'kw2': '5.2'})]
-
-    And commit also calls them in this order.
-
-      >>> t.commit()
-      >>> len(log)
-      2
-      >>> log  #doctest: +NORMALIZE_WHITESPACE
-      ["arg '4' kw1 '4.1' kw2 'no_kw2'",
-       "arg '5' kw1 'no_kw1' kw2 '5.2'"]
-      >>> reset_log()
-
-    While executing, a hook can itself add more hooks, and they will all
-    be called before the real commit starts.
-
-      >>> def recurse(txn, arg):
-      ...     log.append('rec' + str(arg))
-      ...     if arg:
-      ...         txn.beforeCommitHook(hook, '-')
-      ...         txn.beforeCommitHook(recurse, txn, arg-1)
-
-      >>> t = transaction.begin()
-      >>> t.beforeCommitHook(recurse, t, 3)
-      >>> transaction.commit()
-      >>> log  #doctest: +NORMALIZE_WHITESPACE
-      ['rec3',
-               "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
-       'rec2',
-               "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
-       'rec1',
-               "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
-       'rec0']
-      >>> reset_log()
-
-    We have to uninstall the warnings hook so that other warnings don't get
-    lost.
-
-      >>> whook.uninstall()
-
-    Obscure:  There is no API call for removing the filter we added, but
-    filters appears to be a public variable.
-
-      >>> del warnings.filters[0]
-    """
-
 def test_addBeforeCommitHook():
     """Test addBeforeCommitHook.
 
@@ -548,7 +386,7 @@
 
     Now register the hook with a transaction.
 
-      >>> import transaction
+      >>> from zope import transaction
       >>> t = transaction.begin()
       >>> t.addBeforeCommitHook(hook, '1')
 
@@ -722,7 +560,7 @@
 
     Now register the hook with a transaction.
 
-      >>> import transaction
+      >>> from zope import transaction
       >>> t = transaction.begin()
       >>> t.addAfterCommitHook(hook, '1')
 
@@ -936,5 +774,8 @@
         unittest.makeSuite(TransactionTests),
         ))
 
+# additional_tests is for setuptools "setup.py test" support
+additional_tests = test_suite
+
 if __name__ == '__main__':
     unittest.TextTestRunner().run(test_suite())

Added: zope.transaction/trunk/zope/transaction/tests/test_weakset.py
===================================================================
--- zope.transaction/trunk/zope/transaction/tests/test_weakset.py	                        (rev 0)
+++ zope.transaction/trunk/zope/transaction/tests/test_weakset.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -0,0 +1,80 @@
+##############################################################################
+#
+# Copyright (c) 2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+import unittest
+from zope.transaction.weakset import WeakSet
+
+class Dummy:
+    pass
+
+class WeakSetTests(unittest.TestCase):
+    def test_contains(self):
+        w = WeakSet()
+        dummy = Dummy()
+        w.add(dummy)
+        self.assertEqual(dummy in w, True)
+        dummy2 = Dummy()
+        self.assertEqual(dummy2 in w, False)
+
+    def test_len(self):
+        w = WeakSet()
+        d1 = Dummy()
+        d2 = Dummy()
+        w.add(d1)
+        w.add(d2)
+        self.assertEqual(len(w), 2)
+        del d1
+        self.assertEqual(len(w), 1)
+
+    def test_remove(self):
+        w = WeakSet()
+        dummy = Dummy()
+        w.add(dummy)
+        self.assertEqual(dummy in w, True)
+        w.remove(dummy)
+        self.assertEqual(dummy in w, False)
+
+    def test_as_weakref_list(self):
+        w = WeakSet()
+        dummy = Dummy()
+        dummy2 = Dummy()
+        dummy3 = Dummy()
+        w.add(dummy)
+        w.add(dummy2)
+        w.add(dummy3)
+        del dummy3
+        L = [x() for x in w.as_weakref_list()]
+        self.assertEqual(L, [dummy, dummy2])
+
+    def test_map(self):
+        w = WeakSet()
+        dummy = Dummy()
+        dummy2 = Dummy()
+        dummy3 = Dummy()
+        w.add(dummy)
+        w.add(dummy2)
+        w.add(dummy3)
+        def poker(x):
+            x.poked = 1
+        w.map(poker)
+        for thing in dummy, dummy2, dummy3:
+            self.assertEqual(thing.poked, 1)
+        
+        
+def test_suite():
+    return unittest.makeSuite(WeakSetTests)
+
+if __name__ == '__main__':
+    unittest.main()
+    


Property changes on: zope.transaction/trunk/zope/transaction/tests/test_weakset.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zope.transaction/trunk/zope/transaction/tests/warnhook.py
===================================================================
--- zope.transaction/trunk/zope/transaction/tests/warnhook.py	                        (rev 0)
+++ zope.transaction/trunk/zope/transaction/tests/warnhook.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -0,0 +1,57 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+import warnings
+
+class WarningsHook:
+    """Hook to capture warnings generated by Python.
+
+    The function warnings.showwarning() is designed to be hooked by
+    application code, allowing the application to customize the way it
+    handles warnings.
+
+    This hook captures the unformatted warning information and stores
+    it in a list.  A test can inspect this list after the test is over.
+
+    Issues:
+
+    The warnings module has lots of delicate internal state.  If
+    a warning has been reported once, it won't be reported again.  It
+    may be necessary to extend this class with a mechanism for
+    modifying the internal state so that we can be guaranteed a
+    warning will be reported.
+
+    If Python is run with a warnings filter, e.g. python -Werror,
+    then a test that is trying to inspect a particular warning will
+    fail.  Perhaps this class can be extended to install more-specific
+    filters the test to work anyway.
+    """
+
+    def __init__(self):
+        self.original = None
+        self.warnings = []
+
+    def install(self):
+        self.original = warnings.showwarning
+        warnings.showwarning = self.showwarning
+
+    def uninstall(self):
+        assert self.original is not None
+        warnings.showwarning = self.original
+        self.original = None
+
+    def showwarning(self, message, category, filename, lineno):
+        self.warnings.append((str(message), category, filename, lineno))
+
+    def clear(self):
+        self.warnings = []


Property changes on: zope.transaction/trunk/zope/transaction/tests/warnhook.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zope.transaction/trunk/zope/transaction/weakset.py
===================================================================
--- zope.transaction/trunk/zope/transaction/weakset.py	                        (rev 0)
+++ zope.transaction/trunk/zope/transaction/weakset.py	2007-11-09 00:58:54 UTC (rev 81628)
@@ -0,0 +1,81 @@
+############################################################################
+#
+# Copyright (c) 2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+############################################################################
+
+import weakref
+
+# A simple implementation of weak sets, supplying just enough of Python's
+# sets.Set interface for our needs.
+
+class WeakSet(object):
+    """A set of objects that doesn't keep its elements alive.
+
+    The objects in the set must be weakly referencable.
+    The objects need not be hashable, and need not support comparison.
+    Two objects are considered to be the same iff their id()s are equal.
+
+    When the only references to an object are weak references (including
+    those from WeakSets), the object can be garbage-collected, and
+    will vanish from any WeakSets it may be a member of at that time.
+    """
+
+    def __init__(self):
+        # Map id(obj) to obj.  By using ids as keys, we avoid requiring
+        # that the elements be hashable or comparable.
+        self.data = weakref.WeakValueDictionary()
+
+    def __len__(self):
+        return len(self.data)
+
+    def __contains__(self, obj):
+        return id(obj) in self.data
+
+    # Same as a Set, add obj to the collection.
+    def add(self, obj):
+        self.data[id(obj)] = obj
+
+    # Same as a Set, remove obj from the collection, and raise
+    # KeyError if obj not in the collection.
+    def remove(self, obj):
+        del self.data[id(obj)]
+
+    # f is a one-argument function.  Execute f(elt) for each elt in the
+    # set.  f's return value is ignored.
+    def map(self, f):
+        for wr in self.as_weakref_list():
+            elt = wr()
+            if elt is not None:
+                f(elt)
+
+    # Return a list of weakrefs to all the objects in the collection.
+    # Because a weak dict is used internally, iteration is dicey (the
+    # underlying dict may change size during iteration, due to gc or
+    # activity from other threads).  as_weakef_list() is safe.
+    #
+    # Something like this should really be a method of Python's weak dicts.
+    # If we invoke self.data.values() instead, we get back a list of live
+    # objects instead of weakrefs.  If gc occurs while this list is alive,
+    # all the objects move to an older generation (because they're strongly
+    # referenced by the list!).  They can't get collected then, until a
+    # less frequent collection of the older generation.  Before then, if we
+    # invoke self.data.values() again, they're still alive, and if gc occurs
+    # while that list is alive they're all moved to yet an older generation.
+    # And so on.  Stress tests showed that it was easy to get into a state
+    # where a WeakSet grows without bounds, despite that almost all its
+    # elements are actually trash.  By returning a list of weakrefs instead,
+    # we avoid that, although the decision to use weakrefs is now# very
+    # visible to our clients.
+    def as_weakref_list(self):
+        # We're cheating by breaking into the internals of Python's
+        # WeakValueDictionary here (accessing its .data attribute).
+        return self.data.data.values()


Property changes on: zope.transaction/trunk/zope/transaction/weakset.py
___________________________________________________________________
Name: svn:eol-style
   + native



More information about the Checkins mailing list