req_file.py 12 KB

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