Commit a718819d authored by Steve Kowalik's avatar Steve Kowalik

First shot at removing usage of _markerlib and switching to the PEP 508...

First shot at removing usage of _markerlib and switching to the PEP 508 implementation in packaging.
parent dadd14d8
...@@ -3,5 +3,8 @@ empty: ...@@ -3,5 +3,8 @@ empty:
update-vendored: update-vendored:
rm -rf pkg_resources/_vendor/packaging rm -rf pkg_resources/_vendor/packaging
rm -rf pkg_resources/_vendor/pyparsing
pip install -r pkg_resources/_vendor/vendored.txt -t pkg_resources/_vendor/ pip install -r pkg_resources/_vendor/vendored.txt -t pkg_resources/_vendor/
sed -i -e 's/ \(pyparsing\)/ pkg_resources._vendor.\1/' \
pkg_resources/_vendor/packaging/*.py
rm -rf pkg_resources/_vendor/*.{egg,dist}-info rm -rf pkg_resources/_vendor/*.{egg,dist}-info
try:
import ast
from _markerlib.markers import default_environment, compile, interpret
except ImportError:
if 'ast' in globals():
raise
def default_environment():
return {}
def compile(marker):
def marker_fn(environment=None, override=None):
# 'empty markers are True' heuristic won't install extra deps.
return not marker.strip()
marker_fn.__doc__ = marker
return marker_fn
def interpret(marker, environment=None, override=None):
return compile(marker)()
# -*- coding: utf-8 -*-
"""Interpret PEP 345 environment markers.
EXPR [in|==|!=|not in] EXPR [or|and] ...
where EXPR belongs to any of those:
python_version = '%s.%s' % (sys.version_info[0], sys.version_info[1])
python_full_version = sys.version.split()[0]
os.name = os.name
sys.platform = sys.platform
platform.version = platform.version()
platform.machine = platform.machine()
platform.python_implementation = platform.python_implementation()
a free string, like '2.6', or 'win32'
"""
__all__ = ['default_environment', 'compile', 'interpret']
import ast
import os
import platform
import sys
import weakref
_builtin_compile = compile
try:
from platform import python_implementation
except ImportError:
if os.name == "java":
# Jython 2.5 has ast module, but not platform.python_implementation() function.
def python_implementation():
return "Jython"
else:
raise
# restricted set of variables
_VARS = {'sys.platform': sys.platform,
'python_version': '%s.%s' % sys.version_info[:2],
# FIXME parsing sys.platform is not reliable, but there is no other
# way to get e.g. 2.7.2+, and the PEP is defined with sys.version
'python_full_version': sys.version.split(' ', 1)[0],
'os.name': os.name,
'platform.version': platform.version(),
'platform.machine': platform.machine(),
'platform.python_implementation': python_implementation(),
'extra': None # wheel extension
}
for var in list(_VARS.keys()):
if '.' in var:
_VARS[var.replace('.', '_')] = _VARS[var]
def default_environment():
"""Return copy of default PEP 385 globals dictionary."""
return dict(_VARS)
class ASTWhitelist(ast.NodeTransformer):
def __init__(self, statement):
self.statement = statement # for error messages
ALLOWED = (ast.Compare, ast.BoolOp, ast.Attribute, ast.Name, ast.Load, ast.Str)
# Bool operations
ALLOWED += (ast.And, ast.Or)
# Comparison operations
ALLOWED += (ast.Eq, ast.Gt, ast.GtE, ast.In, ast.Is, ast.IsNot, ast.Lt, ast.LtE, ast.NotEq, ast.NotIn)
def visit(self, node):
"""Ensure statement only contains allowed nodes."""
if not isinstance(node, self.ALLOWED):
raise SyntaxError('Not allowed in environment markers.\n%s\n%s' %
(self.statement,
(' ' * node.col_offset) + '^'))
return ast.NodeTransformer.visit(self, node)
def visit_Attribute(self, node):
"""Flatten one level of attribute access."""
new_node = ast.Name("%s.%s" % (node.value.id, node.attr), node.ctx)
return ast.copy_location(new_node, node)
def parse_marker(marker):
tree = ast.parse(marker, mode='eval')
new_tree = ASTWhitelist(marker).generic_visit(tree)
return new_tree
def compile_marker(parsed_marker):
return _builtin_compile(parsed_marker, '<environment marker>', 'eval',
dont_inherit=True)
_cache = weakref.WeakValueDictionary()
def compile(marker):
"""Return compiled marker as a function accepting an environment dict."""
try:
return _cache[marker]
except KeyError:
pass
if not marker.strip():
def marker_fn(environment=None, override=None):
""""""
return True
else:
compiled_marker = compile_marker(parse_marker(marker))
def marker_fn(environment=None, override=None):
"""override updates environment"""
if override is None:
override = {}
if environment is None:
environment = default_environment()
environment.update(override)
return eval(compiled_marker, environment)
marker_fn.__doc__ = marker
_cache[marker] = marker_fn
return _cache[marker]
def interpret(marker, environment=None):
return compile(marker)(environment)
...@@ -90,6 +90,8 @@ except ImportError: ...@@ -90,6 +90,8 @@ except ImportError:
try: try:
import pkg_resources._vendor.packaging.version import pkg_resources._vendor.packaging.version
import pkg_resources._vendor.packaging.specifiers import pkg_resources._vendor.packaging.specifiers
import pkg_resources._vendor.packaging.requirements
import pkg_resources._vendor.packaging.markers
packaging = pkg_resources._vendor.packaging packaging = pkg_resources._vendor.packaging
except ImportError: except ImportError:
# fallback to naturally-installed version; allows system packagers to # fallback to naturally-installed version; allows system packagers to
...@@ -1420,13 +1422,15 @@ class MarkerEvaluation(object): ...@@ -1420,13 +1422,15 @@ class MarkerEvaluation(object):
@classmethod @classmethod
def is_invalid_marker(cls, text): def is_invalid_marker(cls, text):
""" """
Validate text as a PEP 426 environment marker; return an exception Validate text as a PEP 508 environment marker; return an exception
if invalid or False otherwise. if invalid or False otherwise.
""" """
try: try:
cls.evaluate_marker(text) cls.evaluate_marker(text)
except SyntaxError as e: except packaging.markers.InvalidMarker as e:
return cls.normalize_exception(e) e.filename = None
e.lineno = None
return e
return False return False
@staticmethod @staticmethod
...@@ -1518,48 +1522,14 @@ class MarkerEvaluation(object): ...@@ -1518,48 +1522,14 @@ class MarkerEvaluation(object):
@classmethod @classmethod
def evaluate_marker(cls, text, extra=None): def evaluate_marker(cls, text, extra=None):
""" """
Evaluate a PEP 426 environment marker on CPython 2.4+. Evaluate a PEP 508 environment marker on CPython 2.4+.
Return a boolean indicating the marker result in this environment. Return a boolean indicating the marker result in this environment.
Raise SyntaxError if marker is invalid. Raise InvalidMarker if marker is invalid.
This implementation uses the 'parser' module, which is not implemented This implementation uses the 'pyparsing' module.
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]) marker = packaging.markers.Marker(text)
return marker.evaluate()
@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
)
@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
@classmethod @classmethod
def interpret(cls, nodelist): def interpret(cls, nodelist):
...@@ -2314,18 +2284,6 @@ def yield_lines(strs): ...@@ -2314,18 +2284,6 @@ def yield_lines(strs):
for s in yield_lines(ss): for s in yield_lines(ss):
yield s 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 MODULE = re.compile(r"\w+(\.\w+)*$").match
EGG_NAME = re.compile( EGG_NAME = re.compile(
r""" r"""
...@@ -2861,34 +2819,22 @@ class DistInfoDistribution(Distribution): ...@@ -2861,34 +2819,22 @@ class DistInfoDistribution(Distribution):
self.__dep_map = self._compute_dependencies() self.__dep_map = self._compute_dependencies()
return self.__dep_map 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): def _compute_dependencies(self):
"""Recompute this distribution's dependencies.""" """Recompute this distribution's dependencies."""
from _markerlib import compile as compile_marker
dm = self.__dep_map = {None: []} dm = self.__dep_map = {None: []}
reqs = [] reqs = []
# Including any condition expressions # Including any condition expressions
for req in self._parsed_pkg_info.get_all('Requires-Dist') or []: for req in self._parsed_pkg_info.get_all('Requires-Dist') or []:
distvers, mark = self._preparse_requirement(req) current_req = packaging.requirements.Requirement(req)
parsed = next(parse_requirements(distvers)) specs = _parse_requirement_specs(current_req)
parsed.marker_fn = compile_marker(mark) parsed = Requirement(current_req.name, specs, current_req.extras)
parsed._marker = current_req.marker
reqs.append(parsed) reqs.append(parsed)
def reqs_for_extra(extra): def reqs_for_extra(extra):
for req in reqs: for req in reqs:
if req.marker_fn(override={'extra':extra}): if not req._marker or req._marker.evaluate({'extra': extra}):
yield req yield req
common = frozenset(reqs_for_extra(None)) common = frozenset(reqs_for_extra(None))
...@@ -2926,6 +2872,13 @@ class RequirementParseError(ValueError): ...@@ -2926,6 +2872,13 @@ class RequirementParseError(ValueError):
return ' '.join(self.args) return ' '.join(self.args)
def _parse_requirement_specs(req):
if req.specifier:
return [(spec.operator, spec.version) for spec in req.specifier]
else:
return []
def parse_requirements(strs): def parse_requirements(strs):
"""Yield ``Requirement`` objects for each specification in `strs` """Yield ``Requirement`` objects for each specification in `strs`
...@@ -2934,60 +2887,17 @@ def parse_requirements(strs): ...@@ -2934,60 +2887,17 @@ def parse_requirements(strs):
# create a steppable iterator, so we can handle \-continuations # create a steppable iterator, so we can handle \-continuations
lines = iter(yield_lines(strs)) 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: for line in lines:
match = DISTRO(line) # Drop comments -- a hash without a space may be in a URL.
if not match: if ' #' in line:
raise RequirementParseError("Missing distribution spec", line) line = line[:line.find(' #')]
project_name = match.group(1) # If there is a line continuation, drop it, and append the next line.
p = match.end() if line.endswith('\\'):
extras = [] line = line[:-2].strip()
line += next(lines)
match = OBRACKET(line, p) req = packaging.requirements.Requirement(line)
if match: specs = _parse_requirement_specs(req)
p = match.end() yield Requirement(req.name, specs, req.extras)
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: class Requirement:
......
# Copyright 2014 Donald Stufft # This file is dual licensed under the terms of the Apache License, Version
# # 2.0, and the BSD License. See the LICENSE file in the root of this repository
# Licensed under the Apache License, Version 2.0 (the "License"); # for complete details.
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__all__ = [ __all__ = [
...@@ -22,10 +12,10 @@ __title__ = "packaging" ...@@ -22,10 +12,10 @@ __title__ = "packaging"
__summary__ = "Core utilities for Python packages" __summary__ = "Core utilities for Python packages"
__uri__ = "https://github.com/pypa/packaging" __uri__ = "https://github.com/pypa/packaging"
__version__ = "15.3" __version__ = "15.4.dev0"
__author__ = "Donald Stufft" __author__ = "Donald Stufft and individual contributors"
__email__ = "donald@stufft.io" __email__ = "donald@stufft.io"
__license__ = "Apache License, Version 2.0" __license__ = "BSD or Apache License, Version 2.0"
__copyright__ = "Copyright 2014 %s" % __author__ __copyright__ = "Copyright 2014-2015 %s" % __author__
# Copyright 2014 Donald Stufft # This file is dual licensed under the terms of the Apache License, Version
# # 2.0, and the BSD License. See the LICENSE file in the root of this repository
# Licensed under the Apache License, Version 2.0 (the "License"); # for complete details.
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
from .__about__ import ( from .__about__ import (
......
# Copyright 2014 Donald Stufft # This file is dual licensed under the terms of the Apache License, Version
# # 2.0, and the BSD License. See the LICENSE file in the root of this repository
# Licensed under the Apache License, Version 2.0 (the "License"); # for complete details.
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import sys import sys
......
# Copyright 2014 Donald Stufft # This file is dual licensed under the terms of the Apache License, Version
# # 2.0, and the BSD License. See the LICENSE file in the root of this repository
# Licensed under the Apache License, Version 2.0 (the "License"); # for complete details.
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
......
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
from __future__ import absolute_import, division, print_function
import operator
import os
import platform
import sys
from pkg_resources._vendor.pyparsing import ParseException, ParseResults, stringStart, stringEnd
from pkg_resources._vendor.pyparsing import ZeroOrMore, Group, Forward, QuotedString
from pkg_resources._vendor.pyparsing import Literal as L # noqa
from ._compat import string_types
from .specifiers import Specifier, InvalidSpecifier
__all__ = [
"InvalidMarker", "UndefinedComparison", "Marker", "default_environment",
]
class InvalidMarker(ValueError):
"""
An invalid marker was found, users should refer to PEP 508.
"""
class UndefinedComparison(ValueError):
"""
An invalid operation was attempted on a value that doesn't support it.
"""
class Node(object):
def __init__(self, value):
self.value = value
def __str__(self):
return str(self.value)
def __repr__(self):
return "<{0}({1!r})>".format(self.__class__.__name__, str(self))
class Variable(Node):
pass
class Value(Node):
pass
VARIABLE = (
L("implementation_version") |
L("python_implementation") |
L("implementation_name") |
L("python_full_version") |
L("platform_release") |
L("platform_version") |
L("platform_machine") |
L("platform_system") |
L("python_version") |
L("sys_platform") |
L("os_name") |
L("extra")
)
VARIABLE.setParseAction(lambda s, l, t: Variable(t[0]))
VERSION_CMP = (
L("===") |
L("==") |
L(">=") |
L("<=") |
L("!=") |
L("~=") |
L(">") |
L("<")
)
MARKER_OP = VERSION_CMP | L("not in") | L("in")
MARKER_VALUE = QuotedString("'") | QuotedString('"')
MARKER_VALUE.setParseAction(lambda s, l, t: Value(t[0]))
BOOLOP = L("and") | L("or")
MARKER_VAR = VARIABLE | MARKER_VALUE
MARKER_ITEM = Group(MARKER_VAR + MARKER_OP + MARKER_VAR)
MARKER_ITEM.setParseAction(lambda s, l, t: tuple(t[0]))
LPAREN = L("(").suppress()
RPAREN = L(")").suppress()
MARKER_EXPR = Forward()
MARKER_ATOM = MARKER_ITEM | Group(LPAREN + MARKER_EXPR + RPAREN)
MARKER_EXPR << MARKER_ATOM + ZeroOrMore(BOOLOP + MARKER_EXPR)
MARKER = stringStart + MARKER_EXPR + stringEnd
def _coerce_parse_result(results):
if isinstance(results, ParseResults):
return [_coerce_parse_result(i) for i in results]
else:
return results
def _format_marker(marker, first=True):
assert isinstance(marker, (list, tuple, string_types))
# Sometimes we have a structure like [[...]] which is a single item list
# where the single item is itself it's own list. In that case we want skip
# the rest of this function so that we don't get extraneous () on the
# outside.
if (isinstance(marker, list) and len(marker) == 1
and isinstance(marker[0], (list, tuple))):
return _format_marker(marker[0])
if isinstance(marker, list):
inner = (_format_marker(m, first=False) for m in marker)
if first:
return " ".join(inner)
else:
return "(" + " ".join(inner) + ")"
elif isinstance(marker, tuple):
return '{0} {1} "{2}"'.format(*marker)
else:
return marker
_operators = {
"in": lambda lhs, rhs: lhs in rhs,
"not in": lambda lhs, rhs: lhs not in rhs,
"<": operator.lt,
"<=": operator.le,
"==": operator.eq,
"!=": operator.ne,
">=": operator.ge,
">": operator.gt,
}
def _eval_op(lhs, op, rhs):
try:
spec = Specifier("".join([op, rhs]))
except InvalidSpecifier:
pass
else:
return spec.contains(lhs)
oper = _operators.get(op)
if oper is None:
raise UndefinedComparison(
"Undefined {0!r} on {1!r} and {2!r}.".format(op, lhs, rhs)
)
return oper(lhs, rhs)
def _evaluate_markers(markers, environment):
groups = [[]]
for marker in markers:
assert isinstance(marker, (list, tuple, string_types))
if isinstance(marker, list):
groups[-1].append(_evaluate_markers(marker, environment))
elif isinstance(marker, tuple):
lhs, op, rhs = marker
if isinstance(lhs, Variable):
value = _eval_op(environment[lhs.value], op, rhs.value)
else:
value = _eval_op(lhs.value, op, environment[rhs.value])
groups[-1].append(value)
else:
assert marker in ["and", "or"]
if marker == "or":
groups.append([])
return any(all(item) for item in groups)
def format_full_version(info):
version = '{0.major}.{0.minor}.{0.micro}'.format(info)
kind = info.releaselevel
if kind != 'final':
version += kind[0] + str(info.serial)
return version
def default_environment():
if hasattr(sys, 'implementation'):
iver = format_full_version(sys.implementation.version)
implementation_name = sys.implementation.name
else:
iver = '0'
implementation_name = ''
return {
"implementation_name": implementation_name,
"implementation_version": iver,
"os_name": os.name,
"platform_machine": platform.machine(),
"platform_release": platform.release(),
"platform_system": platform.system(),
"platform_version": platform.version(),
"python_full_version": platform.python_version(),
"python_implementation": platform.python_implementation(),
"python_version": platform.python_version()[:3],
"sys_platform": sys.platform,
}
class Marker(object):
def __init__(self, marker):
try:
self._markers = _coerce_parse_result(MARKER.parseString(marker))
except ParseException:
self._markers = None
# We do this because we can't do raise ... from None in Python 2.x
if self._markers is None:
raise InvalidMarker("Invalid marker: {0!r}".format(marker))
def __str__(self):
return _format_marker(self._markers)
def __repr__(self):
return "<Marker({0!r})>".format(str(self))
def evaluate(self, environment=None):
"""Evaluate a marker.
Return the boolean from evaluating the given marker against the
environment. environment is an optional argument to override all or
part of the determined environment.
The environment is determined from the current Python process.
"""
current_environment = default_environment()
if environment is not None:
current_environment.update(environment)
return _evaluate_markers(self._markers, current_environment)
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
from __future__ import absolute_import, division, print_function
import string
import re
from pkg_resources._vendor.pyparsing import stringStart, stringEnd, originalTextFor, ParseException
from pkg_resources._vendor.pyparsing import ZeroOrMore, Word, Optional, Regex, Combine
from pkg_resources._vendor.pyparsing import Literal as L # noqa
from six.moves.urllib import parse as urlparse
from .markers import MARKER_EXPR, Marker
from .specifiers import LegacySpecifier, Specifier, SpecifierSet
class InvalidRequirement(ValueError):
"""
An invalid requirement was found, users should refer to PEP 508.
"""
ALPHANUM = Word(string.ascii_letters + string.digits)
LBRACKET = L("[").suppress()
RBRACKET = L("]").suppress()
LPAREN = L("(").suppress()
RPAREN = L(")").suppress()
COMMA = L(",").suppress()
SEMICOLON = L(";").suppress()
AT = L("@").suppress()
PUNCTUATION = Word("-_.")
IDENTIFIER_END = ALPHANUM | (ZeroOrMore(PUNCTUATION) + ALPHANUM)
IDENTIFIER = Combine(ALPHANUM + ZeroOrMore(IDENTIFIER_END))
NAME = IDENTIFIER("name")
EXTRA = IDENTIFIER
URI = Regex(r'[^ ]+')("url")
URL = (AT + URI)
EXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA)
EXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)("extras")
VERSION_PEP440 = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE)
VERSION_LEGACY = Regex(LegacySpecifier._regex_str, re.VERBOSE | re.IGNORECASE)
VERSION_ONE = VERSION_PEP440 | VERSION_LEGACY
VERSION_MANY = VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE)
VERSION_MANY.setParseAction(
lambda s, l, t: SpecifierSet(','.join(t.asList()))
)
VERSION_SPEC = ((LPAREN + VERSION_MANY + RPAREN) | VERSION_MANY)("specifier")
MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker")
MARKER_EXPR.setParseAction(
lambda s, l, t: Marker(s[t._original_start:t._original_end])
)
MARKER_SEPERATOR = SEMICOLON
MARKER = MARKER_SEPERATOR + MARKER_EXPR
VERSION_AND_MARKER = Optional(VERSION_SPEC) + Optional(MARKER)
URL_AND_MARKER = URL + Optional(MARKER)
NAMED_REQUIREMENT = \
NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER)
REQUIREMENT = stringStart + NAMED_REQUIREMENT + stringEnd
class Requirement(object):
# TODO: Can we test whether something is contained within a requirement?
# If so how do we do that? Do we need to test against the _name_ of
# the thing as well as the version? What about the markers?
# TODO: Can we normalize the name and extra name?
def __init__(self, requirement_string):
try:
req = REQUIREMENT.parseString(requirement_string)
except ParseException:
raise InvalidRequirement(
"Invalid requirement: {0!r}".format(requirement_string))
self.name = req.name
if req.url:
parsed_url = urlparse.urlparse(req.url)
if not (parsed_url.scheme and parsed_url.netloc) or (
not parsed_url.scheme and not parsed_url.netloc):
raise InvalidRequirement("Invalid URL given")
self.url = req.url
else:
self.url = None
self.extras = req.extras.asList() if req.extras else []
self.specifier = req.specifier if req.specifier else None
self.marker = req.marker if req.marker else None
def __str__(self):
parts = [self.name]
if self.extras:
parts.append("[{0}]".format(",".join(sorted(self.extras))))
if self.specifier:
parts.append(str(self.specifier))
if self.url:
parts.append("@ {0}".format(self.url))
if self.marker:
parts.append("; {0}".format(self.marker))
return "".join(parts)
def __repr__(self):
return "<Requirement({0!r})>".format(str(self))
# Copyright 2014 Donald Stufft # This file is dual licensed under the terms of the Apache License, Version
# # 2.0, and the BSD License. See the LICENSE file in the root of this repository
# Licensed under the Apache License, Version 2.0 (the "License"); # for complete details.
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import abc import abc
...@@ -223,10 +213,8 @@ class _IndividualSpecifier(BaseSpecifier): ...@@ -223,10 +213,8 @@ class _IndividualSpecifier(BaseSpecifier):
class LegacySpecifier(_IndividualSpecifier): class LegacySpecifier(_IndividualSpecifier):
_regex = re.compile( _regex_str = (
r""" r"""
^
\s*
(?P<operator>(==|!=|<=|>=|<|>)) (?P<operator>(==|!=|<=|>=|<|>))
\s* \s*
(?P<version> (?P<version>
...@@ -234,12 +222,11 @@ class LegacySpecifier(_IndividualSpecifier): ...@@ -234,12 +222,11 @@ class LegacySpecifier(_IndividualSpecifier):
# is a "legacy" specifier and the version string can be just # is a "legacy" specifier and the version string can be just
# about anything. # about anything.
) )
\s* """
$
""",
re.VERBOSE | re.IGNORECASE,
) )
_regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.X | re.I)
_operators = { _operators = {
"==": "equal", "==": "equal",
"!=": "not_equal", "!=": "not_equal",
...@@ -284,10 +271,8 @@ def _require_version_compare(fn): ...@@ -284,10 +271,8 @@ def _require_version_compare(fn):
class Specifier(_IndividualSpecifier): class Specifier(_IndividualSpecifier):
_regex = re.compile( _regex_str = (
r""" r"""
^
\s*
(?P<operator>(~=|==|!=|<=|>=|<|>|===)) (?P<operator>(~=|==|!=|<=|>=|<|>|===))
(?P<version> (?P<version>
(?: (?:
...@@ -378,12 +363,11 @@ class Specifier(_IndividualSpecifier): ...@@ -378,12 +363,11 @@ class Specifier(_IndividualSpecifier):
(?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
) )
) )
\s* """
$
""",
re.VERBOSE | re.IGNORECASE,
) )
_regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.X | re.I)
_operators = { _operators = {
"~=": "compatible", "~=": "compatible",
"==": "equal", "==": "equal",
......
# Copyright 2014 Donald Stufft # This file is dual licensed under the terms of the Apache License, Version
# # 2.0, and the BSD License. See the LICENSE file in the root of this repository
# Licensed under the Apache License, Version 2.0 (the "License"); # for complete details.
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import collections import collections
......
This source diff could not be displayed because it is too large. You can view the blob instead.
packaging==15.3 packaging==15.3
pyparsing==2.0.6
...@@ -338,88 +338,58 @@ Environment Markers ...@@ -338,88 +338,58 @@ Environment Markers
>>> import os >>> import os
>>> print(im("sys_platform")) >>> print(im("sys_platform"))
Comparison or logical expression expected Invalid marker: 'sys_platform'
>>> print(im("sys_platform==")) >>> print(im("sys_platform=="))
invalid syntax Invalid marker: 'sys_platform=='
>>> print(im("sys_platform=='win32'")) >>> print(im("sys_platform=='win32'"))
False False
>>> print(im("sys=='x'")) >>> print(im("sys=='x'"))
Unknown name 'sys' Invalid marker: "sys=='x'"
>>> print(im("(extra)")) >>> print(im("(extra)"))
Comparison or logical expression expected Invalid marker: '(extra)'
>>> print(im("(extra")) >>> print(im("(extra"))
invalid syntax Invalid marker: '(extra'
>>> print(im("os.open('foo')=='y'")) >>> print(im("os.open('foo')=='y'"))
Language feature not supported in environment markers Invalid marker: "os.open('foo')=='y'"
>>> print(im("'x'=='y' and os.open('foo')=='y'")) # no short-circuit! >>> print(im("'x'=='y' and os.open('foo')=='y'")) # no short-circuit!
Language feature not supported in environment markers Invalid marker: "'x'=='y' and os.open('foo')=='y'"
>>> print(im("'x'=='x' or os.open('foo')=='y'")) # no short-circuit! >>> print(im("'x'=='x' or os.open('foo')=='y'")) # no short-circuit!
Language feature not supported in environment markers Invalid marker: "'x'=='x' or os.open('foo')=='y'"
>>> print(im("'x' < 'y' < 'z'")) >>> print(im("'x' < 'y' < 'z'"))
Chained comparison not allowed in environment markers Invalid marker: "'x' < 'y' < 'z'"
>>> print(im("r'x'=='x'")) >>> print(im("r'x'=='x'"))
Only plain strings allowed in environment markers Invalid marker: "r'x'=='x'"
>>> print(im("'''x'''=='x'")) >>> print(im("'''x'''=='x'"))
Only plain strings allowed in environment markers Invalid marker: "'''x'''=='x'"
>>> print(im('"""x"""=="x"')) >>> print(im('"""x"""=="x"'))
Only plain strings allowed in environment markers Invalid marker: '"""x"""=="x"'
>>> print(im(r"'x\n'=='x'")) >>> print(im(r"x\n=='x'"))
Only plain strings allowed in environment markers Invalid marker: "x\\n=='x'"
>>> print(im("os.open=='y'")) >>> print(im("os.open=='y'"))
Language feature not supported in environment markers Invalid marker: "os.open=='y'"
>>> em('"x"=="x"') >>> em("'linux' in sys_platform")
True True
>>> em('"x"=="y"')
False
>>> em('"x"=="y" and "x"=="x"')
False
>>> em('"x"=="y" or "x"=="x"')
True
>>> em('"x"=="y" and "x"=="q" or "z"=="z"')
True
>>> em('"x"=="y" and ("x"=="q" or "z"=="z")')
False
>>> em('"x"=="y" and "z"=="z" or "x"=="q"')
False
>>> em('"x"=="x" and "z"=="z" or "x"=="q"')
True
>>> em("sys_platform=='win32'") == (sys.platform=='win32')
True
>>> em("'x' in 'yx'")
True
>>> em("'yx' in 'x'")
False
>>> em("python_version >= '2.6'") >>> em("python_version >= '2.6'")
True True
>>> em("python_version > '2.5'") >>> em("python_version > '2.5'")
True True
>>> im("platform_python_implementation=='CPython'") >>> im("implementation_name=='CPython'")
False False
...@@ -8,9 +8,5 @@ from pkg_resources import evaluate_marker ...@@ -8,9 +8,5 @@ from pkg_resources import evaluate_marker
@mock.patch.dict('pkg_resources.MarkerEvaluation.values', @mock.patch.dict('pkg_resources.MarkerEvaluation.values',
python_full_version=mock.Mock(return_value='2.7.10')) python_full_version=mock.Mock(return_value='2.7.10'))
def test_lexicographic_ordering(): def test_ordering():
""" assert evaluate_marker("python_full_version > '2.7.3'") is True
Although one might like 2.7.10 to be greater than 2.7.3,
the marker spec only supports lexicographic ordering.
"""
assert evaluate_marker("python_full_version > '2.7.3'") is False
...@@ -26,7 +26,6 @@ class TestDistInfo: ...@@ -26,7 +26,6 @@ class TestDistInfo:
assert versioned.version == '2.718' # from filename assert versioned.version == '2.718' # from filename
assert unversioned.version == '0.3' # from METADATA assert unversioned.version == '0.3' # from METADATA
@pytest.mark.importorskip('ast')
def test_conditional_dependencies(self): def test_conditional_dependencies(self):
specs = 'splort==4', 'quux>=1.1' specs = 'splort==4', 'quux>=1.1'
requires = list(map(pkg_resources.Requirement.parse, specs)) requires = list(map(pkg_resources.Requirement.parse, specs))
......
import os
import pytest
class TestMarkerlib:
@pytest.mark.importorskip('ast')
def test_markers(self):
from _markerlib import interpret, default_environment, compile
os_name = os.name
assert interpret("")
assert interpret("os.name != 'buuuu'")
assert interpret("os_name != 'buuuu'")
assert interpret("python_version > '1.0'")
assert interpret("python_version < '5.0'")
assert interpret("python_version <= '5.0'")
assert interpret("python_version >= '1.0'")
assert interpret("'%s' in os.name" % os_name)
assert interpret("'%s' in os_name" % os_name)
assert interpret("'buuuu' not in os.name")
assert not interpret("os.name == 'buuuu'")
assert not interpret("os_name == 'buuuu'")
assert not interpret("python_version < '1.0'")
assert not interpret("python_version > '5.0'")
assert not interpret("python_version >= '5.0'")
assert not interpret("python_version <= '1.0'")
assert not interpret("'%s' not in os.name" % os_name)
assert not interpret("'buuuu' in os.name and python_version >= '5.0'")
assert not interpret("'buuuu' in os_name and python_version >= '5.0'")
environment = default_environment()
environment['extra'] = 'test'
assert interpret("extra == 'test'", environment)
assert not interpret("extra == 'doc'", environment)
def raises_nameError():
try:
interpret("python.version == '42'")
except NameError:
pass
else:
raise Exception("Expected NameError")
raises_nameError()
def raises_syntaxError():
try:
interpret("(x for x in (4,))")
except SyntaxError:
pass
else:
raise Exception("Expected SyntaxError")
raises_syntaxError()
statement = "python_version == '5'"
assert compile(statement).__doc__ == statement
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