You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

req_file.py 13KB

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