123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- """
- Requirements file parsing
- """
- from __future__ import absolute_import
- import optparse
- import os
- import re
- import shlex
- import sys
- from pip._vendor.six.moves import filterfalse
- from pip._vendor.six.moves.urllib import parse as urllib_parse
- from pip._internal import cmdoptions
- from pip._internal.download import get_file_content
- from pip._internal.exceptions import RequirementsFileParseError
- from pip._internal.req.req_install import InstallRequirement
- __all__ = ['parse_requirements']
- SCHEME_RE = re.compile(r'^(http|https|file):', re.I)
- COMMENT_RE = re.compile(r'(^|\s)+#.*$')
- # Matches environment variable-style values in '${MY_VARIABLE_1}' with the
- # variable name consisting of only uppercase letters, digits or the '_'
- # (underscore). This follows the POSIX standard defined in IEEE Std 1003.1,
- # 2013 Edition.
- ENV_VAR_RE = re.compile(r'(?P<var>\$\{(?P<name>[A-Z0-9_]+)\})')
- SUPPORTED_OPTIONS = [
- cmdoptions.constraints,
- cmdoptions.editable,
- cmdoptions.requirements,
- cmdoptions.no_index,
- cmdoptions.index_url,
- cmdoptions.find_links,
- cmdoptions.extra_index_url,
- cmdoptions.always_unzip,
- cmdoptions.no_binary,
- cmdoptions.only_binary,
- cmdoptions.pre,
- cmdoptions.process_dependency_links,
- cmdoptions.trusted_host,
- cmdoptions.require_hashes,
- ]
- # options to be passed to requirements
- SUPPORTED_OPTIONS_REQ = [
- cmdoptions.install_options,
- cmdoptions.global_options,
- cmdoptions.hash,
- ]
- # the 'dest' string values
- SUPPORTED_OPTIONS_REQ_DEST = [o().dest for o in SUPPORTED_OPTIONS_REQ]
- def parse_requirements(filename, finder=None, comes_from=None, options=None,
- session=None, constraint=False, wheel_cache=None):
- """Parse a requirements file and yield InstallRequirement instances.
- :param filename: Path or url of requirements file.
- :param finder: Instance of pip.index.PackageFinder.
- :param comes_from: Origin description of requirements.
- :param options: cli options.
- :param session: Instance of pip.download.PipSession.
- :param constraint: If true, parsing a constraint file rather than
- requirements file.
- :param wheel_cache: Instance of pip.wheel.WheelCache
- """
- if session is None:
- raise TypeError(
- "parse_requirements() missing 1 required keyword argument: "
- "'session'"
- )
- _, content = get_file_content(
- filename, comes_from=comes_from, session=session
- )
- lines_enum = preprocess(content, options)
- for line_number, line in lines_enum:
- req_iter = process_line(line, filename, line_number, finder,
- comes_from, options, session, wheel_cache,
- constraint=constraint)
- for req in req_iter:
- yield req
- def preprocess(content, options):
- """Split, filter, and join lines, and return a line iterator
- :param content: the content of the requirements file
- :param options: cli options
- """
- lines_enum = enumerate(content.splitlines(), start=1)
- lines_enum = join_lines(lines_enum)
- lines_enum = ignore_comments(lines_enum)
- lines_enum = skip_regex(lines_enum, options)
- lines_enum = expand_env_variables(lines_enum)
- return lines_enum
- def process_line(line, filename, line_number, finder=None, comes_from=None,
- options=None, session=None, wheel_cache=None,
- constraint=False):
- """Process a single requirements line; This can result in creating/yielding
- requirements, or updating the finder.
- For lines that contain requirements, the only options that have an effect
- are from SUPPORTED_OPTIONS_REQ, and they are scoped to the
- requirement. Other options from SUPPORTED_OPTIONS may be present, but are
- ignored.
- For lines that do not contain requirements, the only options that have an
- effect are from SUPPORTED_OPTIONS. Options from SUPPORTED_OPTIONS_REQ may
- be present, but are ignored. These lines may contain multiple options
- (although our docs imply only one is supported), and all our parsed and
- affect the finder.
- :param constraint: If True, parsing a constraints file.
- :param options: OptionParser options that we may update
- """
- parser = build_parser(line)
- defaults = parser.get_default_values()
- defaults.index_url = None
- if finder:
- # `finder.format_control` will be updated during parsing
- defaults.format_control = finder.format_control
- args_str, options_str = break_args_options(line)
- if sys.version_info < (2, 7, 3):
- # Prior to 2.7.3, shlex cannot deal with unicode entries
- options_str = options_str.encode('utf8')
- opts, _ = parser.parse_args(shlex.split(options_str), defaults)
- # preserve for the nested code path
- line_comes_from = '%s %s (line %s)' % (
- '-c' if constraint else '-r', filename, line_number,
- )
- # yield a line requirement
- if args_str:
- isolated = options.isolated_mode if options else False
- if options:
- cmdoptions.check_install_build_global(options, opts)
- # get the options that apply to requirements
- req_options = {}
- for dest in SUPPORTED_OPTIONS_REQ_DEST:
- if dest in opts.__dict__ and opts.__dict__[dest]:
- req_options[dest] = opts.__dict__[dest]
- yield InstallRequirement.from_line(
- args_str, line_comes_from, constraint=constraint,
- isolated=isolated, options=req_options, wheel_cache=wheel_cache
- )
- # yield an editable requirement
- elif opts.editables:
- isolated = options.isolated_mode if options else False
- yield InstallRequirement.from_editable(
- opts.editables[0], comes_from=line_comes_from,
- constraint=constraint, isolated=isolated, wheel_cache=wheel_cache
- )
- # parse a nested requirements file
- elif opts.requirements or opts.constraints:
- if opts.requirements:
- req_path = opts.requirements[0]
- nested_constraint = False
- else:
- req_path = opts.constraints[0]
- nested_constraint = True
- # original file is over http
- if SCHEME_RE.search(filename):
- # do a url join so relative paths work
- req_path = urllib_parse.urljoin(filename, req_path)
- # original file and nested file are paths
- elif not SCHEME_RE.search(req_path):
- # do a join so relative paths work
- req_path = os.path.join(os.path.dirname(filename), req_path)
- # TODO: Why not use `comes_from='-r {} (line {})'` here as well?
- parser = parse_requirements(
- req_path, finder, comes_from, options, session,
- constraint=nested_constraint, wheel_cache=wheel_cache
- )
- for req in parser:
- yield req
- # percolate hash-checking option upward
- elif opts.require_hashes:
- options.require_hashes = opts.require_hashes
- # set finder options
- elif finder:
- if opts.index_url:
- finder.index_urls = [opts.index_url]
- if opts.no_index is True:
- finder.index_urls = []
- if opts.extra_index_urls:
- finder.index_urls.extend(opts.extra_index_urls)
- if opts.find_links:
- # FIXME: it would be nice to keep track of the source
- # of the find_links: support a find-links local path
- # relative to a requirements file.
- value = opts.find_links[0]
- req_dir = os.path.dirname(os.path.abspath(filename))
- relative_to_reqs_file = os.path.join(req_dir, value)
- if os.path.exists(relative_to_reqs_file):
- value = relative_to_reqs_file
- finder.find_links.append(value)
- if opts.pre:
- finder.allow_all_prereleases = True
- if opts.process_dependency_links:
- finder.process_dependency_links = True
- if opts.trusted_hosts:
- finder.secure_origins.extend(
- ("*", host, "*") for host in opts.trusted_hosts)
- def break_args_options(line):
- """Break up the line into an args and options string. We only want to shlex
- (and then optparse) the options, not the args. args can contain markers
- which are corrupted by shlex.
- """
- tokens = line.split(' ')
- args = []
- options = tokens[:]
- for token in tokens:
- if token.startswith('-') or token.startswith('--'):
- break
- else:
- args.append(token)
- options.pop(0)
- return ' '.join(args), ' '.join(options)
- def build_parser(line):
- """
- Return a parser for parsing requirement lines
- """
- parser = optparse.OptionParser(add_help_option=False)
- option_factories = SUPPORTED_OPTIONS + SUPPORTED_OPTIONS_REQ
- for option_factory in option_factories:
- option = option_factory()
- parser.add_option(option)
- # By default optparse sys.exits on parsing errors. We want to wrap
- # that in our own exception.
- def parser_exit(self, msg):
- # add offending line
- msg = 'Invalid requirement: %s\n%s' % (line, msg)
- raise RequirementsFileParseError(msg)
- parser.exit = parser_exit
- return parser
- def join_lines(lines_enum):
- """Joins a line ending in '\' with the previous line (except when following
- comments). The joined line takes on the index of the first line.
- """
- primary_line_number = None
- new_line = []
- for line_number, line in lines_enum:
- if not line.endswith('\\') or COMMENT_RE.match(line):
- if COMMENT_RE.match(line):
- # this ensures comments are always matched later
- line = ' ' + line
- if new_line:
- new_line.append(line)
- yield primary_line_number, ''.join(new_line)
- new_line = []
- else:
- yield line_number, line
- else:
- if not new_line:
- primary_line_number = line_number
- new_line.append(line.strip('\\'))
- # last line contains \
- if new_line:
- yield primary_line_number, ''.join(new_line)
- # TODO: handle space after '\'.
- def ignore_comments(lines_enum):
- """
- Strips comments and filter empty lines.
- """
- for line_number, line in lines_enum:
- line = COMMENT_RE.sub('', line)
- line = line.strip()
- if line:
- yield line_number, line
- def skip_regex(lines_enum, options):
- """
- Skip lines that match '--skip-requirements-regex' pattern
- Note: the regex pattern is only built once
- """
- skip_regex = options.skip_requirements_regex if options else None
- if skip_regex:
- pattern = re.compile(skip_regex)
- lines_enum = filterfalse(lambda e: pattern.search(e[1]), lines_enum)
- return lines_enum
- def expand_env_variables(lines_enum):
- """Replace all environment variables that can be retrieved via `os.getenv`.
- The only allowed format for environment variables defined in the
- requirement file is `${MY_VARIABLE_1}` to ensure two things:
- 1. Strings that contain a `$` aren't accidentally (partially) expanded.
- 2. Ensure consistency across platforms for requirement files.
- These points are the result of a discusssion on the `github pull
- request #3514 <https://github.com/pypa/pip/pull/3514>`_.
- Valid characters in variable names follow the `POSIX standard
- <http://pubs.opengroup.org/onlinepubs/9699919799/>`_ and are limited
- to uppercase letter, digits and the `_` (underscore).
- """
- for line_number, line in lines_enum:
- for env_var, var_name in ENV_VAR_RE.findall(line):
- value = os.getenv(var_name)
- if not value:
- continue
- line = line.replace(env_var, value)
- yield line_number, line
|