Commit 83096580 authored by Jim Fulton's avatar Jim Fulton

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.
parent 986da3e3
...@@ -11,7 +11,7 @@ priorities include: ...@@ -11,7 +11,7 @@ priorities include:
Change History Change History
************** **************
1.0.0b23 (2007-05-??) 1.0.0b23 (2007-05-09)
===================== =====================
Feature Changes Feature Changes
...@@ -20,11 +20,18 @@ Feature Changes ...@@ -20,11 +20,18 @@ Feature Changes
- Improved error reporting by showing which packages require other - Improved error reporting by showing which packages require other
packages that can't be found or that cause version conflicts. 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 Bugs Fixed
---------- ----------
- 92891: bootstrap crashes with recipe option in buildout section. - 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) 1.0.0b23 (2007-03-19)
===================== =====================
......
...@@ -338,7 +338,7 @@ class Buildout(UserDict.DictMixin): ...@@ -338,7 +338,7 @@ class Buildout(UserDict.DictMixin):
part) part)
try: try:
installed_files = update() installed_files = self[part]._call(update)
except: except:
installed_parts.remove(part) installed_parts.remove(part)
self._uninstall(old_installed_files) self._uninstall(old_installed_files)
...@@ -368,7 +368,7 @@ class Buildout(UserDict.DictMixin): ...@@ -368,7 +368,7 @@ class Buildout(UserDict.DictMixin):
need_to_save_installed = True need_to_save_installed = True
__doing__ = 'Installing %s', part __doing__ = 'Installing %s', part
self._logger.info(*__doing__) self._logger.info(*__doing__)
installed_files = recipe.install() installed_files = self[part]._call(recipe.install)
if installed_files is None: if installed_files is None:
self._logger.warning( self._logger.warning(
"The %s install returned None. A path or " "The %s install returned None. A path or "
...@@ -922,6 +922,32 @@ class Options(UserDict.DictMixin): ...@@ -922,6 +922,32 @@ class Options(UserDict.DictMixin):
result.update(self._data) result.update(self._data)
return result 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]*' _spacey_nl = re.compile('[ \t\r\f\v]*\n[ \t\r\f\v\n]*'
'|' '|'
'^[ \t\r\f\v]+' '^[ \t\r\f\v]+'
...@@ -1103,6 +1129,7 @@ def _check_for_unused_options_in_section(buildout, section): ...@@ -1103,6 +1129,7 @@ def _check_for_unused_options_in_section(buildout, section):
def _internal_error(v): def _internal_error(v):
sys.stderr.write(_internal_error_template % (v.__class__.__name__, v)) sys.stderr.write(_internal_error_template % (v.__class__.__name__, v))
sys.exit(1)
_usage = """\ _usage = """\
......
...@@ -394,6 +394,288 @@ We'll get a user error, not a traceback. ...@@ -394,6 +394,288 @@ We'll get a user error, not a traceback.
Error: Invalid Path 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 Configuration file syntax
------------------------- -------------------------
......
...@@ -72,23 +72,25 @@ supply some input: ...@@ -72,23 +72,25 @@ supply some input:
... p self.options.keys() ... p self.options.keys()
... q ... q
... """), ... """),
buildout: Develop: /tmp/tmpozk_tH/_TEST_/sample-buildout/recipes buildout: Develop: /sample-buildout/recipes
buildout: Installing data-dir buildout: Installing data-dir
While: While:
Installing data-dir Installing data-dir
Traceback (most recent call last): 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) getattr(buildout, command)(args)
File "/zc/buildout/buildout.py", line 324, in install File "/zc/buildout/buildout.py", line 371, in install
installed_files = recipe.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 File "/sample-buildout/recipes/mkdir.py", line 14, in install
directory = self.options['directory'] 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)) raise MissingOption("Missing option: %s:%s" % (self.name, key))
MissingOption: Missing option: data-dir:directory MissingOption: Missing option: data-dir:directory
<BLANKLINE> <BLANKLINE>
Starting pdb: 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)) -> raise MissingOption("Missing option: %s:%s" % (self.name, key))
(Pdb) > /sample-buildout/recipes/mkdir.py(14)install() (Pdb) > /sample-buildout/recipes/mkdir.py(14)install()
-> directory = self.options['directory'] -> directory = self.options['directory']
......
...@@ -1915,6 +1915,76 @@ def bug_105081_Specific_egg_versions_are_ignored_when_newer_eggs_are_around(): ...@@ -1915,6 +1915,76 @@ def bug_105081_Specific_egg_versions_are_ignored_when_newer_eggs_are_around():
1 1 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): def create_sample_eggs(test, executable=sys.executable):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment