Commit b6bb3314 authored by PJ Eby's avatar PJ Eby

Added ``ExtractionError`` and ``ResourceManager.extraction_error()`` so that

cache permission problems get a more user-friendly explanation of the
problem, and so that programs can catch and handle extraction errors if they
need to.

--HG--
branch : setuptools
extra : convert_revision : svn%3A6015fed2-1504-0410-9fe1-9d1591cc4771/sandbox/trunk/setuptools%4043426
parent 281fa7d0
......@@ -57,7 +57,8 @@ __all__ = [
# Exceptions
'ResolutionError','VersionConflict','DistributionNotFound','UnknownExtra',
'ExtractionError',
# Parsing functions and string utilities
'parse_requirements', 'parse_version', 'safe_name', 'safe_version',
'get_platform', 'compatible_platforms', 'yield_lines', 'split_sections',
......@@ -79,7 +80,6 @@ __all__ = [
# Deprecated/backward compatibility only
'run_main', 'AvailableDistributions',
]
class ResolutionError(Exception):
"""Abstract base for dependency resolution errors"""
......@@ -759,20 +759,20 @@ class Environment(object):
AvailableDistributions = Environment # XXX backward compatibility
class ExtractionError(RuntimeError):
"""An error occurred extracting a resource
The following attributes are available from instances of this exception:
manager
The resource manager that raised this exception
cache_path
The base directory for resource extraction
original_error
The exception instance that caused extraction to fail
"""
......@@ -818,6 +818,47 @@ class ResourceManager:
resource_name
)
def extraction_error(self):
"""Give an error message for problems extracting file(s)"""
old_exc = sys.exc_info()[1]
cache_path = self.extraction_path or get_default_cache()
err = ExtractionError("""Can't extract file(s) to egg cache
The following error occurred while trying to extract file(s) to the Python egg
cache:
%s
The Python egg cache directory is currently set to:
%s
Perhaps your account does not have write access to this directory? You can
change the cache directory by setting the PYTHON_EGG_CACHE environment
variable to point to an accessible directory.
""" % (old_exc, cache_path)
)
err.manager = self
err.cache_path = cache_path
err.original_error = old_exc
raise err
def get_cache_path(self, archive_name, names=()):
"""Return absolute location in cache for `archive_name` and `names`
......@@ -833,7 +874,11 @@ class ResourceManager:
"""
extract_path = self.extraction_path or get_default_cache()
target_path = os.path.join(extract_path, archive_name+'-tmp', *names)
ensure_directory(target_path)
try:
ensure_directory(target_path)
except:
self.extraction_error()
self.cached_files[target_path] = 1
return target_path
......@@ -855,10 +900,6 @@ class ResourceManager:
# XXX
def set_extraction_path(self, path):
"""Set the base path where resources will be extracted to, if needed.
......@@ -1188,12 +1229,14 @@ class ZipProvider(EggProvider):
return self._extract_resource(manager, zip_path)
def _extract_resource(self, manager, zip_path):
if zip_path in self._index():
for name in self._index()[zip_path]:
last = self._extract_resource(
manager, os.path.join(zip_path, name)
)
return os.path.dirname(last) # return the extracted directory name
zip_stat = self.zipinfo[zip_path]
t,d,size = zip_stat[5], zip_stat[6], zip_stat[3]
date_time = (
......@@ -1201,32 +1244,45 @@ class ZipProvider(EggProvider):
(t&0xFFFF)>>11, (t>>5)&0x3F, (t&0x1F) * 2, 0, 0, -1 # hms, etc.
)
timestamp = time.mktime(date_time)
real_path = manager.get_cache_path(self.egg_name, self._parts(zip_path))
if os.path.isfile(real_path):
stat = os.stat(real_path)
if stat.st_size==size and stat.st_mtime==timestamp:
# size and stamp match, don't bother extracting
return real_path
outf, tmpnam = _mkstemp(".$extract", dir=os.path.dirname(real_path))
os.write(outf, self.loader.get_data(zip_path))
os.close(outf)
utime(tmpnam, (timestamp,timestamp))
manager.postprocess(tmpnam, real_path)
try: rename(tmpnam, real_path)
except os.error:
try:
real_path = manager.get_cache_path(
self.egg_name, self._parts(zip_path)
)
if os.path.isfile(real_path):
stat = os.stat(real_path)
if stat.st_size==size and stat.st_mtime==timestamp:
# size and stamp match, somebody did it just ahead of us
# so we're done
return real_path
elif os.name=='nt': # Windows, delete old file and retry
unlink(real_path)
rename(tmpnam, real_path)
# size and stamp match, don't bother extracting
return real_path
raise
return real_path
outf, tmpnam = _mkstemp(".$extract", dir=os.path.dirname(real_path))
os.write(outf, self.loader.get_data(zip_path))
os.close(outf)
utime(tmpnam, (timestamp,timestamp))
manager.postprocess(tmpnam, real_path)
try:
rename(tmpnam, real_path)
except os.error:
if os.path.isfile(real_path):
stat = os.stat(real_path)
if stat.st_size==size and stat.st_mtime==timestamp:
# size and stamp match, somebody did it just ahead of
# us, so we're done
return real_path
elif os.name=='nt': # Windows, del old file and retry
unlink(real_path)
rename(tmpnam, real_path)
return real_path
raise
except os.error:
manager.extraction_error() # report a user-friendly error
return real_path
def _get_eager_resources(self):
if self.eagers is None:
......@@ -1264,11 +1320,6 @@ class ZipProvider(EggProvider):
def _listdir(self,fspath):
return list(self._index().get(self._zipinfo_name(fspath), ()))
def _eager_to_zip(self,resource_name):
return self._zipinfo_name(self._fn(self.egg_root,resource_name))
......@@ -1278,6 +1329,28 @@ class ZipProvider(EggProvider):
register_loader_type(zipimport.zipimporter, ZipProvider)
class FileMetadata(EmptyProvider):
"""Metadata handler for standalone PKG-INFO files
......@@ -1310,6 +1383,15 @@ class FileMetadata(EmptyProvider):
class PathMetadata(DefaultProvider):
"""Metadata provider for egg directories
......
......@@ -1194,6 +1194,14 @@ you must therefore have an explicit ``ResourceManager`` instance to use them.
obtain an extraction location, and only for names they intend to
extract, as it tracks the generated names for possible cleanup later.
``extraction_error()``
Raise an ``ExtractionError`` describing the active exception as interfering
with the extraction process. You should call this if you encounter any
OS errors extracting the file to the cache path; it will format the
operating system exception for you, and add other information to the
``ExtractionError`` instance that may be needed by programs that want to
wrap or handle extraction errors themselves.
``postprocess(tempname, filename)``
Perform any platform-specific postprocessing of `tempname`.
Resource providers should call this method ONLY after successfully
......@@ -1277,6 +1285,8 @@ occur when processing requests to locate and activate packages::
VersionConflict
UnknownExtra
ExtractionError
``ResolutionError``
This class is used as a base class for the other three exceptions, so that
you can catch all of them with a single "except" clause. It is also raised
......@@ -1294,6 +1304,19 @@ occur when processing requests to locate and activate packages::
One of the "extras" requested was not recognized by the distribution it
was requested from.
``ExtractionError``
A problem occurred extracting a resource to the Python Egg cache. The
following attributes are available on instances of this exception:
manager
The resource manager that raised this exception
cache_path
The base directory for resource extraction
original_error
The exception instance that caused extraction to fail
Supporting Custom Importers
===========================
......@@ -1629,6 +1652,12 @@ File/Path Utilities
Release Notes/Change History
----------------------------
0.6a11
* Added ``ExtractionError`` and ``ResourceManager.extraction_error()`` so that
cache permission problems get a more user-friendly explanation of the
problem, and so that programs can catch and handle extraction errors if they
need to.
0.6a10
* Added the ``extras`` attribute to ``Distribution``, the ``find_plugins()``
method to ``WorkingSet``, and the ``__add__()`` and ``__iadd__()`` methods
......
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