diff options
Diffstat (limited to 'pkg_resources.py')
| -rw-r--r-- | pkg_resources.py | 238 |
1 files changed, 197 insertions, 41 deletions
diff --git a/pkg_resources.py b/pkg_resources.py index abb90b1a..2ba0febf 100644 --- a/pkg_resources.py +++ b/pkg_resources.py @@ -14,6 +14,7 @@ method. """ import sys, os, zipimport, time, re, imp +from urlparse import urlparse, urlunparse try: frozenset @@ -21,13 +22,27 @@ except NameError: from sets import ImmutableSet as frozenset # capture these to bypass sandboxing -from os import utime, rename, unlink, mkdir +from os import utime +try: + from os import mkdir, rename, unlink + WRITE_SUPPORT = True +except ImportError: + # no write support, probably under GAE + WRITE_SUPPORT = False + 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 def _bypass_ensure_directory(name, mode=0777): # Sandbox-bypassing version of ensure_directory() + if not WRITE_SUPPORT: + raise IOError('"os.mkdir" not supported on this platform.') dirname, filename = split(name) if dirname and filename and not isdir(dirname): _bypass_ensure_directory(dirname) @@ -73,8 +88,6 @@ _sget_none = _sset_none = lambda *args: None - - def get_supported_platform(): """Return this platform's maximum compatible version. @@ -159,7 +172,8 @@ __all__ = [ ] class ResolutionError(Exception): """Abstract base for dependency resolution errors""" - def __repr__(self): return self.__class__.__name__+repr(self.args) + def __repr__(self): + return self.__class__.__name__+repr(self.args) class VersionConflict(ResolutionError): """An already-installed version conflicts with the requested version""" @@ -170,6 +184,7 @@ class DistributionNotFound(ResolutionError): class UnknownExtra(ResolutionError): """Distribution doesn't have an "extra feature" of the given name""" _provider_factories = {} + PY_MAJOR = sys.version[:3] EGG_DIST = 3 BINARY_DIST = 2 @@ -200,8 +215,19 @@ def get_provider(moduleOrReq): def _macosx_vers(_cache=[]): if not _cache: - from platform import mac_ver - _cache.append(mac_ver()[0].split('.')) + import platform + version = platform.mac_ver()[0] + # fallback for MacPorts + if version == '': + import plistlib + plist = '/System/Library/CoreServices/SystemVersion.plist' + if os.path.exists(plist): + if hasattr(plistlib, 'readPlist'): + plist_content = plistlib.readPlist(plist) + if 'ProductVersion' in plist_content: + version = plist_content['ProductVersion'] + + _cache.append(version.split('.')) return _cache[0] def _macosx_arch(machine): @@ -213,7 +239,11 @@ def get_build_platform(): XXX Currently this is the same as ``distutils.util.get_platform()``, but it needs some hacks for Linux and Mac OS X. """ - from distutils.util import get_platform + try: + from distutils.util import get_platform + except ImportError: + from sysconfig import get_platform + plat = get_platform() if sys.platform == "darwin" and not plat.startswith('macosx-'): try: @@ -237,8 +267,6 @@ get_platform = get_build_platform # XXX backward compat - - def compatible_platforms(provided,required): """Can code for the `provided` platform run on the `required` platform? @@ -353,15 +381,6 @@ class IMetadataProvider: - - - - - - - - - class IResourceProvider(IMetadataProvider): """An object that provides access to package resources""" @@ -493,6 +512,10 @@ class WorkingSet(object): """ seen = {} for item in self.entries: + if item not in self.entry_keys: + # workaround a cache issue + continue + for key in self.entry_keys[item]: if key not in seen: seen[key]=1 @@ -557,7 +580,13 @@ class WorkingSet(object): env = Environment(self.entries) dist = best[req.key] = env.best_match(req, self, installer) if dist is None: - raise DistributionNotFound(req) # XXX put more info here + #msg = ("The '%s' distribution was not found on this " + # "system, and is required by this application.") + #raise DistributionNotFound(msg % req) + + # unfortunately, zc.buildout uses a str(err) + # to get the name of the distribution here.. + raise DistributionNotFound(req) to_activate.append(dist) if dist not in req: # Oops, the "best" so far conflicts with a dependency @@ -578,7 +607,7 @@ class WorkingSet(object): Environment(plugin_dirlist) ) map(working_set.add, distributions) # add plugins+libs to sys.path - print "Couldn't load", errors # display errors + print 'Could not load', errors # display errors The `plugin_env` should be an ``Environment`` instance that contains only distributions that are in the project's "plugin directory" or @@ -1167,10 +1196,16 @@ class NullProvider: def has_metadata(self, name): return self.egg_info and self._has(self._fn(self.egg_info,name)) - def get_metadata(self, name): - if not self.egg_info: - return "" - return self._get(self._fn(self.egg_info,name)) + if sys.version_info <= (3,): + def get_metadata(self, name): + if not self.egg_info: + return "" + return self._get(self._fn(self.egg_info,name)) + else: + def get_metadata(self, name): + if not self.egg_info: + return "" + return self._get(self._fn(self.egg_info,name)).decode("utf-8") def get_metadata_lines(self, name): return yield_lines(self.get_metadata(name)) @@ -1288,6 +1323,9 @@ class DefaultProvider(EggProvider): register_loader_type(type(None), DefaultProvider) +if importlib_bootstrap is not None: + register_loader_type(importlib_bootstrap.SourceFileLoader, DefaultProvider) + class EmptyProvider(NullProvider): """Provider that returns nothing for all requests""" @@ -1364,6 +1402,10 @@ class ZipProvider(EggProvider): timestamp = time.mktime(date_time) 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) ) @@ -1489,7 +1531,10 @@ class FileMetadata(EmptyProvider): def get_metadata(self,name): if name=='PKG-INFO': - return open(self.path,'rU').read() + f = open(self.path,'rU') + metadata = f.read() + f.close() + return metadata raise KeyError("No metadata except PKG-INFO is available") def get_metadata_lines(self,name): @@ -1694,7 +1739,7 @@ def find_on_path(importer, path_item, only=False): # scan for .egg and .egg-info in directory for entry in os.listdir(path_item): lower = entry.lower() - if lower.endswith('.egg-info'): + if lower.endswith('.egg-info') or lower.endswith('.dist-info'): fullpath = os.path.join(path_item, entry) if os.path.isdir(fullpath): # egg-info directory, allow getting metadata @@ -1708,15 +1753,24 @@ 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 file(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) -_declare_state('dict', _namespace_handlers = {}) -_declare_state('dict', _namespace_packages = {}) +if importlib_bootstrap is not None: + register_finder(importlib_bootstrap.FileFinder, find_on_path) + +_declare_state('dict', _namespace_handlers={}) +_declare_state('dict', _namespace_packages={}) + def register_namespace_handler(importer_type, namespace_handler): """Register `namespace_handler` to declare namespace packages @@ -1768,7 +1822,8 @@ def declare_namespace(packageName): if '.' in packageName: parent = '.'.join(packageName.split('.')[:-1]) declare_namespace(parent) - __import__(parent) + if parent not in _namespace_packages: + __import__(parent) try: path = sys.modules[parent].__path__ except AttributeError: @@ -1812,6 +1867,9 @@ def file_ns_handler(importer, path_item, packageName, module): register_namespace_handler(ImpWrapper,file_ns_handler) register_namespace_handler(zipimport.zipimporter,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): return None @@ -2040,12 +2098,19 @@ class EntryPoint(object): parse_map = classmethod(parse_map) - - +def _remove_md5_fragment(location): + if not location: + return '' + parsed = urlparse(location) + if parsed[-1].startswith('md5='): + return urlunparse(parsed[:-1] + ('',)) + return 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 @@ -2063,27 +2128,47 @@ class Distribution(object): def from_location(cls,location,basename,metadata=None,**kw): project_name, version, py_version, platform = [None]*4 basename, ext = os.path.splitext(basename) - if ext.lower() in (".egg",".egg-info"): + if ext.lower() in _distributionImpl: + # .dist-info gets much metadata differently match = EGG_NAME(basename) if match: project_name, version, py_version, platform = match.group( 'name','ver','pyver','plat' ) + cls = _distributionImpl[ext.lower()] return cls( location, metadata, project_name=project_name, version=version, py_version=py_version, platform=platform, **kw ) from_location = classmethod(from_location) + hashcmp = property( lambda self: ( - getattr(self,'parsed_version',()), self.precedence, self.key, - -len(self.location or ''), self.location, self.py_version, + getattr(self,'parsed_version',()), + self.precedence, + self.key, + _remove_md5_fragment(self.location), + self.py_version, self.platform ) ) - def __cmp__(self, other): return cmp(self.hashcmp, other) def __hash__(self): return hash(self.hashcmp) + def __lt__(self, other): + return self.hashcmp < other.hashcmp + def __le__(self, other): + return self.hashcmp <= other.hashcmp + def __gt__(self, other): + return self.hashcmp > other.hashcmp + def __ge__(self, other): + return self.hashcmp >= other.hashcmp + def __eq__(self, other): + if not isinstance(other, self.__class__): + # It's not a Distribution, so they are not equal + return False + return self.hashcmp == other.hashcmp + def __ne__(self, other): + return not self == other # These properties have to be lazy so that we don't have to load any # metadata until/unless it's actually needed. (i.e., some distributions @@ -2113,13 +2198,13 @@ class Distribution(object): try: return self._version except AttributeError: - for line in self._get_metadata('PKG-INFO'): + for line in self._get_metadata(self.PKG_INFO): if line.lower().startswith('version:'): self._version = safe_version(line.split(':',1)[1].strip()) return self._version else: raise ValueError( - "Missing 'Version:' header and/or PKG-INFO file", self + "Missing 'Version:' header and/or %s file" % self.PKG_INFO, self ) version = property(version) @@ -2301,9 +2386,11 @@ class Distribution(object): or modname in _namespace_packages ): continue - + if modname in ('pkg_resources', 'setuptools', 'site'): + continue fn = getattr(sys.modules[modname], '__file__', None) - if fn and (normalize_path(fn).startswith(loc) or fn.startswith(loc)): + if fn and (normalize_path(fn).startswith(loc) or + fn.startswith(self.location)): continue issue_warning( "Module %s was already imported from %s, but %s is being added" @@ -2337,6 +2424,74 @@ class Distribution(object): extras = property(extras) +class DistInfoDistribution(Distribution): + """Wrap an actual or potential sys.path entry w/metadata, .dist-info style""" + PKG_INFO = 'METADATA' + EQEQ = re.compile(r"([\(,])\s*(\d.*?)\s*([,\)])") + + @property + def _parsed_pkg_info(self): + """Parse and cache metadata""" + try: + return self._pkg_info + except AttributeError: + 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: + return self.__dep_map + except AttributeError: + self.__dep_map = self._compute_dependencies() + return self.__dep_map + + def _preparse_requirement(self, requires_dist): + """Convert 'Foobar (1); baz' to ('Foobar ==1', 'baz') + Split environment marker, add == prefix to version specifiers as + necessary, and remove parenthesis. + """ + parts = requires_dist.split(';', 1) + [''] + distvers = parts[0].strip() + mark = parts[1].strip() + 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 + dm = self.__dep_map = {None: []} + + reqs = [] + # Including any condition expressions + for req in self._parsed_pkg_info.get_all('Requires-Dist') or []: + distvers, mark = self._preparse_requirement(req) + parsed = parse_requirements(distvers).next() + 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}): + yield req + + 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, + '.dist-info': DistInfoDistribution } + + def issue_warning(*args,**kw): level = 1 g = globals() @@ -2485,8 +2640,9 @@ class Requirement: elif isinstance(item,basestring): item = parse_version(item) last = None + compare = lambda a, b: (a > b) - (a < b) # -1, 0, 1 for parsed,trans,op,ver in self.index: - action = trans[cmp(item,parsed)] + action = trans[compare(item,parsed)] # Indexing: 0, 1, -1 if action=='F': return False elif action=='T': return True elif action=='+': last = True |
