req_file.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. """
  2. Requirements file parsing
  3. """
  4. from __future__ import absolute_import
  5. import optparse
  6. import os
  7. import re
  8. import shlex
  9. import sys
  10. from pip._vendor.six.moves import filterfalse
  11. from pip._vendor.six.moves.urllib import parse as urllib_parse
  12. from pip._internal import cmdoptions
  13. from pip._internal.download import get_file_content
  14. from pip._internal.exceptions import RequirementsFileParseError
  15. from pip._internal.req.req_install import InstallRequirement
  16. __all__ = ['parse_requirements']
  17. SCHEME_RE = re.compile(r'^(http|https|file):', re.I)
  18. COMMENT_RE = re.compile(r'(^|\s)+#.*$')
  19. # Matches environment variable-style values in '${MY_VARIABLE_1}' with the
  20. # variable name consisting of only uppercase letters, digits or the '_'
  21. # (underscore). This follows the POSIX standard defined in IEEE Std 1003.1,
  22. # 2013 Edition.
  23. ENV_VAR_RE = re.compile(r'(?P<var>\$\{(?P<name>[A-Z0-9_]+)\})')
  24. SUPPORTED_OPTIONS = [
  25. cmdoptions.constraints,
  26. cmdoptions.editable,
  27. cmdoptions.requirements,
  28. cmdoptions.no_index,
  29. cmdoptions.index_url,
  30. cmdoptions.find_links,
  31. cmdoptions.extra_index_url,
  32. cmdoptions.always_unzip,
  33. cmdoptions.no_binary,
  34. cmdoptions.only_binary,
  35. cmdoptions.pre,
  36. cmdoptions.process_dependency_links,
  37. cmdoptions.trusted_host,
  38. cmdoptions.require_hashes,
  39. ]
  40. # options to be passed to requirements
  41. SUPPORTED_OPTIONS_REQ = [
  42. cmdoptions.install_options,
  43. cmdoptions.global_options,
  44. cmdoptions.hash,
  45. ]
  46. # the 'dest' string values
  47. SUPPORTED_OPTIONS_REQ_DEST = [o().dest for o in SUPPORTED_OPTIONS_REQ]
  48. def parse_requirements(filename, finder=None, comes_from=None, options=None,
  49. session=None, constraint=False, wheel_cache=None):
  50. """Parse a requirements file and yield InstallRequirement instances.
  51. :param filename: Path or url of requirements file.
  52. :param finder: Instance of pip.index.PackageFinder.
  53. :param comes_from: Origin description of requirements.
  54. :param options: cli options.
  55. :param session: Instance of pip.download.PipSession.
  56. :param constraint: If true, parsing a constraint file rather than
  57. requirements file.
  58. :param wheel_cache: Instance of pip.wheel.WheelCache
  59. """
  60. if session is None:
  61. raise TypeError(
  62. "parse_requirements() missing 1 required keyword argument: "
  63. "'session'"
  64. )
  65. _, content = get_file_content(
  66. filename, comes_from=comes_from, session=session
  67. )
  68. lines_enum = preprocess(content, options)
  69. for line_number, line in lines_enum:
  70. req_iter = process_line(line, filename, line_number, finder,
  71. comes_from, options, session, wheel_cache,
  72. constraint=constraint)
  73. for req in req_iter:
  74. yield req
  75. def preprocess(content, options):
  76. """Split, filter, and join lines, and return a line iterator
  77. :param content: the content of the requirements file
  78. :param options: cli options
  79. """
  80. lines_enum = enumerate(content.splitlines(), start=1)
  81. lines_enum = join_lines(lines_enum)
  82. lines_enum = ignore_comments(lines_enum)
  83. lines_enum = skip_regex(lines_enum, options)
  84. lines_enum = expand_env_variables(lines_enum)
  85. return lines_enum
  86. def process_line(line, filename, line_number, finder=None, comes_from=None,
  87. options=None, session=None, wheel_cache=None,
  88. constraint=False):
  89. """Process a single requirements line; This can result in creating/yielding
  90. requirements, or updating the finder.
  91. For lines that contain requirements, the only options that have an effect
  92. are from SUPPORTED_OPTIONS_REQ, and they are scoped to the
  93. requirement. Other options from SUPPORTED_OPTIONS may be present, but are
  94. ignored.
  95. For lines that do not contain requirements, the only options that have an
  96. effect are from SUPPORTED_OPTIONS. Options from SUPPORTED_OPTIONS_REQ may
  97. be present, but are ignored. These lines may contain multiple options
  98. (although our docs imply only one is supported), and all our parsed and
  99. affect the finder.
  100. :param constraint: If True, parsing a constraints file.
  101. :param options: OptionParser options that we may update
  102. """
  103. parser = build_parser(line)
  104. defaults = parser.get_default_values()
  105. defaults.index_url = None
  106. if finder:
  107. # `finder.format_control` will be updated during parsing
  108. defaults.format_control = finder.format_control
  109. args_str, options_str = break_args_options(line)
  110. if sys.version_info < (2, 7, 3):
  111. # Prior to 2.7.3, shlex cannot deal with unicode entries
  112. options_str = options_str.encode('utf8')
  113. opts, _ = parser.parse_args(shlex.split(options_str), defaults)
  114. # preserve for the nested code path
  115. line_comes_from = '%s %s (line %s)' % (
  116. '-c' if constraint else '-r', filename, line_number,
  117. )
  118. # yield a line requirement
  119. if args_str:
  120. isolated = options.isolated_mode if options else False
  121. if options:
  122. cmdoptions.check_install_build_global(options, opts)
  123. # get the options that apply to requirements
  124. req_options = {}
  125. for dest in SUPPORTED_OPTIONS_REQ_DEST:
  126. if dest in opts.__dict__ and opts.__dict__[dest]:
  127. req_options[dest] = opts.__dict__[dest]
  128. yield InstallRequirement.from_line(
  129. args_str, line_comes_from, constraint=constraint,
  130. isolated=isolated, options=req_options, wheel_cache=wheel_cache
  131. )
  132. # yield an editable requirement
  133. elif opts.editables:
  134. isolated = options.isolated_mode if options else False
  135. yield InstallRequirement.from_editable(
  136. opts.editables[0], comes_from=line_comes_from,
  137. constraint=constraint, isolated=isolated, wheel_cache=wheel_cache
  138. )
  139. # parse a nested requirements file
  140. elif opts.requirements or opts.constraints:
  141. if opts.requirements:
  142. req_path = opts.requirements[0]
  143. nested_constraint = False
  144. else:
  145. req_path = opts.constraints[0]
  146. nested_constraint = True
  147. # original file is over http
  148. if SCHEME_RE.search(filename):
  149. # do a url join so relative paths work
  150. req_path = urllib_parse.urljoin(filename, req_path)
  151. # original file and nested file are paths
  152. elif not SCHEME_RE.search(req_path):
  153. # do a join so relative paths work
  154. req_path = os.path.join(os.path.dirname(filename), req_path)
  155. # TODO: Why not use `comes_from='-r {} (line {})'` here as well?
  156. parser = parse_requirements(
  157. req_path, finder, comes_from, options, session,
  158. constraint=nested_constraint, wheel_cache=wheel_cache
  159. )
  160. for req in parser:
  161. yield req
  162. # percolate hash-checking option upward
  163. elif opts.require_hashes:
  164. options.require_hashes = opts.require_hashes
  165. # set finder options
  166. elif finder:
  167. if opts.index_url:
  168. finder.index_urls = [opts.index_url]
  169. if opts.no_index is True:
  170. finder.index_urls = []
  171. if opts.extra_index_urls:
  172. finder.index_urls.extend(opts.extra_index_urls)
  173. if opts.find_links:
  174. # FIXME: it would be nice to keep track of the source
  175. # of the find_links: support a find-links local path
  176. # relative to a requirements file.
  177. value = opts.find_links[0]
  178. req_dir = os.path.dirname(os.path.abspath(filename))
  179. relative_to_reqs_file = os.path.join(req_dir, value)
  180. if os.path.exists(relative_to_reqs_file):
  181. value = relative_to_reqs_file
  182. finder.find_links.append(value)
  183. if opts.pre:
  184. finder.allow_all_prereleases = True
  185. if opts.process_dependency_links:
  186. finder.process_dependency_links = True
  187. if opts.trusted_hosts:
  188. finder.secure_origins.extend(
  189. ("*", host, "*") for host in opts.trusted_hosts)
  190. def break_args_options(line):
  191. """Break up the line into an args and options string. We only want to shlex
  192. (and then optparse) the options, not the args. args can contain markers
  193. which are corrupted by shlex.
  194. """
  195. tokens = line.split(' ')
  196. args = []
  197. options = tokens[:]
  198. for token in tokens:
  199. if token.startswith('-') or token.startswith('--'):
  200. break
  201. else:
  202. args.append(token)
  203. options.pop(0)
  204. return ' '.join(args), ' '.join(options)
  205. def build_parser(line):
  206. """
  207. Return a parser for parsing requirement lines
  208. """
  209. parser = optparse.OptionParser(add_help_option=False)
  210. option_factories = SUPPORTED_OPTIONS + SUPPORTED_OPTIONS_REQ
  211. for option_factory in option_factories:
  212. option = option_factory()
  213. parser.add_option(option)
  214. # By default optparse sys.exits on parsing errors. We want to wrap
  215. # that in our own exception.
  216. def parser_exit(self, msg):
  217. # add offending line
  218. msg = 'Invalid requirement: %s\n%s' % (line, msg)
  219. raise RequirementsFileParseError(msg)
  220. parser.exit = parser_exit
  221. return parser
  222. def join_lines(lines_enum):
  223. """Joins a line ending in '\' with the previous line (except when following
  224. comments). The joined line takes on the index of the first line.
  225. """
  226. primary_line_number = None
  227. new_line = []
  228. for line_number, line in lines_enum:
  229. if not line.endswith('\\') or COMMENT_RE.match(line):
  230. if COMMENT_RE.match(line):
  231. # this ensures comments are always matched later
  232. line = ' ' + line
  233. if new_line:
  234. new_line.append(line)
  235. yield primary_line_number, ''.join(new_line)
  236. new_line = []
  237. else:
  238. yield line_number, line
  239. else:
  240. if not new_line:
  241. primary_line_number = line_number
  242. new_line.append(line.strip('\\'))
  243. # last line contains \
  244. if new_line:
  245. yield primary_line_number, ''.join(new_line)
  246. # TODO: handle space after '\'.
  247. def ignore_comments(lines_enum):
  248. """
  249. Strips comments and filter empty lines.
  250. """
  251. for line_number, line in lines_enum:
  252. line = COMMENT_RE.sub('', line)
  253. line = line.strip()
  254. if line:
  255. yield line_number, line
  256. def skip_regex(lines_enum, options):
  257. """
  258. Skip lines that match '--skip-requirements-regex' pattern
  259. Note: the regex pattern is only built once
  260. """
  261. skip_regex = options.skip_requirements_regex if options else None
  262. if skip_regex:
  263. pattern = re.compile(skip_regex)
  264. lines_enum = filterfalse(lambda e: pattern.search(e[1]), lines_enum)
  265. return lines_enum
  266. def expand_env_variables(lines_enum):
  267. """Replace all environment variables that can be retrieved via `os.getenv`.
  268. The only allowed format for environment variables defined in the
  269. requirement file is `${MY_VARIABLE_1}` to ensure two things:
  270. 1. Strings that contain a `$` aren't accidentally (partially) expanded.
  271. 2. Ensure consistency across platforms for requirement files.
  272. These points are the result of a discusssion on the `github pull
  273. request #3514 <https://github.com/pypa/pip/pull/3514>`_.
  274. Valid characters in variable names follow the `POSIX standard
  275. <http://pubs.opengroup.org/onlinepubs/9699919799/>`_ and are limited
  276. to uppercase letter, digits and the `_` (underscore).
  277. """
  278. for line_number, line in lines_enum:
  279. for env_var, var_name in ENV_VAR_RE.findall(line):
  280. value = os.getenv(var_name)
  281. if not value:
  282. continue
  283. line = line.replace(env_var, value)
  284. yield line_number, line