[Checkins] SVN: zc.buildout/trunk/ Added documentation of error handling requirementsfor recipes and an

Jim Fulton jim at zope.com
Wed May 9 06:06:15 EDT 2007


Log message for revision 75648:
  Added documentation of error handling requirementsfor recipes and an
  api to help clean up created paths when errors occir in recipe install
  and update methods.
  
  Made sure buildout exited with a non-zeo exit status when errors
  occurred.
  

Changed:
  U   zc.buildout/trunk/CHANGES.txt
  U   zc.buildout/trunk/src/zc/buildout/buildout.py
  U   zc.buildout/trunk/src/zc/buildout/buildout.txt
  U   zc.buildout/trunk/src/zc/buildout/debugging.txt
  U   zc.buildout/trunk/src/zc/buildout/tests.py

-=-
Modified: zc.buildout/trunk/CHANGES.txt
===================================================================
--- zc.buildout/trunk/CHANGES.txt	2007-05-09 09:52:17 UTC (rev 75647)
+++ zc.buildout/trunk/CHANGES.txt	2007-05-09 10:06:14 UTC (rev 75648)
@@ -11,7 +11,7 @@
 Change History
 **************
 
-1.0.0b23 (2007-05-??)
+1.0.0b23 (2007-05-09)
 =====================
 
 Feature Changes
@@ -20,11 +20,18 @@
 - Improved error reporting by showing which packages require other
   packages that can't be found or that cause version conflicts.
 
+- Added an API for use by recipe writers to clean up created files
+  when recipe errors occur.
+
 Bugs Fixed
 ----------
 
 - 92891: bootstrap crashes with recipe option in buildout section.
 
+- 113085: Buildout exited with a zero exist status when internal errors
+  occured.
+
+
 1.0.0b23 (2007-03-19)
 =====================
 

Modified: zc.buildout/trunk/src/zc/buildout/buildout.py
===================================================================
--- zc.buildout/trunk/src/zc/buildout/buildout.py	2007-05-09 09:52:17 UTC (rev 75647)
+++ zc.buildout/trunk/src/zc/buildout/buildout.py	2007-05-09 10:06:14 UTC (rev 75648)
@@ -338,7 +338,7 @@
                         part)
 
                 try:
-                    installed_files = update()
+                    installed_files = self[part]._call(update)
                 except:
                     installed_parts.remove(part)
                     self._uninstall(old_installed_files)
@@ -368,7 +368,7 @@
                 need_to_save_installed = True
                 __doing__ = 'Installing %s', part
                 self._logger.info(*__doing__)
