1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2012-2017 Vinay Sajip.
4# Licensed to the Python Software Foundation under a contributor agreement.
5# See LICENSE.txt and CONTRIBUTORS.txt.
6#
7"""
8Parser for the environment markers micro-language defined in PEP 508.
9"""
10
11# Note: In PEP 345, the micro-language was Python compatible, so the ast
12# module could be used to parse it. However, PEP 508 introduced operators such
13# as ~= and === which aren't in Python, necessitating a different approach.
14
15import os
16import sys
17import platform
18import re
19
20from .compat import python_implementation, urlparse, string_types
21from .util import in_venv, parse_marker
22
23__all__ = ['interpret']
24
25def _is_literal(o):
26    if not isinstance(o, string_types) or not o:
27        return False
28    return o[0] in '\'"'
29
30class Evaluator(object):
31    """
32    This class is used to evaluate marker expessions.
33    """
34
35    operations = {
36        '==': lambda x, y: x == y,
37        '===': lambda x, y: x == y,
38        '~=': lambda x, y: x == y or x > y,
39        '!=': lambda x, y: x != y,
40        '<':  lambda x, y: x < y,
41        '<=':  lambda x, y: x == y or x < y,
42        '>':  lambda x, y: x > y,
43        '>=':  lambda x, y: x == y or x > y,
44        'and': lambda x, y: x and y,
45        'or': lambda x, y: x or y,
46        'in': lambda x, y: x in y,
47        'not in': lambda x, y: x not in y,
48    }
49
50    def evaluate(self, expr, context):
51        """
52        Evaluate a marker expression returned by the :func:`parse_requirement`
53        function in the specified context.
54        """
55        if isinstance(expr, string_types):
56            if expr[0] in '\'"':
57                result = expr[1:-1]
58            else:
59                if expr not in context:
60                    raise SyntaxError('unknown variable: %s' % expr)
61                result = context[expr]
62        else:
63            assert isinstance(expr, dict)
64            op = expr['op']
65            if op not in self.operations:
66                raise NotImplementedError('op not implemented: %s' % op)
67            elhs = expr['lhs']
68            erhs = expr['rhs']
69            if _is_literal(expr['lhs']) and _is_literal(expr['rhs']):
70                raise SyntaxError('invalid comparison: %s %s %s' % (elhs, op, erhs))
71
72            lhs = self.evaluate(elhs, context)
73            rhs = self.evaluate(erhs, context)
74            result = self.operations[op](lhs, rhs)
75        return result
76
77def default_context():
78    def format_full_version(info):
79        version = '%s.%s.%s' % (info.major, info.minor, info.micro)
80        kind = info.releaselevel
81        if kind != 'final':
82            version += kind[0] + str(info.serial)
83        return version
84
85    if hasattr(sys, 'implementation'):
86        implementation_version = format_full_version(sys.implementation.version)
87        implementation_name = sys.implementation.name
88    else:
89        implementation_version = '0'
90        implementation_name = ''
91
92    result = {
93        'implementation_name': implementation_name,
94        'implementation_version': implementation_version,
95        'os_name': os.name,
96        'platform_machine': platform.machine(),
97        'platform_python_implementation': platform.python_implementation(),
98        'platform_release': platform.release(),
99        'platform_system': platform.system(),
100        'platform_version': platform.version(),
101        'platform_in_venv': str(in_venv()),
102        'python_full_version': platform.python_version(),
103        'python_version': platform.python_version()[:3],
104        'sys_platform': sys.platform,
105    }
106    return result
107
108DEFAULT_CONTEXT = default_context()
109del default_context
110
111evaluator = Evaluator()
112
113def interpret(marker, execution_context=None):
114    """
115    Interpret a marker and return a result depending on environment.
116
117    :param marker: The marker to interpret.
118    :type marker: str
119    :param execution_context: The context used for name lookup.
120    :type execution_context: mapping
121    """
122    try:
123        expr, rest = parse_marker(marker)
124    except Exception as e:
125        raise SyntaxError('Unable to interpret marker syntax: %s: %s' % (marker, e))
126    if rest and rest[0] != '#':
127        raise SyntaxError('unexpected trailing data in marker: %s: %s' % (marker, rest))
128    context = dict(DEFAULT_CONTEXT)
129    if execution_context:
130        context.update(execution_context)
131    return evaluator.evaluate(expr, context)
132