[Checkins] SVN: z3c.vcsync/ Initial import.

Martijn Faassen faassen at infrae.com
Mon Jun 25 13:36:05 EDT 2007


Log message for revision 77071:
  Initial import.
  

Changed:
  A   z3c.vcsync/
  A   z3c.vcsync/README.txt
  A   z3c.vcsync/buildout.cfg
  A   z3c.vcsync/setup.py
  A   z3c.vcsync/src/
  A   z3c.vcsync/src/z3c/
  A   z3c.vcsync/src/z3c/__init__.py
  A   z3c.vcsync/src/z3c/vcsync/
  A   z3c.vcsync/src/z3c/vcsync/README.txt
  A   z3c.vcsync/src/z3c/vcsync/__init__.py
  A   z3c.vcsync/src/z3c/vcsync/configure.zcml
  A   z3c.vcsync/src/z3c/vcsync/interfaces.py
  A   z3c.vcsync/src/z3c/vcsync/svn.py
  A   z3c.vcsync/src/z3c/vcsync/tests.py
  A   z3c.vcsync/src/z3c/vcsync/vc.py
  A   z3c.vcsync/svn-commit.tmp

-=-
Added: z3c.vcsync/README.txt
===================================================================
--- z3c.vcsync/README.txt	                        (rev 0)
+++ z3c.vcsync/README.txt	2007-06-25 17:36:04 UTC (rev 77071)
@@ -0,0 +1,17 @@
+Takes an arbitrary object and syncs it through SVN.
+
+This means a serialization to text, and a deserialization from text.
+
+The text is stored in a SVN checkout. Newly appeared texts are svn added,
+removed texts are svn removed. svn move and svn copy are not supported. They
+will instead cause a svn delete/svn add combination.
+
+An svn up can be performed. Any conflicting files will be noted and
+can be resolved (automatically?).
+
+An svn up always takes place before an svn commit.
+
+An svn sync from the server will main all files that have changed will
+be updated in the ZODB, and all files that have been deleted will be
+removed in the ZODB. Added files will be added in the ZODB.
+

Added: z3c.vcsync/buildout.cfg
===================================================================
--- z3c.vcsync/buildout.cfg	                        (rev 0)
+++ z3c.vcsync/buildout.cfg	2007-06-25 17:36:04 UTC (rev 77071)
@@ -0,0 +1,15 @@
+[buildout]
+develop = . grok martian
+parts = test devpython
+
+[test]
+recipe = zc.recipe.testrunner
+extra-paths=/home/faassen/buildout/z331-lp/lib/python
+eggs = z3c.vcsync
+
+# installs bin/devpython to do simple interpreter tests
+[devpython]
+recipe = zc.recipe.egg
+interpreter = devpython
+eggs = z3c.vcsync
+

