diff options
Diffstat (limited to 'pkg_resources.py')
-rw-r--r-- | pkg_resources.py | 290 |
1 files changed, 199 insertions, 91 deletions
diff --git a/pkg_resources.py b/pkg_resources.py index 41c73d42..bdd738f2 100644 --- a/pkg_resources.py +++ b/pkg_resources.py @@ -41,14 +41,10 @@ if sys.version_info >= (3, 3) and sys.implementation.name == "cpython": 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 -# a distribution with the same version. -# -# The bootstrapping script for instance, will check if this -# attribute is present to decide wether to reinstall the package -_distribute = True +try: + import parser +except ImportError: + pass def _bypass_ensure_directory(name, mode=0777): # Sandbox-bypassing version of ensure_directory() @@ -98,6 +94,7 @@ _sget_none = _sset_none = lambda *args: None + def get_supported_platform(): """Return this platform's maximum compatible version. @@ -162,7 +159,7 @@ __all__ = [ # Parsing functions and string utilities 'parse_requirements', 'parse_version', 'safe_name', 'safe_version', 'get_platform', 'compatible_platforms', 'yield_lines', 'split_sections', - 'safe_extra', 'to_filename', + 'safe_extra', 'to_filename', 'invalid_marker', 'evaluate_marker', # filesystem utilities 'ensure_directory', 'normalize_path', @@ -250,9 +247,10 @@ def get_build_platform(): needs some hacks for Linux and Mac OS X. """ try: - from distutils.util import get_platform - except ImportError: + # Python 2.7 or >=3.2 from sysconfig import get_platform + except ImportError: + from distutils.util import get_platform plat = get_platform() if sys.platform == "darwin" and not plat.startswith('macosx-'): @@ -271,6 +269,12 @@ macosVersionString = re.compile(r"macosx-(\d+)\.(\d+)-(.*)") darwinVersionString = re.compile(r"darwin-(\d+)\.(\d+)\.(\d+)-(.*)") get_platform = get_build_platform # XXX backward compat + + + + + + def compatible_platforms(provided,required): """Can code for the `provided` platform run on the `required` platform? @@ -446,7 +450,7 @@ class WorkingSet(object): def add_entry(self, entry): """Add a path item to ``.entries``, finding any distributions on it - ``find_distributions(entry,True)`` is used to find distributions + ``find_distributions(entry, True)`` is used to find distributions corresponding to the path entry, and they are added. `entry` is always appended to ``.entries``, even if it is already present. (This is because ``sys.path`` can contain the same value more than @@ -553,7 +557,7 @@ class WorkingSet(object): keys2.append(dist.key) self._added_new(dist) - def resolve(self, requirements, env=None, installer=None, replacement=True): + def resolve(self, requirements, env=None, installer=None): """List all distributions needed to (recursively) meet `requirements` `requirements` must be a sequence of ``Requirement`` objects. `env`, @@ -572,9 +576,6 @@ class WorkingSet(object): while requirements: req = requirements.pop(0) # process dependencies breadth-first - if _override_setuptools(req) and replacement: - req = Requirement.parse('distribute') - if req in processed: # Ignore cyclic or redundant dependencies continue @@ -694,7 +695,6 @@ class WorkingSet(object): activated to fulfill the requirements; all relevant distributions are included, even if they were already activated in this working set. """ - needed = self.resolve(parse_requirements(requirements)) for dist in needed: @@ -702,7 +702,6 @@ class WorkingSet(object): return needed - def subscribe(self, callback): """Invoke `callback` for all distributions (including existing ones)""" if callback in self.callbacks: @@ -711,14 +710,15 @@ class WorkingSet(object): for dist in self: callback(dist) - def _added_new(self, dist): for callback in self.callbacks: callback(dist) def __getstate__(self): - return (self.entries[:], self.entry_keys.copy(), self.by_key.copy(), - self.callbacks[:]) + return ( + self.entries[:], self.entry_keys.copy(), self.by_key.copy(), + self.callbacks[:] + ) def __setstate__(self, (entries, keys, by_key, callbacks)): self.entries = entries[:] @@ -727,8 +727,6 @@ class WorkingSet(object): self.callbacks = callbacks[:] - - class Environment(object): """Searchable snapshot of distributions on a search path""" @@ -1205,6 +1203,160 @@ def to_filename(name): +_marker_names = { + 'os': ['name'], 'sys': ['platform'], + 'platform': ['version','machine','python_implementation'], + 'python_version': [], 'python_full_version': [], 'extra':[], +} + +_marker_values = { + 'os_name': lambda: os.name, + 'sys_platform': lambda: sys.platform, + 'python_full_version': lambda: sys.version.split()[0], + 'python_version': lambda:'%s.%s' % (sys.version_info[0], sys.version_info[1]), + 'platform_version': lambda: _platinfo('version'), + 'platform_machine': lambda: _platinfo('machine'), + 'python_implementation': lambda: _platinfo('python_implementation') or _pyimp(), +} + +def _platinfo(attr): + try: + import platform + except ImportError: + return '' + return getattr(platform, attr, lambda:'')() + +def _pyimp(): + if sys.platform=='cli': + return 'IronPython' + elif sys.platform.startswith('java'): + return 'Jython' + elif '__pypy__' in sys.builtin_module_names: + return 'PyPy' + else: + return 'CPython' + +def invalid_marker(text): + """Validate text as a PEP 426 environment marker; return exception or False""" + try: + evaluate_marker(text) + except SyntaxError: + return sys.exc_info()[1] + return False + +def evaluate_marker(text, extra=None, _ops={}): + """ + Evaluate a PEP 426 environment marker on CPython 2.4+. + Return a boolean indicating the marker result in this environment. + Raise SyntaxError if marker is invalid. + + This implementation uses the 'parser' module, which is not implemented on + Jython and has been superseded by the 'ast' module in Python 2.6 and + later. + """ + + if not _ops: + + from token import NAME, STRING + import token, symbol, operator + + def and_test(nodelist): + # MUST NOT short-circuit evaluation, or invalid syntax can be skipped! + return reduce(operator.and_, [interpret(nodelist[i]) for i in range(1,len(nodelist),2)]) + + def test(nodelist): + # MUST NOT short-circuit evaluation, or invalid syntax can be skipped! + return reduce(operator.or_, [interpret(nodelist[i]) for i in range(1,len(nodelist),2)]) + + def atom(nodelist): + t = nodelist[1][0] + if t == token.LPAR: + if nodelist[2][0] == token.RPAR: + raise SyntaxError("Empty parentheses") + return interpret(nodelist[2]) + raise SyntaxError("Language feature not supported in environment markers") + + def comparison(nodelist): + if len(nodelist)>4: + raise SyntaxError("Chained comparison not allowed in environment markers") + comp = nodelist[2][1] + cop = comp[1] + if comp[0] == NAME: + if len(nodelist[2]) == 3: + if cop == 'not': + cop = 'not in' + else: + cop = 'is not' + try: + cop = _ops[cop] + except KeyError: + raise SyntaxError(repr(cop)+" operator not allowed in environment markers") + return cop(evaluate(nodelist[1]), evaluate(nodelist[3])) + + _ops.update({ + symbol.test: test, symbol.and_test: and_test, symbol.atom: atom, + symbol.comparison: comparison, 'not in': lambda x,y: x not in y, + 'in': lambda x,y: x in y, '==': operator.eq, '!=': operator.ne, + }) + if hasattr(symbol,'or_test'): + _ops[symbol.or_test] = test + + def interpret(nodelist): + while len(nodelist)==2: nodelist = nodelist[1] + try: + op = _ops[nodelist[0]] + except KeyError: + raise SyntaxError("Comparison or logical expression expected") + raise SyntaxError("Language feature not supported in environment markers: "+symbol.sym_name[nodelist[0]]) + return op(nodelist) + + def evaluate(nodelist): + while len(nodelist)==2: nodelist = nodelist[1] + kind = nodelist[0] + name = nodelist[1] + #while len(name)==2: name = name[1] + if kind==NAME: + try: + op = _marker_values[name] + except KeyError: + raise SyntaxError("Unknown name %r" % name) + return op() + if kind==STRING: + s = nodelist[1] + if s[:1] not in "'\"" or s.startswith('"""') or s.startswith("'''") \ + or '\\' in s: + raise SyntaxError( + "Only plain strings allowed in environment markers") + return s[1:-1] + raise SyntaxError("Language feature not supported in environment markers") + + return interpret(parser.expr(text).totuple(1)[1]) + +def _markerlib_evaluate(text): + """ + Evaluate a PEP 426 environment marker using markerlib. + Return a boolean indicating the marker result in this environment. + Raise SyntaxError if marker is invalid. + """ + import _markerlib + # markerlib implements Metadata 1.2 (PEP 345) environment markers. + # Translate the variables to Metadata 2.0 (PEP 426). + env = _markerlib.default_environment() + for key in env.keys(): + new_key = key.replace('.', '_') + env[new_key] = env.pop(key) + try: + result = _markerlib.interpret(text, env) + except NameError: + e = sys.exc_info()[1] + raise SyntaxError(e.args[0]) + return result + +if 'parser' not in globals(): + # fallback to less-complete _markerlib implementation if 'parser' module + # is not available. + evaluate_marker = _markerlib_evaluate + class NullProvider: """Try to implement resources and metadata for arbitrary PEP 302 loaders""" @@ -1879,7 +2031,7 @@ def _handle_ns(packageName, path_item): return None module = sys.modules.get(packageName) if module is None: - module = sys.modules[packageName] = types.ModuleType(packageName) + module = sys.modules[packageName] = imp.new_module(packageName) module.__path__ = []; _set_parent_ns(packageName) elif not hasattr(module,'__path__'): raise TypeError("Not a package:", packageName) @@ -2058,7 +2210,6 @@ def parse_version(s): parts.pop() parts.append(part) return tuple(parts) - class EntryPoint(object): """Object representing an advertised importable object""" @@ -2299,7 +2450,14 @@ class Distribution(object): dm = self.__dep_map = {None: []} for name in 'requires.txt', 'depends.txt': for extra,reqs in split_sections(self._get_metadata(name)): - if extra: extra = safe_extra(extra) + if extra: + if ':' in extra: + extra, marker = extra.split(':',1) + if invalid_marker(marker): + reqs=[] # XXX warn + elif not evaluate_marker(marker): + reqs=[] + extra = safe_extra(extra) or None dm.setdefault(extra,[]).extend(parse_requirements(reqs)) return dm _dep_map = property(_dep_map) @@ -2323,6 +2481,8 @@ class Distribution(object): for line in self.get_metadata_lines(name): yield line + + def activate(self,path=None): """Ensure distribution is importable on `path` (default=sys.path)""" if path is None: path = sys.path @@ -2361,6 +2521,9 @@ class Distribution(object): raise AttributeError,attr return getattr(self._provider, attr) + + + #@classmethod def from_filename(cls,filename,metadata=None, **kw): return cls.from_location( @@ -2402,42 +2565,16 @@ class Distribution(object): - - - - - - - - - - - - def insert_on(self, path, loc = None): """Insert self.location in path before its nearest parent directory""" loc = loc or self.location - - if self.project_name == 'setuptools': - try: - version = self.version - except ValueError: - version = '' - if '0.7' in version: - raise ValueError( - "A 0.7-series setuptools cannot be installed " - "with distribute. Found one at %s" % str(self.location)) - if not loc: return - if path is sys.path: - self.check_version_conflict() - nloc = _normalize_cached(loc) bdir = os.path.dirname(nloc) - npath= map(_normalize_cached, path) + npath= [(p and _normalize_cached(p) or p) for p in path] bp = None for p, item in enumerate(npath): @@ -2445,10 +2582,14 @@ class Distribution(object): break elif item==bdir and self.precedence==EGG_DIST: # if it's an .egg, give it precedence over its directory + if path is sys.path: + self.check_version_conflict() path.insert(p, loc) npath.insert(p, nloc) break else: + if path is sys.path: + self.check_version_conflict() path.append(loc) return @@ -2465,9 +2606,8 @@ class Distribution(object): return - def check_version_conflict(self): - if self.key=='distribute': + if self.key=='setuptools': return # ignore the inevitable setuptools self-conflicts :( nsp = dict.fromkeys(self._get_metadata('namespace_packages.txt')) @@ -2726,7 +2866,7 @@ class Requirement: def __contains__(self,item): if isinstance(item,Distribution): - if item.key <> self.key: return False + if item.key != self.key: return False if self.index: item = item.parsed_version # only get if we need it elif isinstance(item,basestring): item = parse_version(item) @@ -2748,22 +2888,11 @@ class Requirement: def __repr__(self): return "Requirement.parse(%r)" % str(self) #@staticmethod - def parse(s, replacement=True): + def parse(s): reqs = list(parse_requirements(s)) if reqs: - if len(reqs) == 1: - founded_req = reqs[0] - # if asked for setuptools distribution - # and if distribute is installed, we want to give - # distribute instead - if _override_setuptools(founded_req) and replacement: - distribute = list(parse_requirements('distribute')) - if len(distribute) == 1: - return distribute[0] - return founded_req - else: - return founded_req - + if len(reqs)==1: + return reqs[0] raise ValueError("Expected only one requirement", s) raise ValueError("No requirements found", s) @@ -2780,26 +2909,6 @@ state_machine = { } -def _override_setuptools(req): - """Return True when distribute wants to override a setuptools dependency. - - We want to override when the requirement is setuptools and the version is - a variant of 0.6. - - """ - if req.project_name == 'setuptools': - if not len(req.specs): - # Just setuptools: ok - return True - for comparator, version in req.specs: - if comparator in ['==', '>=', '>']: - if '0.7' in version: - # We want some setuptools not from the 0.6 series. - return False - return True - return False - - def _get_mro(cls): """Get an mro for a type or classic class""" if not isinstance(cls,type): @@ -2865,7 +2974,6 @@ _initialize(globals()) # Prepare the master working set and make the ``require()`` API available _declare_state('object', working_set = WorkingSet()) - try: # Does the main program list any requirements? from __main__ import __requires__ |