[Checkins] SVN: z3c.sharding/ Added a ZODB container type that stores its contents in multiple

Shane Hathaway shane at hathawaymix.org
Wed Dec 10 15:26:47 EST 2008


Log message for revision 93853:
  Added a ZODB container type that stores its contents in multiple 
  databases automatically.
  

Changed:
  A   z3c.sharding/
  A   z3c.sharding/.installed.cfg
  A   z3c.sharding/CHANGES.txt
  A   z3c.sharding/README.txt
  A   z3c.sharding/bootstrap.py
  A   z3c.sharding/buildout.cfg
  A   z3c.sharding/setup.py
  A   z3c.sharding/src/
  A   z3c.sharding/src/z3c/
  A   z3c.sharding/src/z3c/__init__.py
  A   z3c.sharding/src/z3c/sharding/
  A   z3c.sharding/src/z3c/sharding/README.txt
  A   z3c.sharding/src/z3c/sharding/__init__.py
  A   z3c.sharding/src/z3c/sharding/apidoc.zcml
  A   z3c.sharding/src/z3c/sharding/container.py
  A   z3c.sharding/src/z3c/sharding/interfaces.py
  A   z3c.sharding/src/z3c/sharding/tests.py

-=-
Added: z3c.sharding/.installed.cfg
===================================================================
--- z3c.sharding/.installed.cfg	                        (rev 0)
+++ z3c.sharding/.installed.cfg	2008-12-10 20:26:47 UTC (rev 93853)
@@ -0,0 +1,33 @@
+[buildout]
+installed_develop_eggs = /home/shane/src/z3c.sharding/develop-eggs/z3c.sharding.egg-link
+parts = test coverage
+
+[test]
+__buildout_installed__ = /home/shane/src/z3c.sharding/parts/test
+	/home/shane/src/z3c.sharding/bin/test
+__buildout_signature__ = zc.recipe.testrunner-1.1.0-py2.5.egg zc.recipe.egg-1.1.0-py2.5.egg setuptools-5UNm9gjHTOWl6Jm0S8SffQ== zope.testing-3.7.1-py2.5.egg zc.buildout-1.1.1-py2.5.egg zc.buildout-1.1.1-py2.5.egg zope.interface-3.5.0-py2.5-linux-x86_64.egg
+_b = /home/shane/src/z3c.sharding/bin
+_d = /home/shane/src/z3c.sharding/develop-eggs
+_e = /home/shane/.buildout/eggs
+bin-directory = /home/shane/src/z3c.sharding/bin
+develop-eggs-directory = /home/shane/src/z3c.sharding/develop-eggs
+eggs = z3c.sharding [test]
+eggs-directory = /home/shane/.buildout/eggs
+executable = /usr/bin/python
+location = /home/shane/src/z3c.sharding/parts/test
+recipe = zc.recipe.testrunner
+script = /home/shane/src/z3c.sharding/bin/test
+
+[coverage]
+__buildout_installed__ = /home/shane/src/z3c.sharding/bin/coverage
+	/home/shane/src/z3c.sharding/bin/coveragediff
+__buildout_signature__ = zc.recipe.egg-1.1.0-py2.5.egg setuptools-5UNm9gjHTOWl6Jm0S8SffQ== zc.buildout-1.1.1-py2.5.egg
+_b = /home/shane/src/z3c.sharding/bin
+_d = /home/shane/src/z3c.sharding/develop-eggs
+_e = /home/shane/.buildout/eggs
+bin-directory = /home/shane/src/z3c.sharding/bin
+develop-eggs-directory = /home/shane/src/z3c.sharding/develop-eggs
+eggs = z3c.coverage
+eggs-directory = /home/shane/.buildout/eggs
+executable = /usr/bin/python
+recipe = zc.recipe.egg

