Development of an internal social media platform with personalised dashboards for students
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 12KB

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