summaryrefslogtreecommitdiff
path: root/pkg_resources/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'pkg_resources/__init__.py')
-rw-r--r--pkg_resources/__init__.py390
1 files changed, 88 insertions, 302 deletions
diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py
index d04cd347..2eab8230 100644
--- a/pkg_resources/__init__.py
+++ b/pkg_resources/__init__.py
@@ -28,8 +28,6 @@ import warnings
import stat
import functools
import pkgutil
-import token
-import symbol
import operator
import platform
import collections
@@ -67,14 +65,11 @@ try:
except ImportError:
importlib_machinery = None
-try:
- import parser
-except ImportError:
- pass
-
from pkg_resources.extern import packaging
__import__('pkg_resources.extern.packaging.version')
__import__('pkg_resources.extern.packaging.specifiers')
+__import__('pkg_resources.extern.packaging.requirements')
+__import__('pkg_resources.extern.packaging.markers')
if (3, 0) < sys.version_info < (3, 3):
@@ -797,6 +792,8 @@ class WorkingSet(object):
best = {}
to_activate = []
+ req_extras = _ReqExtras()
+
# Mapping of requirement to set of distributions that required it;
# useful for reporting info about conflicts.
required_by = collections.defaultdict(set)
@@ -807,6 +804,10 @@ class WorkingSet(object):
if req in processed:
# Ignore cyclic or redundant dependencies
continue
+
+ if not req_extras.markers_pass(req):
+ continue
+
dist = best.get(req.key)
if dist is None:
# Find the best distribution and add it to the map
@@ -839,6 +840,7 @@ class WorkingSet(object):
# Register the new requirements needed by req
for new_requirement in new_requirements:
required_by[new_requirement].add(req.project_name)
+ req_extras[new_requirement] = req.extras
processed[req] = True
@@ -971,6 +973,26 @@ class WorkingSet(object):
self.callbacks = callbacks[:]
+class _ReqExtras(dict):
+ """
+ Map each requirement to the extras that demanded it.
+ """
+
+ def markers_pass(self, req):
+ """
+ Evaluate markers for req against each extra that
+ demanded it.
+
+ Return False if the req has a marker and fails
+ evaluation. Otherwise, return True.
+ """
+ extra_evals = (
+ req.marker.evaluate({'extra': extra})
+ for extra in self.get(req, ()) + (None,)
+ )
+ return not req.marker or any(extra_evals)
+
+
class Environment(object):
"""Searchable snapshot of distributions on a search path"""
@@ -1385,202 +1407,34 @@ def to_filename(name):
return name.replace('-','_')
-class MarkerEvaluation(object):
- values = {
- 'os_name': lambda: os.name,
- 'sys_platform': lambda: sys.platform,
- 'python_full_version': platform.python_version,
- 'python_version': lambda: platform.python_version()[:3],
- 'platform_version': platform.version,
- 'platform_machine': platform.machine,
- 'platform_python_implementation': platform.python_implementation,
- 'python_implementation': platform.python_implementation,
- }
-
- @classmethod
- def is_invalid_marker(cls, text):
- """
- Validate text as a PEP 426 environment marker; return an exception
- if invalid or False otherwise.
- """
- try:
- cls.evaluate_marker(text)
- except SyntaxError as e:
- return cls.normalize_exception(e)
- return False
-
- @staticmethod
- def normalize_exception(exc):
- """
- Given a SyntaxError from a marker evaluation, normalize the error
- message:
- - Remove indications of filename and line number.
- - Replace platform-specific error messages with standard error
- messages.
- """
- subs = {
- 'unexpected EOF while parsing': 'invalid syntax',
- 'parenthesis is never closed': 'invalid syntax',
- }
- exc.filename = None
- exc.lineno = None
- exc.msg = subs.get(exc.msg, exc.msg)
- return exc
-
- @classmethod
- def and_test(cls, nodelist):
- # MUST NOT short-circuit evaluation, or invalid syntax can be skipped!
- items = [
- cls.interpret(nodelist[i])
- for i in range(1, len(nodelist), 2)
- ]
- return functools.reduce(operator.and_, items)
-
- @classmethod
- def test(cls, nodelist):
- # MUST NOT short-circuit evaluation, or invalid syntax can be skipped!
- items = [
- cls.interpret(nodelist[i])
- for i in range(1, len(nodelist), 2)
- ]
- return functools.reduce(operator.or_, items)
-
- @classmethod
- def atom(cls, nodelist):
- t = nodelist[1][0]
- if t == token.LPAR:
- if nodelist[2][0] == token.RPAR:
- raise SyntaxError("Empty parentheses")
- return cls.interpret(nodelist[2])
- msg = "Language feature not supported in environment markers"
- raise SyntaxError(msg)
-
- @classmethod
- def comparison(cls, nodelist):
- if len(nodelist) > 4:
- msg = "Chained comparison not allowed in environment markers"
- raise SyntaxError(msg)
- comp = nodelist[2][1]
- cop = comp[1]
- if comp[0] == token.NAME:
- if len(nodelist[2]) == 3:
- if cop == 'not':
- cop = 'not in'
- else:
- cop = 'is not'
- try:
- cop = cls.get_op(cop)
- except KeyError:
- msg = repr(cop) + " operator not allowed in environment markers"
- raise SyntaxError(msg)
- return cop(cls.evaluate(nodelist[1]), cls.evaluate(nodelist[3]))
-
- @classmethod
- def get_op(cls, op):
- ops = {
- symbol.test: cls.test,
- symbol.and_test: cls.and_test,
- symbol.atom: cls.atom,
- symbol.comparison: cls.comparison,
- 'not in': lambda x, y: x not in y,
- 'in': lambda x, y: x in y,
- '==': operator.eq,
- '!=': operator.ne,
- '<': operator.lt,
- '>': operator.gt,
- '<=': operator.le,
- '>=': operator.ge,
- }
- if hasattr(symbol, 'or_test'):
- ops[symbol.or_test] = cls.test
- return ops[op]
-
- @classmethod
- def evaluate_marker(cls, text, extra=None):
- """
- 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.
- """
- return cls.interpret(parser.expr(text).totuple(1)[1])
-
- @staticmethod
- def _translate_metadata2(env):
- """
- Markerlib implements Metadata 1.2 (PEP 345) environment markers.
- Translate the variables to Metadata 2.0 (PEP 426).
- """
- return dict(
- (key.replace('.', '_'), value)
- for key, value in env.items()
- )
-
- @classmethod
- def _markerlib_evaluate(cls, 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
-
- env = cls._translate_metadata2(_markerlib.default_environment())
- try:
- result = _markerlib.interpret(text, env)
- except NameError as e:
- raise SyntaxError(e.args[0])
- return result
-
- if 'parser' not in globals():
- # Fall back to less-complete _markerlib implementation if 'parser' module
- # is not available.
- evaluate_marker = _markerlib_evaluate
+def invalid_marker(text):
+ """
+ Validate text as a PEP 508 environment marker; return an exception
+ if invalid or False otherwise.
+ """
+ try:
+ evaluate_marker(text)
+ except SyntaxError as e:
+ e.filename = None
+ e.lineno = None
+ return e
+ return False
- @classmethod
- def interpret(cls, nodelist):
- while len(nodelist)==2: nodelist = nodelist[1]
- try:
- op = cls.get_op(nodelist[0])
- except KeyError:
- raise SyntaxError("Comparison or logical expression expected")
- return op(nodelist)
- @classmethod
- def evaluate(cls, nodelist):
- while len(nodelist)==2: nodelist = nodelist[1]
- kind = nodelist[0]
- name = nodelist[1]
- if kind==token.NAME:
- try:
- op = cls.values[name]
- except KeyError:
- raise SyntaxError("Unknown name %r" % name)
- return op()
- if kind==token.STRING:
- s = nodelist[1]
- if not cls._safe_string(s):
- raise SyntaxError(
- "Only plain strings allowed in environment markers")
- return s[1:-1]
- msg = "Language feature not supported in environment markers"
- raise SyntaxError(msg)
+def evaluate_marker(text, extra=None):
+ """
+ Evaluate a PEP 508 environment marker.
+ Return a boolean indicating the marker result in this environment.
+ Raise SyntaxError if marker is invalid.
- @staticmethod
- def _safe_string(cand):
- return (
- cand[:1] in "'\"" and
- not cand.startswith('"""') and
- not cand.startswith("'''") and
- '\\' not in cand
- )
+ This implementation uses the 'pyparsing' module.
+ """
+ try:
+ marker = packaging.markers.Marker(text)
+ return marker.evaluate()
+ except packaging.markers.InvalidMarker as e:
+ raise SyntaxError(e)
-invalid_marker = MarkerEvaluation.is_invalid_marker
-evaluate_marker = MarkerEvaluation.evaluate_marker
class NullProvider:
"""Try to implement resources and metadata for arbitrary PEP 302 loaders"""
@@ -2005,7 +1859,13 @@ class FileMetadata(EmptyProvider):
def get_metadata(self, name):
if name=='PKG-INFO':
with io.open(self.path, encoding='utf-8') as f:
- metadata = f.read()
+ try:
+ metadata = f.read()
+ except UnicodeDecodeError as exc:
+ # add path context to error message
+ tmpl = " in {self.path}"
+ exc.reason += tmpl.format(self=self)
+ raise
return metadata
raise KeyError("No metadata except PKG-INFO is available")
@@ -2194,12 +2054,13 @@ def _rebuild_mod_path(orig_path, package_name, module):
corresponding to their sys.path order
"""
sys_path = [_normalize_cached(p) for p in sys.path]
- def position_in_sys_path(p):
+ def position_in_sys_path(path):
"""
Return the ordinal of the path based on its position in sys.path
"""
- parts = p.split(os.sep)
- parts = parts[:-(package_name.count('.') + 1)]
+ path_parts = path.split(os.sep)
+ module_parts = package_name.count('.') + 1
+ parts = path_parts[:-module_parts]
return sys_path.index(_normalize_cached(os.sep.join(parts)))
orig_path.sort(key=position_in_sys_path)
@@ -2314,18 +2175,6 @@ def yield_lines(strs):
for s in yield_lines(ss):
yield s
-# whitespace and comment
-LINE_END = re.compile(r"\s*(#.*)?$").match
-# line continuation
-CONTINUE = re.compile(r"\s*\\\s*(#.*)?$").match
-# Distribution or extra
-DISTRO = re.compile(r"\s*((\w|[-.])+)").match
-# ver. info
-VERSION = re.compile(r"\s*(<=?|>=?|===?|!=|~=)\s*((\w|[-.*_!+])+)").match
-# comma between items
-COMMA = re.compile(r"\s*,").match
-OBRACKET = re.compile(r"\s*\[").match
-CBRACKET = re.compile(r"\s*\]").match
MODULE = re.compile(r"\w+(\.\w+)*$").match
EGG_NAME = re.compile(
r"""
@@ -2864,34 +2713,18 @@ class DistInfoDistribution(Distribution):
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 = next(parse_requirements(distvers))
- parsed.marker_fn = compile_marker(mark)
- reqs.append(parsed)
+ reqs.extend(parse_requirements(req))
def reqs_for_extra(extra):
for req in reqs:
- if req.marker_fn(override={'extra':extra}):
+ if not req.marker or req.marker.evaluate({'extra': extra}):
yield req
common = frozenset(reqs_for_extra(None))
@@ -2937,85 +2770,38 @@ def parse_requirements(strs):
# create a steppable iterator, so we can handle \-continuations
lines = iter(yield_lines(strs))
- def scan_list(ITEM, TERMINATOR, line, p, groups, item_name):
-
- items = []
-
- while not TERMINATOR(line, p):
- if CONTINUE(line, p):
- try:
- line = next(lines)
- p = 0
- except StopIteration:
- msg = "\\ must not appear on the last nonblank line"
- raise RequirementParseError(msg)
-
- match = ITEM(line, p)
- if not match:
- msg = "Expected " + item_name + " in"
- raise RequirementParseError(msg, line, "at", line[p:])
-
- items.append(match.group(*groups))
- p = match.end()
-
- match = COMMA(line, p)
- if match:
- # skip the comma
- p = match.end()
- elif not TERMINATOR(line, p):
- msg = "Expected ',' or end-of-list in"
- raise RequirementParseError(msg, line, "at", line[p:])
-
- match = TERMINATOR(line, p)
- # skip the terminator, if any
- if match:
- p = match.end()
- return line, p, items
-
for line in lines:
- match = DISTRO(line)
- if not match:
- raise RequirementParseError("Missing distribution spec", line)
- project_name = match.group(1)
- p = match.end()
- extras = []
-
- match = OBRACKET(line, p)
- if match:
- p = match.end()
- line, p, extras = scan_list(
- DISTRO, CBRACKET, line, p, (1,), "'extra' name"
- )
-
- line, p, specs = scan_list(VERSION, LINE_END, line, p, (1, 2),
- "version spec")
- specs = [(op, val) for op, val in specs]
- yield Requirement(project_name, specs, extras)
-
-
-class Requirement:
- def __init__(self, project_name, specs, extras):
+ # Drop comments -- a hash without a space may be in a URL.
+ if ' #' in line:
+ line = line[:line.find(' #')]
+ # If there is a line continuation, drop it, and append the next line.
+ if line.endswith('\\'):
+ line = line[:-2].strip()
+ line += next(lines)
+ yield Requirement(line)
+
+
+class Requirement(packaging.requirements.Requirement):
+ def __init__(self, requirement_string):
"""DO NOT CALL THIS UNDOCUMENTED METHOD; use Requirement.parse()!"""
- self.unsafe_name, project_name = project_name, safe_name(project_name)
+ try:
+ super(Requirement, self).__init__(requirement_string)
+ except packaging.requirements.InvalidRequirement as e:
+ raise RequirementParseError(str(e))
+ self.unsafe_name = self.name
+ project_name = safe_name(self.name)
self.project_name, self.key = project_name, project_name.lower()
- self.specifier = packaging.specifiers.SpecifierSet(
- ",".join(["".join([x, y]) for x, y in specs])
- )
- self.specs = specs
- self.extras = tuple(map(safe_extra, extras))
+ self.specs = [
+ (spec.operator, spec.version) for spec in self.specifier]
+ self.extras = tuple(map(safe_extra, self.extras))
self.hashCmp = (
self.key,
self.specifier,
frozenset(self.extras),
+ str(self.marker) if self.marker else None,
)
self.__hash = hash(self.hashCmp)
- def __str__(self):
- extras = ','.join(self.extras)
- if extras:
- extras = '[%s]' % extras
- return '%s%s%s' % (self.project_name, extras, self.specifier)
-
def __eq__(self, other):
return (
isinstance(other, Requirement) and