123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240 |
- from __future__ import unicode_literals
- import re
- from pkg_resources import Requirement as Req
- from .fragment import get_hash_info, parse_fragment, parse_extras_require
- from .vcs import VCS, VCS_SCHEMES
- URI_REGEX = re.compile(
- r'^(?P<scheme>https?|file|ftps?)://(?P<path>[^#]+)'
- r'(#(?P<fragment>\S+))?'
- )
- VCS_REGEX = re.compile(
- r'^(?P<scheme>{0})://'.format(r'|'.join(
- [scheme.replace('+', r'\+') for scheme in VCS_SCHEMES])) +
- r'((?P<login>[^/@]+)@)?'
- r'(?P<path>[^#@]+)'
- r'(@(?P<revision>[^#]+))?'
- r'(#(?P<fragment>\S+))?'
- )
- # This matches just about everyting
- LOCAL_REGEX = re.compile(
- r'^((?P<scheme>file)://)?'
- r'(?P<path>[^#]+)' +
- r'(#(?P<fragment>\S+))?'
- )
- NAME_EQ_REGEX = re.compile(r'name="(\S+)"')
- PER_REQ_PARAM_REGEX = re.compile(r'\s*--[a-z-]+=\S+')
- EDITABLE_REGEX = re.compile(r'^(-e|--editable=?)\s*')
- class Requirement(object):
- """
- Represents a single requirement
- Typically instances of this class are created with ``Requirement.parse``.
- For local file requirements, there's no verification that the file
- exists. This class attempts to be *dict-like*.
- See: http://www.pip-installer.org/en/latest/logic.html
- **Members**:
- * ``line`` - the actual requirement line being parsed
- * ``editable`` - a boolean whether this requirement is "editable"
- * ``local_file`` - a boolean whether this requirement is a local file/path
- * ``specifier`` - a boolean whether this requirement used a requirement
- specifier (eg. "django>=1.5" or "requirements")
- * ``vcs`` - a string specifying the version control system
- * ``revision`` - a version control system specifier
- * ``name`` - the name of the requirement
- * ``uri`` - the URI if this requirement was specified by URI
- * ``subdirectory`` - the subdirectory fragment of the URI
- * ``path`` - the local path to the requirement
- * ``hash_name`` - the type of hashing algorithm indicated in the line
- * ``hash`` - the hash value indicated by the requirement line
- * ``extras`` - a list of extras for this requirement
- (eg. "mymodule[extra1, extra2]")
- * ``specs`` - a list of specs for this requirement
- (eg. "mymodule>1.5,<1.6" => [('>', '1.5'), ('<', '1.6')])
- * ``provenance`` - a tuple of (file_name, start_line, end_line), line numbers are 1-based
- """
- def __init__(self, line):
- # Do not call this private method
- self.line = line
- self.editable = False
- self.local_file = False
- self.specifier = False
- self.vcs = None
- self.name = None
- self.subdirectory = None
- self.uri = None
- self.path = None
- self.revision = None
- self.hash_name = None
- self.hash = None
- self.extras = []
- self.specs = []
- self.provenance = None
- def __repr__(self):
- return '<Requirement: "{0}">'.format(self.line)
- def __getitem__(self, key):
- return getattr(self, key)
- def keys(self):
- return self.__dict__.keys()
- @classmethod
- def parse_editable(cls, line):
- """
- Parses a Requirement from an "editable" requirement which is either
- a local project path or a VCS project URI.
- See: pip/req.py:from_editable()
- :param line: an "editable" requirement
- :returns: a Requirement instance for the given line
- :raises: ValueError on an invalid requirement
- """
- req = cls('-e {0}'.format(line))
- req.editable = True
- vcs_match = VCS_REGEX.match(line)
- local_match = LOCAL_REGEX.match(line)
- if vcs_match is not None:
- groups = vcs_match.groupdict()
- if groups.get('login'):
- req.uri = '{scheme}://{login}@{path}'.format(**groups)
- else:
- req.uri = '{scheme}://{path}'.format(**groups)
- req.revision = groups['revision']
- if groups['fragment']:
- fragment = parse_fragment(groups['fragment'])
- egg = fragment.get('egg')
- req.name, req.extras = parse_extras_require(egg)
- req.hash_name, req.hash = get_hash_info(fragment)
- req.subdirectory = fragment.get('subdirectory')
- for vcs in VCS:
- if req.uri.startswith(vcs):
- req.vcs = vcs
- else:
- assert local_match is not None, 'This should match everything'
- groups = local_match.groupdict()
- req.local_file = True
- if groups['fragment']:
- fragment = parse_fragment(groups['fragment'])
- egg = fragment.get('egg')
- req.name, req.extras = parse_extras_require(egg)
- req.hash_name, req.hash = get_hash_info(fragment)
- req.subdirectory = fragment.get('subdirectory')
- req.path = groups['path']
- return req
- @classmethod
- def parse_line(cls, line):
- """
- Parses a Requirement from a non-editable requirement.
- See: pip/req.py:from_line()
- :param line: a "non-editable" requirement
- :returns: a Requirement instance for the given line
- :raises: ValueError on an invalid requirement
- """
- req = cls(line)
- vcs_match = VCS_REGEX.match(line)
- uri_match = URI_REGEX.match(line)
- local_match = LOCAL_REGEX.match(line)
- if vcs_match is not None:
- groups = vcs_match.groupdict()
- if groups.get('login'):
- req.uri = '{scheme}://{login}@{path}'.format(**groups)
- else:
- req.uri = '{scheme}://{path}'.format(**groups)
- req.revision = groups['revision']
- if groups['fragment']:
- fragment = parse_fragment(groups['fragment'])
- egg = fragment.get('egg')
- req.name, req.extras = parse_extras_require(egg)
- req.hash_name, req.hash = get_hash_info(fragment)
- req.subdirectory = fragment.get('subdirectory')
- for vcs in VCS:
- if req.uri.startswith(vcs):
- req.vcs = vcs
- elif uri_match is not None:
- groups = uri_match.groupdict()
- req.uri = '{scheme}://{path}'.format(**groups)
- if groups['fragment']:
- fragment = parse_fragment(groups['fragment'])
- egg = fragment.get('egg')
- req.name, req.extras = parse_extras_require(egg)
- req.hash_name, req.hash = get_hash_info(fragment)
- req.subdirectory = fragment.get('subdirectory')
- if groups['scheme'] == 'file':
- req.local_file = True
- elif '#egg=' in line:
- # Assume a local file match
- assert local_match is not None, 'This should match everything'
- groups = local_match.groupdict()
- req.local_file = True
- if groups['fragment']:
- fragment = parse_fragment(groups['fragment'])
- egg = fragment.get('egg')
- name, extras = parse_extras_require(egg)
- req.name = fragment.get('egg')
- req.hash_name, req.hash = get_hash_info(fragment)
- req.subdirectory = fragment.get('subdirectory')
- req.path = groups['path']
- elif line.startswith('./'):
- setup_file = open(line + "/setup.py", "r")
- setup_content = setup_file.read()
- name_search = NAME_EQ_REGEX.search(setup_content)
- if name_search:
- req.name = name_search.group(1)
- req.local_file = True
- else:
- # This is a requirement specifier.
- # Delegate to pkg_resources and hope for the best
- req.specifier = True
- # an optional per-requirement param is not part of the req specifier
- # see https://pip.pypa.io/en/stable/reference/pip_install/#per-requirement-overrides
- # and https://pip.pypa.io/en/stable/reference/pip_install/#hash-checking-mode
- line = PER_REQ_PARAM_REGEX.sub('', line)
- pkg_req = Req.parse(line)
- req.name = pkg_req.unsafe_name
- req.extras = list(pkg_req.extras)
- req.specs = pkg_req.specs
- return req
- @classmethod
- def parse(cls, line):
- """
- Parses a Requirement from a line of a requirement file.
- :param line: a line of a requirement file
- :returns: a Requirement instance for the given line
- :raises: ValueError on an invalid requirement
- """
- if line.startswith('-e') or line.startswith('--editable'):
- # Editable installs are either a local project path
- # or a VCS project URI
- return cls.parse_editable(
- EDITABLE_REGEX.sub('', line))
- return cls.parse_line(line)
|