Added: z3c.sharding/CHANGES.txt
===================================================================
--- z3c.sharding/CHANGES.txt	                        (rev 0)
+++ z3c.sharding/CHANGES.txt	2008-12-10 20:26:47 UTC (rev 93853)
@@ -0,0 +1,8 @@
+=======
+CHANGES
+=======
+
+Version 0.1.0 (2008-??-??)
+--------------------------
+
+- Initial Release

Added: z3c.sharding/README.txt
===================================================================
--- z3c.sharding/README.txt	                        (rev 0)
+++ z3c.sharding/README.txt	2008-12-10 20:26:47 UTC (rev 93853)
@@ -0,0 +1,3 @@
+This package provides a database sharding API based on persistent
+multi-database references.  A more complete description is provided
+in src/z3c/sharding/README.txt.

Added: z3c.sharding/bootstrap.py
===================================================================
--- z3c.sharding/bootstrap.py	                        (rev 0)
+++ z3c.sharding/bootstrap.py	2008-12-10 20:26:47 UTC (rev 93853)
@@ -0,0 +1,52 @@
+##############################################################################
+#
+# 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.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+
+$Id: bootstrap.py 17 2008-02-27 09:29:05Z srichter $
+"""
+
+import os, shutil, sys, tempfile, urllib2
+
+tmpeggs = tempfile.mkdtemp()
+
+ez = {}
+exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
+                     ).read() in ez
+ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
+
+import pkg_resources
+
+cmd = 'from setuptools.command.easy_install import main; main()'
+if sys.platform == 'win32':
+    cmd = '"%s"' % cmd # work around spawn lamosity on windows
+
+ws = pkg_resources.working_set
+assert os.spawnle(
+    os.P_WAIT, sys.executable, sys.executable,
+    '-c', cmd, '-mqNxd', tmpeggs, 'zc.buildout',
+    dict(os.environ,
+         PYTHONPATH=
+         ws.find(pkg_resources.Requirement.parse('setuptools')).location
+         ),
+    ) == 0
+
+ws.add_entry(tmpeggs)
+ws.require('zc.buildout')
+import zc.buildout.buildout
+zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
+shutil.rmtree(tmpeggs)

Added: z3c.sharding/buildout.cfg
===================================================================
--- z3c.sharding/buildout.cfg	                        (rev 0)
+++ z3c.sharding/buildout.cfg	2008-12-10 20:26:47 UTC (rev 93853)
@@ -0,0 +1,11 @@
+[buildout]
+develop = .
+parts = test coverage
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = z3c.sharding [test]
+
+[coverage]
+recipe = zc.recipe.egg
+eggs = z3c.coverage

Added: z3c.sharding/setup.py
===================================================================
--- z3c.sharding/setup.py	                        (rev 0)
+++ z3c.sharding/setup.py	2008-12-10 20:26:47 UTC (rev 93853)
@@ -0,0 +1,48 @@
+##############################################################################
+#
+# Copyright (c) 2008 Zope Foundation 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.
+#
+##############################################################################
+"""Package setup.
+
+$Id: setup.py 17 2008-02-27 09:29:05Z srichter $
+"""
+import os
+from setuptools import setup, find_packages
+
+def read(*rnames):
+    return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
+
+setup(
+    name='z3c.sharding',
+    version = '0.1.0-dev',
+    author='Keas, Inc.',
+    description='Database Sharding for ZODB',
+    long_description=(
+        read('README.txt')
+        + '\n\n' +
+        read('CHANGES.txt')
+        ),
+    license = "ZPL-2.1",
+    keywords = "ZODB sharding",
+    packages=find_packages('src'),
+    package_dir = {'': 'src'},
+    namespace_packages = ['z3c'],
+    extras_require=dict(
+        test=['zope.app.testing',
+              'zope.testing',],
+        ),
+    install_requires=[
+        'setuptools',
+        ],
+    include_package_data = True,
+    zip_safe = False,
+    )

Added: z3c.sharding/src/z3c/__init__.py
===================================================================
--- z3c.sharding/src/z3c/__init__.py	                        (rev 0)
+++ z3c.sharding/src/z3c/__init__.py	2008-12-10 20:26:47 UTC (rev 93853)
@@ -0,0 +1,7 @@
+# this is a namespace package
+try:
+    import pkg_resources
+    pkg_resources.declare_namespace(__name__)
+except ImportError:
+    import pkgutil
+    __path__ = pkgutil.extend_path(__path__, __name__)

Added: z3c.sharding/src/z3c/sharding/README.txt
===================================================================
--- z3c.sharding/src/z3c/sharding/README.txt	                        (rev 0)
+++ z3c.sharding/src/z3c/sharding/README.txt	2008-12-10 20:26:47 UTC (rev 93853)
@@ -0,0 +1,159 @@
+===========================
+Container Database Sharding
+===========================
+
+This package provides a container that automatically partitions its contents
+across multiple databases.  Objects are spread between participating databases
+in order to reduce database load and increase scalability.
+
+This code puts a master index (a BTree) in the database holding the
+`ShardContainer` object.  The values in the master index are ZODB
+cross-database references to objects in partitions.  Partitions are
+`BTreeContainer` components.
+
+The `ShardContainer` chooses the partition for a new object based on a weighted
+random selection of partition names.  The partition names are configured in
+the `ShardContainer` instance.  `ShardContainer` expects the multi-database
+configuration to be set up ahead of time.
+
+See the tests below for further details.
+
+  >>> from zope.interface.verify import verifyClass
+  >>> from transaction import commit, abort
+  >>> from z3c.sharding.interfaces import IShardPartition, IShardContainer
+  >>> from z3c.sharding.container import Partition, ShardContainer
+  >>> verifyClass(IShardPartition, Partition)
+  True
+  >>> verifyClass(IShardContainer, ShardContainer)
+  True
+
+Manually create a partition and verify its representation.
+
+  >>> p1 = Partition('p1', 1.0)
+  >>> print p1
+  Partition('p1', 1.0)
+
+Create a multi-database out of DemoStorages.  A multi-database is a set of
+ZODB DB objects linked by a common 'databases' map.  Objects stored
+in multi-databases can freely cross database boundaries.
+
+  >>> from ZODB.DemoStorage import DemoStorage
+  >>> from ZODB.DB import DB
+  >>> databases = {}
+  >>> for name in ('p1', 'p2', 'p3'):
+  ...     db = DB(DemoStorage(), database_name=name, databases=databases)
+  >>> main_db = DB(DemoStorage(), database_name='main', databases=databases)
+  >>> conn = main_db.open()
+
+Create a ShardContainer named "things" with a shard name of "things000".
+The shard name must be globally unique, but the shard name
+can be distinct from the container's __name__ attribute.
+
+  >>> things = ShardContainer('things000')
+  >>> conn.root()['things'] = things
+  >>> commit()
+
+A partition is required before storing anything.
+
+  >>> from zope.app.container.btree import BTreeContainer
+  >>> things['x'] = BTreeContainer()
+  Traceback (most recent call last):
+  ...
+  AssertionError: No partitions defined
+
+Set up a partition for the "things" contianer.
+
+  >>> parts = []
+  >>> for name in ['p1']:
+  ...     parts.append(Partition(name, 0))
+  >>> things.partitions = parts
+
+Add 1 item to the container.  The item should be a `Persistent` obejct and
+should implement the `IContained` interface.
+
+  >>> thing = BTreeContainer()
+  >>> thing.attr = 'value'
+  >>> things['0'] = thing
+  >>> thing._p_jar.db().database_name
+  'p1'
+  >>> things._p_jar.db().database_name
+  'main'
+
+Commit.
+
+  >>> things._p_changed
+  True
+  >>> commit()
+  >>> thing._p_changed
+  False
+  >>> things._p_changed
+  False
+
+Verify it's possible to load the thing from a new connection.
+
+  >>> conn2 = main_db.open()
+  >>> things2 = conn2.root()['things']
+  >>> thing2 = things2['0']
+  >>> things2._p_changed
+  False
+  >>> thing2.attr
+  'value'
+  >>> thing2._p_changed
+  False
+  >>> thing2._p_jar.db().database_name
+  'p1'
+  >>> things2._p_jar.db().database_name
+  'main'
+  >>> conn2.close()
+
+Enable another partition.  Assign p1 a weight of 1 and p2 a weight of 0,
+but note that the manual weights will be ignored until automatic
+weighting is disabled.
+
+  >>> parts = []
+  >>> for name in ['p1', 'p2']:
+  ...     parts.append(Partition(name, 2 - int(name[1])))
+  >>> things.partitions = parts
+
+Add another object.  Automatic weighting should cause the new object to always
+land in the new partition.
+
+  >>> stuff = BTreeContainer()
+  >>> things['1'] = stuff
+  >>> stuff._p_jar.db().database_name
+  'p2'
+
+Apply manual weighting.  Partition `p1` has weight 1 and `p2` has weight 0,
+so `p1` should get everything.
+
+  >>> things.auto_weight = False
+  >>> stuff = BTreeContainer()
+  >>> things['2'] = stuff
+  >>> stuff._p_jar.db().database_name
+  'p1'
+
+Verify all of the objects can be found directly in the partitions.
+
+  >>> conn.get_connection('p1').root()['shards'].keys()
+  ['things000']
+  >>> conn.get_connection('p2').root()['shards'].keys()
+  ['things000']
+  >>> sorted(conn.get_connection('p1').root()['shards']['things000'].keys())
+  [u'0', u'2']
+  >>> sorted(conn.get_connection('p2').root()['shards']['things000'].keys())
+  [u'1']
+
+Delete an object from the shard container, which should cause
+the same object to be deleted from its partition.
+
+  >>> del things['0']
+  >>> sorted(conn.get_connection('p1').root()['shards']['things000'].keys())
+  [u'2']
+  >>> commit()
+
+Clean up.
+
+  >>> abort()
+  >>> conn.close()
+  >>> for database in databases.values():
+  ...     database.close()

Added: z3c.sharding/src/z3c/sharding/apidoc.zcml
===================================================================
--- z3c.sharding/src/z3c/sharding/apidoc.zcml	                        (rev 0)
+++ z3c.sharding/src/z3c/sharding/apidoc.zcml	2008-12-10 20:26:47 UTC (rev 93853)
@@ -0,0 +1,19 @@
+
+<configure xmlns="http://namespaces.zope.org/zope"
+           i18n_domain="zope">
+
+  <configure
+      xmlns:zcml="http://namespaces.zope.org/zcml"
+      xmlns:apidoc="http://namespaces.zope.org/apidoc"
+      zcml:condition="have apidoc">
+
+    <apidoc:bookchapter
+        id="z3c.sharding"
+        title="Container Database Sharding (z3c.sharding)"
+        doc_path="README.txt"
+        parent="z3c"
+        />
+
+  </configure>
+
+</configure>

Added: z3c.sharding/src/z3c/sharding/container.py
===================================================================
--- z3c.sharding/src/z3c/sharding/container.py	                        (rev 0)
+++ z3c.sharding/src/z3c/sharding/container.py	2008-12-10 20:26:47 UTC (rev 93853)
@@ -0,0 +1,192 @@
+##############################################################################
+#
+# Copyright (c) 2008 Zope Foundation 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.
+#
+##############################################################################
+"""ShardContainer implementation.
+"""
+
+import random
+import time
+
+from persistent import Persistent
+from persistent.list import PersistentList
+from persistent.mapping import PersistentMapping
+from zope.interface import implements
+from zope.app.container.btree import BTreeContainer
+from zope.proxy import removeAllProxies
+
+from z3c.sharding.interfaces import IShardPartition, IShardContainer
+
+
+class Partition(Persistent):
+    """Describes a partition in a shard"""
+    implements(IShardPartition)
+
+    def __init__(self, database_name, weight):
+        self.database_name = database_name
+        self.weight = weight
+
+    def __repr__(self):
+        return '%s(%r, %r)' % (
+            self.__class__.__name__, self.database_name, self.weight)
+
+
+class ShardContainer(BTreeContainer):
+    """A shard container partitions its contents in multiple databases."""
+    implements(IShardContainer)
+
+    _part_table_timeout = 60 * 60  # seconds
+
+    _v_part_table = None           # [Partition]
+    _v_part_table_expiration = 0   # time when _v_part_table expires
+
+    def __init__(self, shard_name):
+        super(ShardContainer, self).__init__()
+        self._shard_name = shard_name
+        self._partitions = ()  # (Partition,)
+        self._auto_weight = True
+
+    def set_partitions(self, partitions):
+        """Replace the partition list.
+
+        The partitions argument holds a list of IShardPartition objects.
+        """
+        for part in partitions:
+            assert IShardPartition.providedBy(part)
+        self._partitions = tuple(partitions)
+        self._v_part_table = None
+        self._get_part_table()  # test the new configuration
+
+    def get_partitions(self):
+        return self.partitions
+
+    partitions = property(get_partitions, set_partitions)
+
+    def set_auto_weight(self, auto_weight):
+        """Configure whether the automatic weighting feature is enabled.
+
+        If enabled, the weight of each partition is computed to balance
+        the size of the underlying databases.  If disabled, the weight
+        comes from each partition's weight attribute.
+        """
+        self._auto_weight = bool(auto_weight)
+        self._v_part_table = None
+
+    def get_auto_weight(self):
+        return self._auto_weight
+
+    auto_weight = property(get_auto_weight, set_auto_weight)
+
+    def _compute_weights(self):
+        """Returns [(Partition, weight)]."""
+        computed_weights = []  # [(partition, weight)]
+        if self._auto_weight:
+            # gather the current size of all partitions
+            sizes = {}
+            for part in self._partitions:
+                c = self._p_jar.get_connection(part.database_name)
+                size = c.db().getSize()  # returns a byte count
+                sizes[part.database_name] = size
+            max_size = 0
+            if sizes:
+                max_size = max(sizes.values())
+            if max_size <= 0:
+                # no sizes are known, so give all partitions the same weight
+                computed_weights = [(part,  0) for part in self._partitions]
+            else:
+                # assign partitions a weight that causes them to auto-balance
+                for part in self._partitions:
+                    weight = max_size - sizes[part.database_name]
+                    computed_weights.append((part, weight))
+        else:
+            # use the manually chosen weights
+            for part in self._partitions:
+                computed_weights = [(part,  part.weight)
+                                    for part in self._partitions]
+        return computed_weights
+
+    def _get_part_table(self):
+        """Return a list of Partitions to select from.
+
+        Partitions will often be listed more than once in order to give
+        them more weight.
+
+        The result is cached in self._v_part_table.
+        """
+        res = self._v_part_table
+        if res is not None and time.time() < self._v_part_table_expiration:
+            return self._v_part_table
+
+        if self._p_jar is None:
+            # Can't proceed until the container is stored in
+            # some database.
+            raise AssertionError(
+                "ShardContainer is not yet added to a database")
+
+        computed_weights = self._compute_weights()
+        total_weight = 0
+        for part, weight in computed_weights:
+            if weight > 0:
+                total_weight += weight
+        if not total_weight:
+            # no weights given, so give all partitions the same weight
+            part_table = [part for (part, weight) in computed_weights]
+        else:
+            # make a table with approx. 1000 entries.
+            scale = 1000.0 / total_weight
+            part_table = []
+            for part, weight in computed_weights:
+                count = int(scale * weight)
+                for i in xrange(count):
+                    part_table.append(part)
+
+        self._v_part_table = part_table
+        self._v_part_table_expiration = time.time() + self._part_table_timeout
+
+        return part_table
+
+    def _choose_container(self):
+        """Returns the container where a new object should be stored."""
+        part_table = self._get_part_table()
+        if not part_table:
+            raise AssertionError("No partitions defined")
+        part = random.choice(part_table)
+        jar = self._p_jar.get_connection(part.database_name)
+        root = jar.root()
+        o1 = root.get('shards')
+        if o1 is None:
+            root['shards'] = o1 = PersistentMapping()
+            jar.add(o1)
+        o2 = o1.get(self._shard_name)
+        if o2 is None:
+            o1[self._shard_name] = o2 = BTreeContainer()
+            jar.add(o2)
+        return o2
+
+    def __setitem__(self, key, value):
+        if value._p_jar is not None:
+            raise ValueError("Contained object is already stored")
+        super(ShardContainer, self).__setitem__(key, value)
+        f = self._choose_container()
+        f[key] = value
+        f._p_jar.add(value)
+
+    def __delitem__(self, key):
+        obj = self._SampleContainer__data[key]
+        obj = removeAllProxies(obj)
+        conn = obj._p_jar
+        partition = conn.root()['shards'][self._shard_name]
+        # Avoid triggering an event involving the partition by deleting
+        # from the partition's underlying BTree.
+        tree = partition._SampleContainer__data
+        super(ShardContainer, self).__delitem__(key)
+        del tree[key]

Added: z3c.sharding/src/z3c/sharding/interfaces.py
===================================================================
--- z3c.sharding/src/z3c/sharding/interfaces.py	                        (rev 0)
+++ z3c.sharding/src/z3c/sharding/interfaces.py	2008-12-10 20:26:47 UTC (rev 93853)
@@ -0,0 +1,54 @@
+##############################################################################
+#
+# Copyright (c) 2008 Zope Foundation 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.
+#
+##############################################################################
+"""Shard Database Interfaces
+
+$Id:$
+"""
+import zope.interface
+from zope import schema
+from zope.app.container.interfaces import IContainer
+
+# set up internationalization
+import zope.i18nmessageid
+_ = zope.i18nmessageid.MessageFactory("zope")
+
+class IShardPartition(zope.interface.Interface):
+    """Represents a database back-end"""
+
+    database_name = schema.TextLine(
+        title=u'Database name',
+        description=u'The name of the database to access',
+        required=True)
+
+    weight = schema.Float(
+        title=u'Weight',
+        description=u'How much of the hash space to assign to this database',
+        required=True,
+        default=1.0)
+
+
+class IShardContainer(IContainer):
+    """A container that partitions its contents in multiple databases"""
+
+    partitions = zope.interface.Attribute(
+        "A tuple of IShardPartition objects describing where to store objects")
+
+    auto_weight = schema.Bool(
+        title=u'Automatic Weighting',
+        description=
+        u"""If true, partition weights are chosen automatically to balance
+        the available storage space.  If false, weights come from the weight
+        attribute of each IShardPartition.""",
+        required=True,
+        default=True)

Added: z3c.sharding/src/z3c/sharding/tests.py
===================================================================
--- z3c.sharding/src/z3c/sharding/tests.py	                        (rev 0)
+++ z3c.sharding/src/z3c/sharding/tests.py	2008-12-10 20:26:47 UTC (rev 93853)
@@ -0,0 +1,37 @@
+##############################################################################
+#
+# Copyright (c) 2008 Zope Foundation 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 Setup.
+
+$Id: tests.py 165 2008-03-12 00:09:25Z pcardune $
+"""
+import unittest
+from zope.testing import doctestunit, doctest
+#from zope.app.testing import placelesssetup
+
+# set up internationalization
+#import zope.i18nmessageid
+#_ = zope.i18nmessageid.MessageFactory("zope")
+
+def test_suite():
+    return unittest.TestSuite((
+        doctestunit.DocFileSuite(
+            'README.txt',
+            #'implementation.txt',
+            #setUp=placelesssetup.setUp, tearDown=placelesssetup.tearDown,
+            optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+            ),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')



More information about the Checkins mailing list