markers.py 5.1 KB

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