aboutsummaryrefslogtreecommitdiffstats
path: root/pkg_resources.py
diff options
context:
space:
mode:
Diffstat (limited to 'pkg_resources.py')
-rw-r--r--pkg_resources.py150
1 files changed, 97 insertions, 53 deletions
diff --git a/pkg_resources.py b/pkg_resources.py
index b63e3f0f..4c05b09f 100644
--- a/pkg_resources.py
+++ b/pkg_resources.py
@@ -13,7 +13,7 @@ The package resource API is designed to work with normal filesystem packages,
method.
"""
-import sys, os, zipimport, time, re, imp, types
+import sys, os, time, re, imp, types, zipfile, zipimport
try:
from urlparse import urlparse, urlunparse
except ImportError:
@@ -60,6 +60,12 @@ except ImportError:
from os import open as os_open
from os.path import isdir, split
+# Avoid try/except due to potential problems with delayed import mechanisms.
+if sys.version_info >= (3, 3) and sys.implementation.name == "cpython":
+ import importlib._bootstrap as importlib_bootstrap
+else:
+ importlib_bootstrap = None
+
# This marker is used to simplify the process that checks is the
# setuptools package was installed by the Setuptools project
# or by the Distribute project, in case Setuptools creates
@@ -1354,13 +1360,8 @@ class DefaultProvider(EggProvider):
register_loader_type(type(None), DefaultProvider)
-try:
- # CPython >=3.3
- import _frozen_importlib
-except ImportError:
- pass
-else:
- register_loader_type(_frozen_importlib.SourceFileLoader, DefaultProvider)
+if importlib_bootstrap is not None:
+ register_loader_type(importlib_bootstrap.SourceFileLoader, DefaultProvider)
class EmptyProvider(NullProvider):
@@ -1377,6 +1378,37 @@ class EmptyProvider(NullProvider):
empty_provider = EmptyProvider()
+def build_zipmanifest(path):
+ """
+ This builds a similar dictionary to the zipimport directory
+ caches. However instead of tuples, ZipInfo objects are stored.
+
+ The translation of the tuple is as follows:
+ * [0] - zipinfo.filename on stock pythons this needs "/" --> os.sep
+ on pypy it is the same (one reason why distribute did work
+ in some cases on pypy and win32).
+ * [1] - zipinfo.compress_type
+ * [2] - zipinfo.compress_size
+ * [3] - zipinfo.file_size
+ * [4] - len(utf-8 encoding of filename) if zipinfo & 0x800
+ len(ascii encoding of filename) otherwise
+ * [5] - (zipinfo.date_time[0] - 1980) << 9 |
+ zipinfo.date_time[1] << 5 | zipinfo.date_time[2]
+ * [6] - (zipinfo.date_time[3] - 1980) << 11 |
+ zipinfo.date_time[4] << 5 | (zipinfo.date_time[5] // 2)
+ * [7] - zipinfo.CRC
+ """
+ zipinfo = dict()
+ zfile = zipfile.ZipFile(path)
+ #Got ZipFile has not __exit__ on python 3.1
+ try:
+ for zitem in zfile.namelist():
+ zpath = zitem.replace('/', os.sep)
+ zipinfo[zpath] = zfile.getinfo(zitem)
+ assert zipinfo[zpath] is not None
+ finally:
+ zfile.close()
+ return zipinfo
class ZipProvider(EggProvider):
@@ -1386,7 +1418,7 @@ class ZipProvider(EggProvider):
def __init__(self, module):
EggProvider.__init__(self,module)
- self.zipinfo = zipimport._zip_directory_cache[self.loader.archive]
+ self.zipinfo = build_zipmanifest(self.loader.archive)
self.zip_pre = self.loader.archive+os.sep
def _zipinfo_name(self, fspath):
@@ -1420,6 +1452,14 @@ class ZipProvider(EggProvider):
self._extract_resource(manager, self._eager_to_zip(name))
return self._extract_resource(manager, zip_path)
+ @staticmethod
+ def _get_date_and_size(zip_stat):
+ size = zip_stat.file_size
+ date_time = zip_stat.date_time + (0, 0, -1) # ymdhms+wday, yday, dst
+ #1980 offset already done
+ timestamp = time.mktime(date_time)
+ return timestamp, size
+
def _extract_resource(self, manager, zip_path):
if zip_path in self._index():
@@ -1429,28 +1469,19 @@ class ZipProvider(EggProvider):
)
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 = (
- (d>>9)+1980, (d>>5)&0xF, d&0x1F, # ymd
- (t&0xFFFF)>>11, (t>>5)&0x3F, (t&0x1F) * 2, 0, 0, -1 # hms, etc.
- )
- timestamp = time.mktime(date_time)
+ timestamp, size = self._get_date_and_size(self.zipinfo[zip_path])
+ if not WRITE_SUPPORT:
+ raise IOError('"os.rename" and "os.unlink" are not supported '
+ 'on this platform')
try:
- if not WRITE_SUPPORT:
- raise IOError('"os.rename" and "os.unlink" are not supported '
- 'on this platform')
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
+ if self._is_current(real_path, zip_path):
+ return real_path
outf, tmpnam = _mkstemp(".$extract", dir=os.path.dirname(real_path))
os.write(outf, self.loader.get_data(zip_path))
@@ -1463,11 +1494,9 @@ class ZipProvider(EggProvider):
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
+ if self._is_current(real_path, zip_path):
+ # the file became current since it was checked above,
+ # so proceed.
return real_path
elif os.name=='nt': # Windows, del old file and retry
unlink(real_path)
@@ -1480,6 +1509,23 @@ class ZipProvider(EggProvider):
return real_path
+ def _is_current(self, file_path, zip_path):
+ """
+ Return True if the file_path is current for this zip_path
+ """
+ timestamp, size = self._get_date_and_size(self.zipinfo[zip_path])
+ if not os.path.isfile(file_path):
+ return False
+ stat = os.stat(file_path)
+ if stat.st_size!=size or stat.st_mtime!=timestamp:
+ return False
+ # check that the contents match
+ zip_contents = self.loader.get_data(zip_path)
+ f = open(file_path, 'rb')
+ file_contents = f.read()
+ f.close()
+ return zip_contents == file_contents
+
def _get_eager_resources(self):
if self.eagers is None:
eagers = []
@@ -1622,7 +1668,7 @@ class EggMetadata(ZipProvider):
def __init__(self, importer):
"""Create a metadata provider from a zipimporter"""
- self.zipinfo = zipimport._zip_directory_cache[importer.archive]
+ self.zipinfo = build_zipmanifest(importer.archive)
self.zip_pre = importer.archive+os.sep
self.loader = importer
if importer.prefix:
@@ -1789,20 +1835,20 @@ def find_on_path(importer, path_item, only=False):
for dist in find_distributions(os.path.join(path_item, entry)):
yield dist
elif not only and lower.endswith('.egg-link'):
- for line in open(os.path.join(path_item, entry)):
+ entry_file = open(os.path.join(path_item, entry))
+ try:
+ entry_lines = entry_file.readlines()
+ finally:
+ entry_file.close()
+ for line in entry_lines:
if not line.strip(): continue
for item in find_distributions(os.path.join(path_item,line.rstrip())):
yield item
break
register_finder(ImpWrapper,find_on_path)
-try:
- # CPython >=3.3
- import _frozen_importlib
-except ImportError:
- pass
-else:
- register_finder(_frozen_importlib.FileFinder, find_on_path)
+if importlib_bootstrap is not None:
+ register_finder(importlib_bootstrap.FileFinder, find_on_path)
_declare_state('dict', _namespace_handlers={})
_declare_state('dict', _namespace_packages={})
@@ -1903,13 +1949,8 @@ def file_ns_handler(importer, path_item, packageName, module):
register_namespace_handler(ImpWrapper,file_ns_handler)
register_namespace_handler(zipimport.zipimporter,file_ns_handler)
-try:
- # CPython >=3.3
- import _frozen_importlib
-except ImportError:
- pass
-else:
- register_namespace_handler(_frozen_importlib.FileFinder, file_ns_handler)
+if importlib_bootstrap is not None:
+ register_namespace_handler(importlib_bootstrap.FileFinder, file_ns_handler)
def null_ns_handler(importer, path_item, packageName, module):
@@ -1969,7 +2010,7 @@ replace = {'pre':'c', 'preview':'c','-':'final-','rc':'c','dev':'@'}.get
def _parse_version_parts(s):
for part in component_re.split(s):
part = replace(part,part)
- if part in ['', '.']:
+ if not part or part=='.':
continue
if part[:1] in '0123456789':
yield part.zfill(8) # pad for numeric comparison
@@ -2012,6 +2053,8 @@ def parse_version(s):
parts = []
for part in _parse_version_parts(s.lower()):
if part.startswith('*'):
+ if part<'*final': # remove '-' before a prerelease tag
+ while parts and parts[-1]=='*final-': parts.pop()
# remove trailing zeros from each series of numeric parts
while parts and parts[-1]=='00000000':
parts.pop()
@@ -2149,7 +2192,7 @@ def _remove_md5_fragment(location):
class Distribution(object):
"""Wrap an actual or potential sys.path entry w/metadata"""
PKG_INFO = 'PKG-INFO'
-
+
def __init__(self,
location=None, metadata=None, project_name=None, version=None,
py_version=PY_MAJOR, platform=None, precedence = EGG_DIST
@@ -2488,7 +2531,7 @@ class DistInfoDistribution(Distribution):
from email.parser import Parser
self._pkg_info = Parser().parsestr(self.get_metadata(self.PKG_INFO))
return self._pkg_info
-
+
@property
def _dep_map(self):
try:
@@ -2499,7 +2542,7 @@ class DistInfoDistribution(Distribution):
def _preparse_requirement(self, requires_dist):
"""Convert 'Foobar (1); baz' to ('Foobar ==1', 'baz')
- Split environment marker, add == prefix to version specifiers as
+ Split environment marker, add == prefix to version specifiers as
necessary, and remove parenthesis.
"""
parts = requires_dist.split(';', 1) + ['']
@@ -2508,7 +2551,7 @@ class DistInfoDistribution(Distribution):
distvers = re.sub(self.EQEQ, r"\1==\2\3", distvers)
distvers = distvers.replace('(', '').replace(')', '')
return (distvers, mark)
-
+
def _compute_dependencies(self):
"""Recompute this distribution's dependencies."""
from _markerlib import compile as compile_marker
@@ -2521,7 +2564,7 @@ class DistInfoDistribution(Distribution):
parsed = next(parse_requirements(distvers))
parsed.marker_fn = compile_marker(mark)
reqs.append(parsed)
-
+
def reqs_for_extra(extra):
for req in reqs:
if req.marker_fn(override={'extra':extra}):
@@ -2529,13 +2572,13 @@ class DistInfoDistribution(Distribution):
common = frozenset(reqs_for_extra(None))
dm[None].extend(common)
-
+
for extra in self._parsed_pkg_info.get_all('Provides-Extra') or []:
extra = safe_extra(extra.strip())
dm[extra] = list(frozenset(reqs_for_extra(extra)) - common)
return dm
-
+
_distributionImpl = {'.egg': Distribution,
'.egg-info': Distribution,
@@ -2856,3 +2899,4 @@ run_main = run_script # backward compatibility
add_activation_listener(lambda dist: dist.activate())
working_set.entries=[]; list(map(working_set.add_entry,sys.path)) # match order
+