Source code for requirements.requirement

# encoding: utf-8

# This file is part of requirements-parser library.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0

from __future__ import unicode_literals

import re
from typing import Any, Dict, List, Match, Optional, Tuple, cast

from pkg_resources import Requirement as Req

from .fragment import get_hash_info, parse_extras_require, parse_fragment
from .vcs import VCS, VCS_SCHEMES

URI_REGEX = re.compile(
    r'^(?P<scheme>https?|file|ftps?)://(?P<path>[^#]+)'
    r'(#(?P<fragment>\S+))?'
)

VCS_SCHEMES_REGEX = r'|'.join([scheme.replace('+', r'\+') for scheme in VCS_SCHEMES])
VCS_REGEX = re.compile(
    rf'^(?P<scheme>{VCS_SCHEMES_REGEX})://((?P<login>[^/@]+)@)?'
    r'(?P<path>[^#@]+)(@(?P<revision>[^#]+))?(#(?P<fragment>\S+))?'
)

# This matches just about everything
LOCAL_REGEX = re.compile(r'^((?P<scheme>file)://)?(?P<path>[^#]+)#(?P<fragment>\S+)?')


[docs] class Requirement: """ 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')]) """ def __init__(self, line: str) -> None: # 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: List[str] = [] self.specs: List[Tuple[str, str]] = [] def __repr__(self) -> str: return f'<Requirement: "{self.line}">' def __getitem__(self, key: str) -> Any: return getattr(self, key) def __eq__(self, other: object) -> bool: if isinstance(other, Requirement): return all([ self.name == other.name, set(self.specs) == set(other.specs), self.editable == other.editable, self.specifier == other.specifier, self.revision == other.revision, self.hash_name == other.hash_name, self.hash == other.hash, set(self.extras) == set(other.extras), ]) return False def __ne__(self, other: object) -> bool: return not self == other def keys(self) -> Any: return self.__dict__.keys()
[docs] @classmethod def parse_editable(cls, line: str) -> 'Requirement': """ 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(f'-e {line}') req.editable = True if ' #' in line: line = line[:line.find(' #')] vcs_match: Optional[Match[str]] = VCS_REGEX.match(line) local_match: Optional[Match[str]] = LOCAL_REGEX.match(line) if vcs_match is not None: groups: Dict[str, str] = vcs_match.groupdict() if groups.get('login'): req.uri = f'{groups["scheme"]}://{groups["login"]}@{groups["path"]}' # type: ignore else: req.uri = f'{groups["scheme"]}://{groups["path"]}' # type: ignore req.revision = groups['revision'] # type: ignore if groups['fragment']: fragment = parse_fragment(groups['fragment']) egg = cast(str, fragment.get('egg')) req.name, req.extras = parse_extras_require(egg) # type: ignore req.hash_name, req.hash = get_hash_info(fragment) # type: ignore req.subdirectory = fragment.get('subdirectory') # type: ignore for vcs in VCS: if str(req.uri).startswith(vcs): req.vcs = vcs # type: ignore elif local_match is not None: groups = local_match.groupdict() req.local_file = True if groups['fragment']: fragment = parse_fragment(groups['fragment']) egg = cast(str, fragment.get('egg')) req.name, req.extras = parse_extras_require(egg) # type: ignore req.hash_name, req.hash = get_hash_info(fragment) # type: ignore req.subdirectory = fragment.get('subdirectory') # type: ignore req.path = cast(str, groups['path']) # type: ignore else: req.local_file = True req.path, req.name = line.rsplit('/', 1) # type: ignore return req
[docs] @classmethod def parse_line(cls, line: str) -> 'Requirement': """ 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: Optional[Match[str]] = VCS_REGEX.match(line) uri_match: Optional[Match[str]] = URI_REGEX.match(line) local_match: Optional[Match[str]] = LOCAL_REGEX.match(line) if vcs_match is not None: groups = vcs_match.groupdict() if groups.get('login'): req.uri = f'{groups["scheme"]}://{groups["login"]}@{groups["path"]}' # type: ignore else: req.uri = f'{groups["scheme"]}://{groups["path"]}' # type: ignore req.revision = groups['revision'] # type: ignore if groups['fragment']: fragment = parse_fragment(groups['fragment']) egg = fragment.get('egg') req.name, req.extras = parse_extras_require(egg) # type: ignore req.hash_name, req.hash = get_hash_info(fragment) # type: ignore req.subdirectory = fragment.get('subdirectory') # type: ignore for vcs in VCS: if str(req.uri).startswith(vcs): req.vcs = vcs # type: ignore elif uri_match is not None: groups = uri_match.groupdict() req.uri = f'{groups["scheme"]}://{groups["path"]}' # type: ignore if groups['fragment']: fragment = parse_fragment(groups['fragment']) egg = fragment.get('egg') req.name, req.extras = parse_extras_require(egg) # type: ignore req.hash_name, req.hash = get_hash_info(fragment) # type: ignore req.subdirectory = fragment.get('subdirectory') # type: ignore 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') # type: ignore req.hash_name, req.hash = get_hash_info(fragment) # type: ignore req.subdirectory = fragment.get('subdirectory') # type: ignore req.path = groups['path'] # type: ignore else: # This is a requirement specifier. # Delegate to pkg_resources and hope for the best req.specifier = True pkg_req = Req.parse(line) req.name = pkg_req.unsafe_name # type: ignore req.extras = list(pkg_req.extras) req.specs = pkg_req.specs return req
[docs] @classmethod def parse(cls, line: str) -> 'Requirement': """ 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( re.sub(r'^(-e|--editable=?)\s*', '', line)) if ' --hash=' in line: line = line[:line.find(' --hash=')] if ' \\' in line: line = line[:line.find(' \\')] return cls.parse_line(line)