[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