123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474 |
- """Backing implementation for InstallRequirement's various constructors
- The idea here is that these formed a major chunk of InstallRequirement's size
- so, moving them and support code dedicated to them outside of that class
- helps creates for better understandability for the rest of the code.
- These are meant to be used elsewhere within pip to create instances of
- InstallRequirement.
- """
- import logging
- import os
- import re
- from typing import Any, Dict, Optional, Set, Tuple, Union
- from pip._vendor.packaging.markers import Marker
- from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
- from pip._vendor.packaging.specifiers import Specifier
- from pip._vendor.pkg_resources import RequirementParseError, parse_requirements
- from pip._internal.exceptions import InstallationError
- from pip._internal.models.index import PyPI, TestPyPI
- from pip._internal.models.link import Link
- from pip._internal.models.wheel import Wheel
- from pip._internal.pyproject import make_pyproject_path
- from pip._internal.req.req_file import ParsedRequirement
- from pip._internal.req.req_install import InstallRequirement
- from pip._internal.utils.filetypes import is_archive_file
- from pip._internal.utils.misc import is_installable_dir
- from pip._internal.utils.urls import path_to_url
- from pip._internal.vcs import is_url, vcs
- __all__ = [
- "install_req_from_editable", "install_req_from_line",
- "parse_editable"
- ]
- logger = logging.getLogger(__name__)
- operators = Specifier._operators.keys()
- def _strip_extras(path: str) -> Tuple[str, Optional[str]]:
- m = re.match(r'^(.+)(\[[^\]]+\])$', path)
- extras = None
- if m:
- path_no_extras = m.group(1)
- extras = m.group(2)
- else:
- path_no_extras = path
- return path_no_extras, extras
- def convert_extras(extras: Optional[str]) -> Set[str]:
- if not extras:
- return set()
- return Requirement("placeholder" + extras.lower()).extras
- def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
- """Parses an editable requirement into:
- - a requirement name
- - an URL
- - extras
- - editable options
- Accepted requirements:
- svn+http://blahblah@rev#egg=Foobar[baz]&subdirectory=version_subdir
- .[some_extra]
- """
- url = editable_req
- # If a file path is specified with extras, strip off the extras.
- url_no_extras, extras = _strip_extras(url)
- if os.path.isdir(url_no_extras):
- setup_py = os.path.join(url_no_extras, 'setup.py')
- setup_cfg = os.path.join(url_no_extras, 'setup.cfg')
- if not os.path.exists(setup_py) and not os.path.exists(setup_cfg):
- msg = (
- 'File "setup.py" or "setup.cfg" not found. Directory cannot be '
- 'installed in editable mode: {}'
- .format(os.path.abspath(url_no_extras))
- )
- pyproject_path = make_pyproject_path(url_no_extras)
- if os.path.isfile(pyproject_path):
- msg += (
- '\n(A "pyproject.toml" file was found, but editable '
- 'mode currently requires a setuptools-based build.)'
- )
- raise InstallationError(msg)
- # Treating it as code that has already been checked out
- url_no_extras = path_to_url(url_no_extras)
- if url_no_extras.lower().startswith('file:'):
- package_name = Link(url_no_extras).egg_fragment
- if extras:
- return (
- package_name,
- url_no_extras,
- Requirement("placeholder" + extras.lower()).extras,
- )
- else:
- return package_name, url_no_extras, set()
- for version_control in vcs:
- if url.lower().startswith(f'{version_control}:'):
- url = f'{version_control}+{url}'
- break
- link = Link(url)
- if not link.is_vcs:
- backends = ", ".join(vcs.all_schemes)
- raise InstallationError(
- f'{editable_req} is not a valid editable requirement. '
- f'It should either be a path to a local project or a VCS URL '
- f'(beginning with {backends}).'
- )
- package_name = link.egg_fragment
- if not package_name:
- raise InstallationError(
- "Could not detect requirement name for '{}', please specify one "
- "with #egg=your_package_name".format(editable_req)
- )
- return package_name, url, set()
- def deduce_helpful_msg(req: str) -> str:
- """Returns helpful msg in case requirements file does not exist,
- or cannot be parsed.
- :params req: Requirements file path
- """
- msg = ""
- if os.path.exists(req):
- msg = " The path does exist. "
- # Try to parse and check if it is a requirements file.
- try:
- with open(req) as fp:
- # parse first line only
- next(parse_requirements(fp.read()))
- msg += (
- "The argument you provided "
- "({}) appears to be a"
- " requirements file. If that is the"
- " case, use the '-r' flag to install"
- " the packages specified within it."
- ).format(req)
- except RequirementParseError:
- logger.debug(
- "Cannot parse '%s' as requirements file", req, exc_info=True
- )
- else:
- msg += f" File '{req}' does not exist."
- return msg
- class RequirementParts:
- def __init__(
- self,
- requirement: Optional[Requirement],
- link: Optional[Link],
- markers: Optional[Marker],
- extras: Set[str],
- ):
- self.requirement = requirement
- self.link = link
- self.markers = markers
- self.extras = extras
- def parse_req_from_editable(editable_req: str) -> RequirementParts:
- name, url, extras_override = parse_editable(editable_req)
- if name is not None:
- try:
- req: Optional[Requirement] = Requirement(name)
- except InvalidRequirement:
- raise InstallationError(f"Invalid requirement: '{name}'")
- else:
- req = None
- link = Link(url)
- return RequirementParts(req, link, None, extras_override)
- # ---- The actual constructors follow ----
- def install_req_from_editable(
- editable_req: str,
- comes_from: Optional[Union[InstallRequirement, str]] = None,
- use_pep517: Optional[bool] = None,
- isolated: bool = False,
- options: Optional[Dict[str, Any]] = None,
- constraint: bool = False,
- user_supplied: bool = False,
- ) -> InstallRequirement:
- parts = parse_req_from_editable(editable_req)
- return InstallRequirement(
- parts.requirement,
- comes_from=comes_from,
- user_supplied=user_supplied,
- editable=True,
- link=parts.link,
- constraint=constraint,
- use_pep517=use_pep517,
- isolated=isolated,
- install_options=options.get("install_options", []) if options else [],
- global_options=options.get("global_options", []) if options else [],
- hash_options=options.get("hashes", {}) if options else {},
- extras=parts.extras,
- )
- def _looks_like_path(name: str) -> bool:
- """Checks whether the string "looks like" a path on the filesystem.
- This does not check whether the target actually exists, only judge from the
- appearance.
- Returns true if any of the following conditions is true:
- * a path separator is found (either os.path.sep or os.path.altsep);
- * a dot is found (which represents the current directory).
- """
- if os.path.sep in name:
- return True
- if os.path.altsep is not None and os.path.altsep in name:
- return True
- if name.startswith("."):
- return True
- return False
- def _get_url_from_path(path: str, name: str) -> Optional[str]:
- """
- First, it checks whether a provided path is an installable directory. If it
- is, returns the path.
- If false, check if the path is an archive file (such as a .whl).
- The function checks if the path is a file. If false, if the path has
- an @, it will treat it as a PEP 440 URL requirement and return the path.
- """
- if _looks_like_path(name) and os.path.isdir(path):
- if is_installable_dir(path):
- return path_to_url(path)
- raise InstallationError(
- f"Directory {name!r} is not installable. Neither 'setup.py' "
- "nor 'pyproject.toml' found."
- )
- if not is_archive_file(path):
- return None
- if os.path.isfile(path):
- return path_to_url(path)
- urlreq_parts = name.split('@', 1)
- if len(urlreq_parts) >= 2 and not _looks_like_path(urlreq_parts[0]):
- # If the path contains '@' and the part before it does not look
- # like a path, try to treat it as a PEP 440 URL req instead.
- return None
- logger.warning(
- 'Requirement %r looks like a filename, but the '
- 'file does not exist',
- name
- )
- return path_to_url(path)
- def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementParts:
- if is_url(name):
- marker_sep = '; '
- else:
- marker_sep = ';'
- if marker_sep in name:
- name, markers_as_string = name.split(marker_sep, 1)
- markers_as_string = markers_as_string.strip()
- if not markers_as_string:
- markers = None
- else:
- markers = Marker(markers_as_string)
- else:
- markers = None
- name = name.strip()
- req_as_string = None
- path = os.path.normpath(os.path.abspath(name))
- link = None
- extras_as_string = None
- if is_url(name):
- link = Link(name)
- else:
- p, extras_as_string = _strip_extras(path)
- url = _get_url_from_path(p, name)
- if url is not None:
- link = Link(url)
- # it's a local file, dir, or url
- if link:
- # Handle relative file URLs
- if link.scheme == 'file' and re.search(r'\.\./', link.url):
- link = Link(
- path_to_url(os.path.normpath(os.path.abspath(link.path))))
- # wheel file
- if link.is_wheel:
- wheel = Wheel(link.filename) # can raise InvalidWheelFilename
- req_as_string = f"{wheel.name}=={wheel.version}"
- else:
- # set the req to the egg fragment. when it's not there, this
- # will become an 'unnamed' requirement
- req_as_string = link.egg_fragment
- # a requirement specifier
- else:
- req_as_string = name
- extras = convert_extras(extras_as_string)
- def with_source(text: str) -> str:
- if not line_source:
- return text
- return f'{text} (from {line_source})'
- def _parse_req_string(req_as_string: str) -> Requirement:
- try:
- req = Requirement(req_as_string)
- except InvalidRequirement:
- if os.path.sep in req_as_string:
- add_msg = "It looks like a path."
- add_msg += deduce_helpful_msg(req_as_string)
- elif ('=' in req_as_string and
- not any(op in req_as_string for op in operators)):
- add_msg = "= is not a valid operator. Did you mean == ?"
- else:
- add_msg = ''
- msg = with_source(
- f'Invalid requirement: {req_as_string!r}'
- )
- if add_msg:
- msg += f'\nHint: {add_msg}'
- raise InstallationError(msg)
- else:
- # Deprecate extras after specifiers: "name>=1.0[extras]"
- # This currently works by accident because _strip_extras() parses
- # any extras in the end of the string and those are saved in
- # RequirementParts
- for spec in req.specifier:
- spec_str = str(spec)
- if spec_str.endswith(']'):
- msg = f"Extras after version '{spec_str}'."
- raise InstallationError(msg)
- return req
- if req_as_string is not None:
- req: Optional[Requirement] = _parse_req_string(req_as_string)
- else:
- req = None
- return RequirementParts(req, link, markers, extras)
- def install_req_from_line(
- name: str,
- comes_from: Optional[Union[str, InstallRequirement]] = None,
- use_pep517: Optional[bool] = None,
- isolated: bool = False,
- options: Optional[Dict[str, Any]] = None,
- constraint: bool = False,
- line_source: Optional[str] = None,
- user_supplied: bool = False,
- ) -> InstallRequirement:
- """Creates an InstallRequirement from a name, which might be a
- requirement, directory containing 'setup.py', filename, or URL.
- :param line_source: An optional string describing where the line is from,
- for logging purposes in case of an error.
- """
- parts = parse_req_from_line(name, line_source)
- return InstallRequirement(
- parts.requirement, comes_from, link=parts.link, markers=parts.markers,
- use_pep517=use_pep517, isolated=isolated,
- install_options=options.get("install_options", []) if options else [],
- global_options=options.get("global_options", []) if options else [],
- hash_options=options.get("hashes", {}) if options else {},
- constraint=constraint,
- extras=parts.extras,
- user_supplied=user_supplied,
- )
- def install_req_from_req_string(
- req_string: str,
- comes_from: Optional[InstallRequirement] = None,
- isolated: bool = False,
- use_pep517: Optional[bool] = None,
- user_supplied: bool = False,
- ) -> InstallRequirement:
- try:
- req = Requirement(req_string)
- except InvalidRequirement:
- raise InstallationError(f"Invalid requirement: '{req_string}'")
- domains_not_allowed = [
- PyPI.file_storage_domain,
- TestPyPI.file_storage_domain,
- ]
- if (req.url and comes_from and comes_from.link and
- comes_from.link.netloc in domains_not_allowed):
- # Explicitly disallow pypi packages that depend on external urls
- raise InstallationError(
- "Packages installed from PyPI cannot depend on packages "
- "which are not also hosted on PyPI.\n"
- "{} depends on {} ".format(comes_from.name, req)
- )
- return InstallRequirement(
- req,
- comes_from,
- isolated=isolated,
- use_pep517=use_pep517,
- user_supplied=user_supplied,
- )
- def install_req_from_parsed_requirement(
- parsed_req: ParsedRequirement,
- isolated: bool = False,
- use_pep517: Optional[bool] = None,
- user_supplied: bool = False,
- ) -> InstallRequirement:
- if parsed_req.is_editable:
- req = install_req_from_editable(
- parsed_req.requirement,
- comes_from=parsed_req.comes_from,
- use_pep517=use_pep517,
- constraint=parsed_req.constraint,
- isolated=isolated,
- user_supplied=user_supplied,
- )
- else:
- req = install_req_from_line(
- parsed_req.requirement,
- comes_from=parsed_req.comes_from,
- use_pep517=use_pep517,
- isolated=isolated,
- options=parsed_req.options,
- constraint=parsed_req.constraint,
- line_source=parsed_req.line_source,
- user_supplied=user_supplied,
- )
- return req
- def install_req_from_link_and_ireq(
- link: Link, ireq: InstallRequirement
- ) -> InstallRequirement:
- return InstallRequirement(
- req=ireq.req,
- comes_from=ireq.comes_from,
- editable=ireq.editable,
- link=link,
- markers=ireq.markers,
- use_pep517=ireq.use_pep517,
- isolated=ireq.isolated,
- install_options=ireq.install_options,
- global_options=ireq.global_options,
- hash_options=ireq.hash_options,
- )
|