summaryrefslogtreecommitdiffstats
path: root/Lib/packaging/markers.py
blob: 63fdc1900d7e72fadfd34fcf679ca8f1179bf070 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
"""Parser for the environment markers micro-language defined in PEP 345."""

import os
import sys
import platform
from io import BytesIO
from tokenize import tokenize, NAME, OP, STRING, ENDMARKER, ENCODING

__all__ = ['interpret']


# allowed operators
_OPERATORS = {'==': lambda x, y: x == y,
              '!=': lambda x, y: x != y,
              '>': lambda x, y: x > y,
              '>=': lambda x, y: x >= y,
              '<': lambda x, y: x < y,
              '<=': lambda x, y: x <= y,
              'in': lambda x, y: x in y,
              'not in': lambda x, y: x not in y}


def _operate(operation, x, y):
    return _OPERATORS[operation](x, y)


# 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': platform.python_implementation(),
        }


class _Operation:

    def __init__(self, execution_context=None):
        self.left = None
        self.op = None
        self.right = None
        if execution_context is None:
            execution_context = {}
        self.execution_context = execution_context

    def _get_var(self, name):
        if name in self.execution_context:
            return self.execution_context[name]
        return _VARS[name]

    def __repr__(self):
        return '%s %s %s' % (self.left, self.op, self.right)

    def _is_string(self, value):
        if value is None or len(value) < 2:
            return False
        for delimiter in '"\'':
            if value[0] == value[-1] == delimiter:
                return True
        return False

    def _is_name(self, value):
        return value in _VARS

    def _convert(self, value):
        if value in _VARS:
            return self._get_var(value)
        return value.strip('"\'')

    def _check_name(self, value):
        if value not in _VARS:
            raise NameError(value)

    def _nonsense_op(self):
        msg = 'This operation is not supported : "%s"' % self
        raise SyntaxError(msg)

    def __call__(self):
        # make sure we do something useful
        if self._is_string(self.left):
            if self._is_string(self.right):
                self._nonsense_op()
            self._check_name(self.right)
        else:
            if not self._is_string(self.right):
                self._nonsense_op()
            self._check_name(self.left)

        if self.op not in _OPERATORS:
            raise TypeError('Operator not supported "%s"' % self.op)

        left = self._convert(self.left)
        right = self._convert(self.right)
        return _operate(self.op, left, right)


class _OR:
    def __init__(self, left, right=None):
        self.left = left
        self.right = right

    def filled(self):
        return self.right is not None

    def __repr__(self):
        return 'OR(%r, %r)' % (self.left, self.right)

    def __call__(self):
        return self.left() or self.right()


class _AND:
    def __init__(self, left, right=None):
        self.left = left
        self.right = right

    def filled(self):
        return self.right is not None

    def __repr__(self):
        return 'AND(%r, %r)' % (self.left, self.right)

    def __call__(self):
        return self.left() and self.right()


def interpret(marker, execution_context=None):
    """Interpret a marker and return a result depending on environment."""
    marker = marker.strip().encode()
    ops = []
    op_starting = True
    for token in tokenize(BytesIO(marker).readline):
        # Unpack token
        toktype, tokval, rowcol, line, logical_line = token
        if toktype not in (NAME, OP, STRING, ENDMARKER, ENCODING):
            raise SyntaxError('Type not supported "%s"' % tokval)

        if op_starting:
            op = _Operation(execution_context)
            if len(ops) > 0:
                last = ops[-1]
                if isinstance(last, (_OR, _AND)) and not last.filled():
                    last.right = op
                else:
                    ops.append(op)
            else:
                ops.append(op)
            op_starting = False
        else:
            op = ops[-1]

        if (toktype == ENDMARKER or
            (toktype == NAME and tokval in ('and', 'or'))):
            if toktype == NAME and tokval == 'and':
                ops.append(_AND(ops.pop()))
            elif toktype == NAME and tokval == 'or':
                ops.append(_OR(ops.pop()))
            op_starting = True
            continue

        if isinstance(op, (_OR, _AND)) and op.right is not None:
            op = op.right

        if ((toktype in (NAME, STRING) and tokval not in ('in', 'not'))
            or (toktype == OP and tokval == '.')):
            if op.op is None:
                if op.left is None:
                    op.left = tokval
                else:
                    op.left += tokval
            else:
                if op.right is None:
                    op.right = tokval
                else:
                    op.right += tokval
        elif toktype == OP or tokval in ('in', 'not'):
            if tokval == 'in' and op.op == 'not':
                op.op = 'not in'
            else:
                op.op = tokval

    for op in ops:
        if not op():
            return False
    return True