-                installed_files = recipe.install()
+                installed_files = self[part]._call(recipe.install)
                 if installed_files is None:
                     self._logger.warning(
                         "The %s install returned None.  A path or "
@@ -922,6 +922,32 @@
         result.update(self._data)
         return result
 
+    def _call(self, f):
+        self._created = []
+        try:
+            try:
+                return f()
+            except:
+                for p in self._created:
+                    if os.path.isdir(p):
+                        shutil.rmtree(p)
+                    elif os.path.isfile(p):
+                        os.remove(p)
+                    else:
+                        self._buildout._logger.warn("Couldn't clean up %s", p)
+                raise
+        finally:
+            self._created = None
+
+    def created(self, *paths):
+        try:
+            self._created.extend(paths)
+        except AttributeError:
+            raise TypeError(
+                "Attempt to register a created path while not installing",
+                self.name)
+        return self._created
+
 _spacey_nl = re.compile('[ \t\r\f\v]*\n[ \t\r\f\v\n]*'
                         '|'
                         '^[ \t\r\f\v]+'
@@ -1103,6 +1129,7 @@
 
 def _internal_error(v):
     sys.stderr.write(_internal_error_template % (v.__class__.__name__, v))
+    sys.exit(1)
     
 
 _usage = """\

Modified: zc.buildout/trunk/src/zc/buildout/buildout.txt
===================================================================
--- zc.buildout/trunk/src/zc/buildout/buildout.txt	2007-05-09 09:52:17 UTC (rev 75647)
+++ zc.buildout/trunk/src/zc/buildout/buildout.txt	2007-05-09 10:06:14 UTC (rev 75648)
@@ -394,6 +394,288 @@
     Error: Invalid Path
 
 
+Recipe Error Handling
+---------------------
+
+If an error occurs during installation, it is up to the recipe to
+clean up any system side effects, such as files created.  Let's update
+the mkdir recipe to support multiple paths:
+
+    >>> write(sample_buildout, 'recipes', 'mkdir.py', 
+    ... """
+    ... import logging, os, zc.buildout
+    ...
+    ... class Mkdir:
+    ...
+    ...     def __init__(self, buildout, name, options):
+    ...         self.name, self.options = name, options
+    ...
+    ...         # Normalize paths and check that their parent
+    ...         # directories exist:
+    ...         paths = []
+    ...         for path in options['path'].split(): 
+    ...             path = os.path.join(buildout['buildout']['directory'], path)
+    ...             if not os.path.isdir(os.path.dirname(path)):
+    ...                 logging.getLogger(self.name).error(
+    ...                     'Cannot create %s. %s is not a directory.',
+    ...                     options['path'], os.path.dirname(options['path']))
+    ...                 raise zc.buildout.UserError('Invalid Path')
+    ...             paths.append(path)
+    ...         options['path'] = ' '.join(paths)
+    ...
+    ...     def install(self):
+    ...         paths = self.options['path'].split()
+    ...         for path in paths: 
+    ...             logging.getLogger(self.name).info(
+    ...                 'Creating directory %s', os.path.basename(path))
+    ...             os.mkdir(path)
+    ...         return paths
+    ...
+    ...     def update(self):
+    ...         pass
+    ... """)
+
+If there is an error creating a path, the install method will exit and
+leave previously created paths in place:
+
+    >>> write(sample_buildout, 'buildout.cfg',
+    ... """
+    ... [buildout]
+    ... develop = recipes
+    ... parts = data-dir
+    ...
+    ... [data-dir]
+    ... recipe = recipes:mkdir
+    ... path = foo bin
+    ... """)
+
+    >>> print system(buildout),
+    buildout: Develop: /sample-buildout/recipes
+    buildout: Uninstalling data-dir
+    buildout: Installing data-dir
+    data-dir: Creating directory foo
+    data-dir: Creating directory bin
+    While:
+      Installing data-dir
+    <BLANKLINE>
+    An internal error occured due to a bug in either zc.buildout or in a
+    recipe being used:
+    <BLANKLINE>
+    OSError:
+    [Errno 17] File exists: '/sample-buildout/bin'
+
+We meant to create a directiry bins, but typed bin.  Now foo was
+left behind.
+
+    >>> os.path.exists('foo')
+    True
+
+If we fix the typo:
+
+    >>> write(sample_buildout, 'buildout.cfg',
+    ... """
+    ... [buildout]
+    ... develop = recipes
+    ... parts = data-dir
+    ...
+    ... [data-dir]
+    ... recipe = recipes:mkdir
+    ... path = foo bins
+    ... """)
+
+    >>> print system(buildout),
+    buildout: Develop: /sample-buildout/recipes
+    buildout: Installing data-dir
+    data-dir: Creating directory foo
+    While:
+      Installing data-dir
+    <BLANKLINE>
+    An internal error occured due to a bug in either zc.buildout or in a
+    recipe being used:
+    <BLANKLINE>
+    OSError:
+    [Errno 17] File exists: '/sample-buildout/foo'
+
+Now they fail because foo exists, because it was left behind.
+
+    >>> remove('foo')
+
+Let's fix the recipe:
+
+    >>> write(sample_buildout, 'recipes', 'mkdir.py', 
+    ... """
+    ... import logging, os, zc.buildout
+    ...
+    ... class Mkdir:
+    ...
+    ...     def __init__(self, buildout, name, options):
+    ...         self.name, self.options = name, options
+    ...
+    ...         # Normalize paths and check that their parent
+    ...         # directories exist:
+    ...         paths = []
+    ...         for path in options['path'].split(): 
+    ...             path = os.path.join(buildout['buildout']['directory'], path)
+    ...             if not os.path.isdir(os.path.dirname(path)):
+    ...                 logging.getLogger(self.name).error(
+    ...                     'Cannot create %s. %s is not a directory.',
+    ...                     options['path'], os.path.dirname(options['path']))
+    ...                 raise zc.buildout.UserError('Invalid Path')
+    ...             paths.append(path)
+    ...         options['path'] = ' '.join(paths)
+    ...
+    ...     def install(self):
+    ...         paths = self.options['path'].split()
+    ...         created = []
+    ...         try:
+    ...             for path in paths: 
+    ...                 logging.getLogger(self.name).info(
+    ...                     'Creating directory %s', os.path.basename(path))
+    ...                 os.mkdir(path)
+    ...                 created.append(path)
+    ...         except:
+    ...             for d in created:
+    ...                 os.rmdir(d)
+    ...             raise
+    ...
+    ...         return paths
+    ...
+    ...     def update(self):
+    ...         pass
+    ... """)
+
+And put back the typo:
+
+    >>> write(sample_buildout, 'buildout.cfg',
+    ... """
+    ... [buildout]
+    ... develop = recipes
+    ... parts = data-dir
+    ...
+    ... [data-dir]
+    ... recipe = recipes:mkdir
+    ... path = foo bin
+    ... """)
+
+When we rerun the buildout:
+
+    >>> print system(buildout),
+    buildout: Develop: /sample-buildout/recipes
+    buildout: Installing data-dir
+    data-dir: Creating directory foo
+    data-dir: Creating directory bin
+    While:
+      Installing data-dir
+    <BLANKLINE>
+    An internal error occured due to a bug in either zc.buildout or in a
+    recipe being used:
+    <BLANKLINE>
+    OSError:
+    [Errno 17] File exists: '/sample-buildout/bin'
+
+we get the same error, but we don't get the directory left behind:
+
+    >>> os.path.exists('foo')
+    False
+
+It's critical that recipes clean up partial effects when errors
+occur.  Because recipes most commonly create files and directories,
+buildout provides a helper API for removing created files when an
+error occurs.  Option objects have a created method that can be called
+to record files as they are created.  If the install or update methof
+returns with an error, then any registered paths are removed
+automatically.  The method returns the files registered and can be
+used to return the files created.  Let's use this API to simplify the
+recipe:
+
+    >>> write(sample_buildout, 'recipes', 'mkdir.py', 
+    ... """
+    ... import logging, os, zc.buildout
+    ...
+    ... class Mkdir:
+    ...
+    ...     def __init__(self, buildout, name, options):
+    ...         self.name, self.options = name, options
+    ...
+    ...         # Normalize paths and check that their parent
+    ...         # directories exist:
+    ...         paths = []
+    ...         for path in options['path'].split(): 
+    ...             path = os.path.join(buildout['buildout']['directory'], path)
+    ...             if not os.path.isdir(os.path.dirname(path)):
+    ...                 logging.getLogger(self.name).error(
+    ...                     'Cannot create %s. %s is not a directory.',
+    ...                     options['path'], os.path.dirname(options['path']))
+    ...                 raise zc.buildout.UserError('Invalid Path')
+    ...             paths.append(path)
+    ...         options['path'] = ' '.join(paths)
+    ...
+    ...     def install(self):
+    ...         paths = self.options['path'].split()
+    ...         for path in paths: 
+    ...             logging.getLogger(self.name).info(
+    ...                 'Creating directory %s', os.path.basename(path))
+    ...             os.mkdir(path)
+    ...             self.options.created(path)
+    ...
+    ...         return self.options.created()
+    ...
+    ...     def update(self):
+    ...         pass
+    ... """)
+
+..
+
+    >>> remove(sample_buildout, 'recipes', 'mkdir.pyc')
+
+We returned by calling created, taking advantage of the fact that it
+returns the registered paths.  We did this for illustrative purposes.
+It would be simpler just to return the paths as before.
+
+If we rerun the buildout, again, we'll get the error and no
+directiories will be created:
+
+    >>> print system(buildout),
+    buildout: Develop: /sample-buildout/recipes
+    buildout: Installing data-dir
+    data-dir: Creating directory foo
+    data-dir: Creating directory bin
+    While:
+      Installing data-dir
+    <BLANKLINE>
+    An internal error occured due to a bug in either zc.buildout or in a
+    recipe being used:
+    <BLANKLINE>
+    OSError:
+    [Errno 17] File exists: '/sample-buildout/bin'
+
+    >>> os.path.exists('foo')
+    False
+
+Now, we'll fix the typo again and we'll get the directories we expect:
+
+    >>> write(sample_buildout, 'buildout.cfg',
+    ... """
+    ... [buildout]
+    ... develop = recipes
+    ... parts = data-dir
+    ...
+    ... [data-dir]
+    ... recipe = recipes:mkdir
+    ... path = foo bins
+    ... """)
+
+    >>> print system(buildout),
+    buildout: Develop: /sample-buildout/recipes
+    buildout: Installing data-dir
+    data-dir: Creating directory foo
+    data-dir: Creating directory bins
+
+    >>> os.path.exists('foo')
+    True
+    >>> os.path.exists('bins')
+    True
+
 Configuration file syntax
 -------------------------
 

Modified: zc.buildout/trunk/src/zc/buildout/debugging.txt
===================================================================
--- zc.buildout/trunk/src/zc/buildout/debugging.txt	2007-05-09 09:52:17 UTC (rev 75647)
+++ zc.buildout/trunk/src/zc/buildout/debugging.txt	2007-05-09 10:06:14 UTC (rev 75648)
@@ -72,23 +72,25 @@
     ... p self.options.keys()
     ... q
     ... """),
-    buildout: Develop: /tmp/tmpozk_tH/_TEST_/sample-buildout/recipes
+    buildout: Develop: /sample-buildout/recipes
     buildout: Installing data-dir
     While:
       Installing data-dir
     Traceback (most recent call last):
-      File "/zc/buildout/buildout.py", line 1173, in main
+      File "/zc/buildout/buildout.py", line 1294, in main
         getattr(buildout, command)(args)
-      File "/zc/buildout/buildout.py", line 324, in install
-        installed_files = recipe.install()
+      File "/zc/buildout/buildout.py", line 371, in install
+        installed_files = self[part]._call(recipe.install)
+      File "/zc/buildout/buildout.py", line 929, in _call
+        return f()
       File "/sample-buildout/recipes/mkdir.py", line 14, in install
         directory = self.options['directory']
-      File "/zc/buildout/buildout.py", line 815, in __getitem__
+      File "/zc/buildout/buildout.py", line 895, in __getitem__
         raise MissingOption("Missing option: %s:%s" % (self.name, key))
     MissingOption: Missing option: data-dir:directory
     <BLANKLINE>
     Starting pdb:
-    > /zc/buildout/buildout.py(815)__getitem__()
+    > /Users/jim/p/buildout/trunk/src/zc/buildout/buildout.py(895)__getitem__()
     -> raise MissingOption("Missing option: %s:%s" % (self.name, key))
     (Pdb) > /sample-buildout/recipes/mkdir.py(14)install()
     -> directory = self.options['directory']

Modified: zc.buildout/trunk/src/zc/buildout/tests.py
===================================================================
--- zc.buildout/trunk/src/zc/buildout/tests.py	2007-05-09 09:52:17 UTC (rev 75647)
+++ zc.buildout/trunk/src/zc/buildout/tests.py	2007-05-09 10:06:14 UTC (rev 75648)
@@ -1915,6 +1915,76 @@
     1 1
     """
 
+if sys.version_info > (2, 4):
+    def test_exit_codes():
+        """
+        >>> import subprocess
+        >>> def call(s):
+        ...     p = subprocess.Popen(s, stdin=subprocess.PIPE,
+        ...                 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        ...     p.stdin.close()
+        ...     print p.stdout.read()
+        ...     print 'Exit:', bool(p.wait())
+        
+        >>> call(buildout)
+        <BLANKLINE>
+        Exit: False
+
+        >>> write('buildout.cfg',
+        ... '''
+        ... [buildout]
+        ... parts = x
+        ... ''')
+
+        >>> call(buildout)
+        While:
+          Installing
+          Getting section x
+        Error: The referenced section, 'x', was not defined.
+        <BLANKLINE>
+        Exit: True
+
+        >>> write('setup.py',
+        ... '''
+        ... from setuptools import setup
+        ... setup(name='zc.buildout.testexit', entry_points={
+        ...    'zc.buildout': ['default = testexitrecipe:x']})
+        ... ''')
+
+        >>> write('testexitrecipe.py',
+        ... '''
+        ... x y
+        ... ''')
+
+        >>> write('buildout.cfg',
+        ... '''
+        ... [buildout]
+        ... parts = x
+        ... develop = .
+        ...
+        ... [x]
+        ... recipe = zc.buildout.testexit
+        ... ''')
+
+        >>> call(buildout)
+        buildout: Develop: /sample-buildout/.
+        While:
+          Installing
+          Getting section x
+          Initializing section x
+          Loading zc.buildout recipe entry zc.buildout.testexit:default
+        <BLANKLINE>
+        An internal error occured due to a bug in either zc.buildout or in a
+        recipe being used:
+        <BLANKLINE>
+        SyntaxError:
+        invalid syntax (testexitrecipe.py, line 2)
+        <BLANKLINE>
+        Exit: True
+
+        """
+
+
 ######################################################################
     
 def create_sample_eggs(test, executable=sys.executable):



More information about the Checkins mailing list