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.
 
 
 
 

283 lines
9.4 KiB

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