Added: z3c.vcsync/setup.py
===================================================================
--- z3c.vcsync/setup.py	                        (rev 0)
+++ z3c.vcsync/setup.py	2007-06-25 17:36:04 UTC (rev 77071)
@@ -0,0 +1,19 @@
+from setuptools import setup, find_packages
+import sys, os
+
+setup(name='z3c.vcsync',
+      version='0.1',
+      description="Sync ZODB data with version control system, currently SVN",
+      package_dir={'': 'src'},
+      packages=find_packages('src'),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+        'setuptools',
+        'grok',
+        'py',
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      """,
+      )

Added: z3c.vcsync/src/z3c/__init__.py
===================================================================
--- z3c.vcsync/src/z3c/__init__.py	                        (rev 0)
+++ z3c.vcsync/src/z3c/__init__.py	2007-06-25 17:36:04 UTC (rev 77071)
@@ -0,0 +1 @@
+#

Added: z3c.vcsync/src/z3c/vcsync/README.txt
===================================================================
--- z3c.vcsync/src/z3c/vcsync/README.txt	                        (rev 0)
+++ z3c.vcsync/src/z3c/vcsync/README.txt	2007-06-25 17:36:04 UTC (rev 77071)
@@ -0,0 +1,568 @@
+Version Control Synchronization
+===============================
+
+This package contains code that helps with handling synchronization of
+persistent content with a version control system. This can be useful
+in software that needs to be able to work offline. The web application
+runs on a user's laptop that may be away from an internet
+connection. When connected again, the user syncs with a version
+control server, receiving updates that may have been made by others,
+and committing their own changes.
+
+The synchronization sequence is as follows (example given with SVN as
+the version control system):
+ 
+  1) save persistent state to svn checkout on the same machine as the
+     Zope application.
+
+  2) ``svn up``. Subversion merges in changed made by others users
+     that were checked into the svn server.
+
+  3) Any svn conflicts are automatically resolved.
+
+  4) reload changes in svn checkout into persistent Python objects
+
+  5) ``svn commit``.
+
+This is all happening in a single step. It can happen over and over
+again in a reasonably safe manner, as after the synchronization has
+concluded, the state of the persistent objects and that of the local
+SVN checkout will always be perfectly in sync.
+
+SVN difficulties
+----------------
+
+Changing a file into a directory with SVN requires the following
+procedure::
+  
+  * svn remove file
+
+  * svn commit file
+
+  * svn up
+
+  * mdkir file
+
+  * svn add file
+
+If during the serialization procedure a file changed into a directory,
+it would require an ``svn up`` to be issued during step 1. This is too
+early. As we see later, we instead ask the application developer to
+avoid this situation altogether.
+
+To start
+--------
+
+Let's first grok this package::
+
+  >>> import grok
+  >>> grok.grok('z3c.vcsync')
+
+Serialization
+-------------
+
+In order to export content to a version control system, it first needs
+to be possible to serialize a content object to a text representation.
+
+For the purposes of this document, we have defined a simple item that
+just carries an integer payload attribute::
+
+  >>> class Item(object):
+  ...   def __init__(self, payload):
+  ...     self.payload = payload
+  >>> item = Item(payload=1)
+  >>> item.payload
+  1
+
+We will use an ISerializer adapter to serialize it to a file. Let's
+define the adapter::
+
+  >>> from z3c.vcsync.interfaces import ISerializer
+  >>> class ItemSerializer(grok.Adapter):
+  ...     grok.provides(ISerializer)
+  ...     grok.context(Item)
+  ...     def serialize(self, f):
+  ...         f.write(str(self.context.payload))
+  ...         f.write('\n')
+  ...     def name(self):
+  ...         return self.context.__name__ + '.test'
+
+Let's test our adapter::
+
+  >>> from StringIO import StringIO
+  >>> f= StringIO()
+  >>> ItemSerializer(item).serialize(f)
+  >>> f.getvalue()
+  '1\n'
+
+Let's register the adapter::
+
+  >>> grok.grok_component('ItemSerializer', ItemSerializer)
+  True
+
+We can now use the adapter::
+
+  >>> f = StringIO()
+  >>> ISerializer(item).serialize(f)
+  >>> f.getvalue()
+  '1\n'
+
+Export persistent state to version control system checkout
+----------------------------------------------------------
+
+As part of the synchronization procedure we need the ability to export
+persistent python objects to the version control checkout directory in
+the form of files and directories. 
+ 
+Content is assumed to consist of two types of objects:
+
+* containers. These are represented as directories on the filesystem.
+
+* items. These are represented as files on the filesystem. The files
+  will have an extension to indicate the type of item.
+
+Let's imagine we have this object structure consisting of a container
+with some items and sub-containers in it::
+
+  >>> data = Container()
+  >>> data.__name__ = 'root'
+  >>> data['foo'] = Item(payload=1)
+  >>> data['bar'] = Item(payload=2)
+  >>> data['sub'] = Container()
+  >>> data['sub']['qux'] = Item(payload=3)
+
+This object structure has some test payload data::
+
+  >>> data['foo'].payload
+  1
+  >>> data['sub']['qux'].payload
+  3
+
+We have a checkout in testpath on the filesystem::
+
+  >>> testpath = create_test_dir()
+  >>> checkout = TestCheckout(testpath)
+
+The object structure can now be saved into that checkout::
+
+  >>> checkout.save(data)
+
+The filesystem should now contain the right objects.
+
+Everything is always saved in a directory called ``root``:
+ 
+  >>> root = testpath.join('root')
+  >>> root.check(dir=True)
+  True
+
+This root directory should contain the right objects::
+
+  >>> sorted([entry.basename for entry in root.listdir()])
+  ['bar.test', 'foo.test', 'sub']
+
+We expect the right contents in ``bar.test`` and ``foo.test``::
+
+  >>> root.join('bar.test').read()
+  '2\n'
+  >>> root.join('foo.test').read()
+  '1\n'
+
+``sub`` is a container so should be represented as a directory::
+
+  >>> sub_path = root.join('sub')
+  >>> sub_path.check(dir=True)
+  True
+
+  >>> sorted([entry.basename for entry in sub_path.listdir()])
+  ['qux.test']
+
+  >>> sub_path.join('qux.test').read()
+  '3\n'
+
+We know that no existing files or directories were deleted by this save,
+as the checkout was empty before this::
+
+  >>> checkout.deleted_by_save()
+  []
+
+We also know that certain files have been added::
+
+  >>> rel_paths(checkout, checkout.added_by_save())
+  ['/root', '/root/bar.test', '/root/foo.test', '/root/sub', 
+   '/root/sub/qux.test']
+
+Modifying an existing checkout
+------------------------------
+
+Now let's assume that the version control checkout is that as
+generated by step 1a). We will bring it to its initial state first::
+
+  >>> checkout.clear()
+
+We will now change some data in the ZODB again to test whether we
+detect additions and deletions (we need to inform the version control
+system about these).
+
+Let's add ``hoi``::
+  
+  >>> data['hoi'] = Item(payload=4)
+
+And let's delete ``bar``::
+
+  >>> del data['bar']
+
+Let's save the object structure again to the same checkout::
+  
+  >>> checkout.save(data)
+
+The checkout will now know which files were added and deleted during
+the save::
+
+  >>> rel_paths(checkout, checkout.added_by_save())
+  ['/root/hoi.test']
+
+We also know which files got deleted::
+
+  >>> rel_paths(checkout, checkout.deleted_by_save())
+  ['/root/bar.test']
+
+Modifying an existing checkout, some edge cases
+-----------------------------------------------
+
+Let's take our checkout as one fully synched up again::
+
+  >>> checkout.clear()
+
+The ZODB has changed again.  Item 'hoi' has changed from an item into
+a container::
+
+  >>> data['hoi'] = Container()
+
+We put some things into the container::
+
+  >>> data['hoi']['something'] = Item(payload=15)
+
+We export again into the existing checkout (which still has 'hoi' as a
+file)::
+
+  >>> checkout.save(data)
+
+The file ``hoi.test`` should now be removed::
+
+  >>> rel_paths(checkout, checkout.deleted_by_save())
+  ['/root/hoi.test']
+
+And the directory ``hoi`` should now be added::
+
+  >>> rel_paths(checkout, checkout.added_by_save())
+  ['/root/hoi', '/root/hoi/something.test']
+
+Let's check the filesystem state::
+
+  >>> sorted([entry.basename for entry in root.listdir()])
+  ['foo.test', 'hoi', 'sub']
+
+We expect ``hoi`` to contain ``something.test``::
+
+  >>> hoi_path = root.join('hoi')
+  >>> something_path = hoi_path.join('something.test')
+  >>> something_path.read()
+  '15\n'
+
+Let's now consider the checkout synched up entirely again::
+
+  >>> checkout.clear()
+
+Let's now change the ZODB again and change the ``hoi`` container back
+into a file::
+
+  >>> data['hoi'] = Item(payload=16)
+  >>> checkout.save(data)
+
+The ``hoi`` directory (and everything in it, implicitly) is now
+deleted::
+
+  >>> rel_paths(checkout, checkout.deleted_by_save())
+  ['/root/hoi']
+
+We have added ``hoi.test``::
+
+  >>> rel_paths(checkout, checkout.added_by_save())
+  ['/root/hoi.test']
+
+We expect to see a ``hoi.test`` but no ``hoi`` directory anymore::
+
+  >>> sorted([entry.basename for entry in root.listdir()])
+  ['foo.test', 'hoi.test', 'sub']
+
+Let's be synched-up again::
+
+  >>> checkout.clear()
+
+Note: creating a container with the name ``hoi.test`` (using the
+``.test`` postfix) will lead to trouble now, as we already have a file
+``hoi.test``. ``svn`` doesn't allow a single-step replace of a file
+with a directory - as expressed earlier, an ``svn up`` would need to
+be issued first, but this would be too early in the process. Solving
+this problem is quite involved. Instead, we require the application to
+avoid creating any directories with a postfix in use by items. The
+following should be forbidden::
+
+  data['hoi.test'] = Container()
+
+loading a checkout state into python objects
+--------------------------------------------
+
+Let's load the currentfilesystem layout into python objects. Factories
+are registered as utilities for the different things we can encounter
+on the filesystem. Let's look at items first. A factory is registered
+for the ``.test`` extension::
+
+  >>> from z3c.vcsync.interfaces import IVcFactory
+  >>> class ItemFactory(grok.GlobalUtility):
+  ...   grok.provides(IVcFactory)
+  ...   grok.name('.test')
+  ...   def __call__(self, checkout, path):
+  ...       payload = int(path.read())
+  ...       return Item(payload)
+  >>> grok.grok_component('ItemFactory', ItemFactory)
+  True
+
+Now for containers. They are registered for an empty extension. They
+are also required to use VcLoad to load their contents::
+
+  >>> from z3c.vcsync.interfaces import IVcLoad
+  >>> class ContainerFactory(grok.GlobalUtility):
+  ...   grok.provides(IVcFactory)
+  ...   def __call__(self, checkout, path):
+  ...       container = Container()
+  ...       IVcLoad(container).load(checkout, path)
+  ...       return container
+  >>> grok.grok_component('ContainerFactory', ContainerFactory)
+  True
+
+We have registered enough. Let's load up the contents from the
+filesystem now::
+
+  >>> container2 = Container()
+  >>> container2.__name__ = 'root'
+  >>> checkout.load(container2)
+  >>> sorted(container2.keys())
+  ['foo', 'hoi', 'sub']
+
+We check whether the items contains the right information::
+
+  >>> isinstance(container2['foo'], Item)
+  True
+  >>> container2['foo'].payload
+  1
+  >>> isinstance(container2['hoi'], Item)
+  True
+  >>> container2['hoi'].payload
+  16
+  >>> isinstance(container2['sub'], Container)
+  True
+  >>> sorted(container2['sub'].keys())
+  ['qux']
+  >>> container2['sub']['qux'].payload
+  3
+
+version control changes a file
+------------------------------
+
+Now we synchronize our checkout by synchronizing the checkout with the
+central coordinating server (or shared branch in case of a distributed
+version control system). We do a ``checkout.up()`` that causes the
+text in a file to be modified.
+
+The special checkout class we use for example purposes will call
+``update_function`` during an update. This function should then
+simulate what might happen during a version control system ``update``
+operation. Let's define one here that modifies text in a file::
+
+  >>> hoi_path = root.join('hoi.test')
+  >>> def update_function():
+  ...    hoi_path.write('200\n')
+  >>> checkout.update_function = update_function
+
+  >>> checkout.up()
+
+We will reload the checkout into Python objects::
+
+  >>> checkout.load(container2)
+ 
+We expect the ``hoi`` object to be modified::
+
+  >>> container2['hoi'].payload
+  200
+
+
+version control adds a file
+---------------------------
+
+We update our checkout again and cause a file to be added::
+
+  >>> hallo = root.join('hallo.test').ensure()
+  >>> def update_function():
+  ...   hallo.write('300\n')
+  >>> checkout.update_function = update_function
+
+  >>> checkout.up()
+
+We will reload the checkout into Python objects again::
+
+  >>> checkout.load(container2)
+ 
+We expect there to be a new object ``hallo``::
+
+  >>> 'hallo' in container2.keys()
+  True
+
+version control removes a file
+------------------------------
+
+We update our checkout and cause a file to be removed::
+
+  >>> def update_function():
+  ...   root.join('hallo.test').remove()
+  >>> checkout.update_function = update_function
+
+  >>> checkout.up()
+
+We will reload the checkout into Python objects::
+
+  >>> checkout.load(container2)
+
+We expect the object ``hallo`` to be gone again::
+
+  >>> 'hallo' in container2.keys()
+  False
+
+version control adds a directory
+--------------------------------
+
+We update our checkout and cause a directory (with a file inside) to be
+added::
+
+  >>> newdir_path = root.join('newdir')
+  >>> def update_function():
+  ...   newdir_path.ensure(dir=True)
+  ...   newfile_path = newdir_path.join('newfile.test').ensure()
+  ...   newfile_path.write('400\n')
+  >>> checkout.update_function = update_function
+  
+  >>> checkout.up()
+
+Reloading this will cause a new container to exist::
+
+  >>> checkout.load(container2)
+
+  >>> 'newdir' in container2.keys()
+  True
+  >>> isinstance(container2['newdir'], Container)
+  True
+  >>> container2['newdir']['newfile'].payload
+  400
+
+version control removes a directory
+-----------------------------------
+
+We update our checkout once again and cause a directory to be removed::
+
+  >>> def update_function():
+  ...   newdir_path.remove()
+  >>> checkout.update_function = update_function
+
+  >>> checkout.up()
+
+  >>> checkout.load(container2)
+
+Reloading this will cause the new container to be gone again::
+
+  >>> checkout.load(container2)
+  >>> 'newdir' in container2.keys()
+  False
+
+version control changes a file into a directory
+-----------------------------------------------
+
+Some sequence of actions by other users has caused a name that previously
+referred to a file to now refer to a directory::
+
+  >>> hoi_path2 = root.join('hoi')
+  >>> def update_function():
+  ...   hoi_path.remove()
+  ...   hoi_path2.ensure(dir=True)
+  ...   some_path = hoi_path2.join('some.test').ensure(file=True)
+  ...   some_path.write('1000\n')
+  >>> checkout.update_function = update_function
+
+  >>> checkout.up()
+
+Reloading this will cause a new container to be there instead of the file::
+
+  >>> checkout.load(container2)
+  >>> isinstance(container2['hoi'], Container)
+  True
+  >>> container2['hoi']['some'].payload
+  1000
+
+version control changes a directory into a file
+-----------------------------------------------
+
+Some sequence of actions by other users has caused a name that
+previously referred to a directory to now refer to a file::
+
+  >>> def update_function():
+  ...   hoi_path2.remove()
+  ...   hoi_path = root.join('hoi.test').ensure()
+  ...   hoi_path.write('2000\n')
+  >>> checkout.update_function = update_function
+
+  >>> checkout.up()
+
+Reloading this will cause a new item to be there instead of the
+container::
+
+  >>> checkout.load(container2)
+  >>> isinstance(container2['hoi'], Item)
+  True
+  >>> container2['hoi'].payload
+  2000
+
+Complete synchronization
+------------------------
+
+Let's now exercise the ``sync`` method directly. First we'll modify
+the payload of the ``hoi`` item::
+
+  >>> container2['hoi'].payload = 3000
+ 
+Next, we willl add a new ``alpha`` file to the checkout when we do an
+``up()``, so again we simulate the actions of our version control system::
+
+  >>> def update_function():
+  ...   alpha_path = root.join('alpha.test').ensure()
+  ...   alpha_path.write('4000\n')
+  >>> checkout.update_function = update_function
+
+Now we'll synchronize with the memory structure::
+
+  >>> checkout.sync(container2)
+
+We expect the checkout to reflect the changed state of the ``hoi`` object::
+
+  >>> root.join('hoi.test').read()
+  '3000\n'
+
+We also expect the database to reflect the creation of the new
+``alpha`` object::
+
+  >>> container2['alpha'].payload
+  4000
+
+
+
+* are the right events being generated
+
+* check save of unicode names

Added: z3c.vcsync/src/z3c/vcsync/__init__.py
===================================================================
--- z3c.vcsync/src/z3c/vcsync/__init__.py	                        (rev 0)
+++ z3c.vcsync/src/z3c/vcsync/__init__.py	2007-06-25 17:36:04 UTC (rev 77071)
@@ -0,0 +1,2 @@
+#
+

Added: z3c.vcsync/src/z3c/vcsync/configure.zcml
===================================================================
--- z3c.vcsync/src/z3c/vcsync/configure.zcml	                        (rev 0)
+++ z3c.vcsync/src/z3c/vcsync/configure.zcml	2007-06-25 17:36:04 UTC (rev 77071)
@@ -0,0 +1,3 @@
+<configure xmlns="http://namespaces.zope.org/grok">
+  <grok package="." />
+</configure>

Added: z3c.vcsync/src/z3c/vcsync/interfaces.py
===================================================================
--- z3c.vcsync/src/z3c/vcsync/interfaces.py	                        (rev 0)
+++ z3c.vcsync/src/z3c/vcsync/interfaces.py	2007-06-25 17:36:04 UTC (rev 77071)
@@ -0,0 +1,104 @@
+from zope.interface import Interface
+
+class IVcDump(Interface):
+    def save(checkout, path):
+        """Save context object to path in checkout.
+
+        checkout - an ICheckout object
+        path - a py.path object referring to directory to save in.
+
+        This might result in the creation of a new file or directory under
+        the path, or alternatively to the modification of an existing file
+        or directory.
+
+        Returns the path just created.
+        """
+
+class IVcLoad(Interface):
+    def load(checkout, path):
+        """Load data in checkout's path into context object.
+        """
+
+class ISerializer(Interface):
+    def serialize(f):
+        """Serialize object to file object.
+        """
+
+class IParser(Interface):
+    def parse(f):
+        """Parse object and load it into new object, returning it.
+        """
+
+    def parse_into(f):
+        """Parse object and replace current object's content with it.
+        """
+
+class IVcFactory(Interface):
+    def __call__():
+        """Create new instance of object.
+        """
+    
+class IModified(Interface):
+    def modified_since(dt):
+        """Return True if the object has been modified since dt.
+        """
+
+    def update():
+        """Update modification datetime.
+        """
+
+class ICheckout(Interface):
+    """A version control system checkout.
+    """
+    def sync(object, message=''):
+        """Synchronize persistent Python state with remove version control.
+        """
+        
+    def save(object):
+        """Save root object to filesystem location of checkout.
+        """
+
+    def load(object):
+        """Load filesystem state of checkout into object.
+        """
+        
+    def up():
+        """Update the checkout with the state of the version control system.
+        """
+
+    def resolve():
+        """Resolve all conflicts that may be in the checkout.
+        """
+
+    def commit(message):
+        """Commit checkout to version control system.
+        """
+
+    def add(path):
+        """Add a file to the checkout (so it gets committed).
+        """
+
+    def delete(path):
+        """Delete a file from the checkout (so the delete gets committed).
+        """
+
+    def added_by_save():
+        """A list of files and directories that have been added by a save.
+        """
+
+    def deleted_by_save():
+        """A list of files and directories that have been deleted by a save.
+        """
+
+    def added_by_up():
+        """A list of those files that have been added after 'up'.
+        """
+
+    def deleted_by_up():
+        """A list of those files that have been deleted after 'up'.
+        """
+
+    def modified_by_up():
+        """A list of those files that have been modified after 'up'.
+        """
+    

Added: z3c.vcsync/src/z3c/vcsync/svn.py
===================================================================
--- z3c.vcsync/src/z3c/vcsync/svn.py	                        (rev 0)
+++ z3c.vcsync/src/z3c/vcsync/svn.py	2007-06-25 17:36:04 UTC (rev 77071)
@@ -0,0 +1,24 @@
+import py
+
+from z3c.vcsync.vc import CheckoutBase
+
+class SvnCheckout(CheckoutBase):
+    """A checkout for SVN.
+
+    This is a simplistic implementation. Advanced implementations
+    might check what has been changed in an SVN update and change the
+    load() method to only bother to load changed (or added or removed)
+    data. Similarly save() could be adjusted to only save changed
+    data.
+    
+    It is assumed to be initialized with py.path.svnwc
+    """
+    
+    def up(self):
+        self.path.update()
+
+    def resolve(self):
+        pass
+
+    def commit(self, message):
+        self.path.commit(message)

Added: z3c.vcsync/src/z3c/vcsync/tests.py
===================================================================
--- z3c.vcsync/src/z3c/vcsync/tests.py	                        (rev 0)
+++ z3c.vcsync/src/z3c/vcsync/tests.py	2007-06-25 17:36:04 UTC (rev 77071)
@@ -0,0 +1,105 @@
+import unittest
+from zope.testing import doctest, cleanup
+import tempfile
+import shutil
+import py.path
+from datetime import datetime
+import grok
+
+from zope.interface import implements, Interface
+from zope.app.container.interfaces import IContainer
+
+from z3c.vcsync.interfaces import ISerializer, IVcDump, IVcLoad, IVcFactory, IModified
+from z3c.vcsync import vc
+
+class TestCheckout(vc.CheckoutBase):
+    def __init__(self, path):
+        super(TestCheckout, self).__init__(path)
+        self.update_function = None
+
+    def up(self):
+        # call update_function which will modify the checkout as might
+        # happen in a version control update. Function should be set before
+        # calling this in testing code
+        self.update_function()
+
+    def resolve(self):
+        pass
+
+    def commit(self, message):
+        pass
+
+class Container(object):
+    implements(IContainer)
+    
+    def __init__(self):
+        self.__name__ = None
+        self._data = {}
+
+    def keys(self):
+        return self._data.keys()
+
+    def values(self):
+        return self._data.values()
+    
+    def __setitem__(self, name, value):
+        self._data[name] = value
+        value.__name__ = name
+        
+    def __getitem__(self, name):
+        return self._data[name]
+
+    def __delitem__(self, name):
+        del self._data[name]
+
+
+## class ItemModified(grok.Adapter):
+##     grok.context(Item)
+##     grok.implements(IModified)
+
+##     def modified_since(self, dt):
+##         return dt is None or self.context._modified is None or self.context._modified > dt
+
+##     def update(self):
+##         self.context._modified = datetime.now()
+
+def setUpZope(test):
+    pass
+
+def cleanUpZope(test):
+    for dirpath in _test_dirs:
+        shutil.rmtree(dirpath)
+    cleanup.cleanUp()
+
+_test_dirs = []
+
+def create_test_dir():
+    dirpath = tempfile.mkdtemp()
+    _test_dirs.append(dirpath)
+    return py.path.local(dirpath)
+
+def rel_paths(checkout, paths):
+    result = []
+    start = len(checkout.path.strpath)
+    for path in paths:
+        result.append(path.strpath[start:])
+    return sorted(result)
+
+
+globs = {'Container': Container,
+         'TestCheckout': TestCheckout,
+         'create_test_dir': create_test_dir,
+         'rel_paths': rel_paths}
+
+def test_suite():
+    suite = unittest.TestSuite([
+        doctest.DocFileSuite(
+        'README.txt',
+        setUp=setUpZope,
+        tearDown=cleanUpZope,
+        globs=globs,
+        optionflags=doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE)])
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

Added: z3c.vcsync/src/z3c/vcsync/vc.py
===================================================================
--- z3c.vcsync/src/z3c/vcsync/vc.py	                        (rev 0)
+++ z3c.vcsync/src/z3c/vcsync/vc.py	2007-06-25 17:36:04 UTC (rev 77071)
@@ -0,0 +1,150 @@
+import os
+
+from zope.interface import Interface
+from zope.component import queryUtility
+from zope.app.container.interfaces import IContainer
+
+from z3c.vcsync.interfaces import (IVcDump, IVcLoad,
+                                   ISerializer, IVcFactory,
+                                   IModified, ICheckout)
+
+import grok
+
+class VcDump(grok.Adapter):
+    """General VcDump for arbitrary objects.
+
+    Can be overridden for specific objects (such as containers).
+    """
+    grok.provides(IVcDump)
+    grok.context(Interface)
+
+    def save(self, checkout, path):
+        serializer = ISerializer(self.context)
+        path = path.join(serializer.name())
+        if not path.check():
+            checkout.add(path)
+        path.ensure()
+        f = path.open('w')
+        serializer.serialize(f)
+        f.close()
+        return path
+    
+class ContainerVcDump(grok.Adapter):
+    grok.provides(IVcDump)
+    grok.context(IContainer)
+        
+    def save(self, checkout, path):
+        path = path.join(self.context.__name__)
+        if not path.check():
+            checkout.add(path)
+        path.ensure(dir=True)
+        added_paths = []
+        for value in self.context.values():
+            added_paths.append(IVcDump(value).save(checkout, path))
+        # remove any paths not there anymore
+        for existing_path in path.listdir():
+            if existing_path not in added_paths:
+                checkout.delete(existing_path)
+                existing_path.remove()
+        return path
+
+class ContainerVcLoad(grok.Adapter):
+    grok.provides(IVcLoad)
+    grok.context(IContainer)
+    
+    def load(self, checkout, path):
+        loaded = []
+        for sub in path.listdir():
+            if sub.basename.startswith('.'):
+                continue
+            if sub.check(dir=True):
+                object_name = '' # containers are indicated by empty string
+            else:
+                object_name = sub.ext
+            #if sub.read().strip() == '200':
+            #    import pdb; pdb.set_trace()
+            factory = queryUtility(IVcFactory, name=object_name, default=None)
+            # we cannot handle this kind of object, so skip it
+            if factory is None:
+                continue
+            # create instance of object and put it into the container
+            # XXX what if object is already there?
+            obj = factory(checkout, sub)
+            # store the newly created object into the container
+            self.context[sub.purebasename] = obj
+            loaded.append(sub.purebasename)
+        # remove any objects not there anymore
+        for name in list(self.context.keys()):
+            if name not in loaded:
+                del self.context[name]
+
+class CheckoutBase(object):
+    """Checkout base class.
+
+    (hopefully) version control system agnostic.
+    """
+    grok.implements(ICheckout)
+    
+    def __init__(self, path):
+        self.path = path
+        self.clear()
+
+    def sync(self, object, message=''):
+        self.save(object)
+        self.up()
+        self.resolve()
+        self.load(object)
+        self.commit(message)
+
+    def save(self, object):
+        IVcDump(object).save(self, self.path)
+
+    def load(self, object):
+        # XXX can only load containers here, not items
+        names = [path.purebasename for path in self.path.listdir()
+                 if not path.purebasename.startswith('.')]
+        assert len(names) == 1
+        IVcLoad(object).load(self, self.path.join(names[0]))
+        
+    def clear(self):
+        self._added_by_save = []
+        self._deleted_by_save = []
+        
+    def up(self):
+        raise NotImplementedError
+
+    def resolve(self):
+        raise NotImplementedError
+
+    def commit(self, message):
+        raise NotImplementedError
+
+    def add(self, path):
+        self._added_by_save.append(path)
+
+    def delete(self, path):
+        self._deleted_by_save.append(path)
+
+    def added_by_save(self):
+        return self._added_by_save
+
+    def deleted_by_save(self):
+        return self._deleted_by_save
+
+    def added_by_up(self):
+        raise NotImplementedError
+
+    def deleted_by_up(self):
+        raise NotImplementedError
+
+    def modified_by_up(self):
+        raise NotImplementedError
+
+class ContainerModified(grok.Adapter):
+    grok.provides(IModified)
+    grok.context(IContainer)
+
+    def modified_since(self, dt):
+        # containers themselves are never modified
+        return False
+

Added: z3c.vcsync/svn-commit.tmp
===================================================================
--- z3c.vcsync/svn-commit.tmp	                        (rev 0)
+++ z3c.vcsync/svn-commit.tmp	2007-06-25 17:36:04 UTC (rev 77071)
@@ -0,0 +1,4 @@
+Initial import of z3c.vcsync.
+--This line, and those below, will be ignored--
+
+A    .



More information about the Checkins mailing list