From 69b494aa29096838addad0735359f9406d74252a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 21 Aug 2018 20:02:25 +0530 Subject: [PATCH 1/3] Move InstallRequirement.from_editable to a constructors module --- src/pip/_internal/cli/base_command.py | 3 +- src/pip/_internal/operations/freeze.py | 3 +- src/pip/_internal/req/constructors.py | 116 +++++++++++++++++++++++++ src/pip/_internal/req/req_file.py | 3 +- src/pip/_internal/req/req_install.py | 96 +------------------- tests/unit/test_req.py | 12 +-- tests/unit/test_req_file.py | 5 +- 7 files changed, 133 insertions(+), 105 deletions(-) create mode 100644 src/pip/_internal/req/constructors.py diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 75c6c9f0f86..6e1fd5b1af5 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -22,6 +22,7 @@ ) from pip._internal.index import PackageFinder from pip._internal.locations import running_under_virtualenv +from pip._internal.req.constructors import install_req_from_editable from pip._internal.req.req_file import parse_requirements from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.logging import setup_logging @@ -216,7 +217,7 @@ def populate_requirement_set(requirement_set, args, options, finder, requirement_set.add_requirement(req_to_add) for req in options.editables: - req_to_add = InstallRequirement.from_editable( + req_to_add = install_req_from_editable( req, isolated=options.isolated_mode, wheel_cache=wheel_cache diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 4bbc27b0a76..8ae8afe5d94 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -11,6 +11,7 @@ from pip._internal.exceptions import InstallationError from pip._internal.req import InstallRequirement +from pip._internal.req.constructors import install_req_from_editable from pip._internal.req.req_file import COMMENT_RE from pip._internal.utils.deprecation import deprecated from pip._internal.utils.misc import ( @@ -99,7 +100,7 @@ def freeze( line = line[2:].strip() else: line = line[len('--editable'):].strip().lstrip('=') - line_req = InstallRequirement.from_editable( + line_req = install_req_from_editable( line, isolated=isolated, wheel_cache=wheel_cache, diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py new file mode 100644 index 00000000000..770c66e37dd --- /dev/null +++ b/src/pip/_internal/req/constructors.py @@ -0,0 +1,116 @@ +"""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 os + +from pip._vendor.packaging.requirements import InvalidRequirement, Requirement + +# XXX: Temporarily importing _strip_extras +from pip._internal.download import path_to_url, url_to_path +from pip._internal.exceptions import InstallationError +from pip._internal.models.link import Link +from pip._internal.req.req_install import InstallRequirement, _strip_extras +from pip._internal.vcs import vcs + +__all__ = ["install_req_from_editable", "parse_editable"] + + +def parse_editable(editable_req): + """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): + if not os.path.exists(os.path.join(url_no_extras, 'setup.py')): + raise InstallationError( + "Directory %r is not installable. File 'setup.py' not found." % + url_no_extras + ) + # 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, None + + for version_control in vcs: + if url.lower().startswith('%s:' % version_control): + url = '%s+%s' % (version_control, url) + break + + if '+' not in url: + raise InstallationError( + '%s should either be a path to a local project or a VCS url ' + 'beginning with svn+, git+, hg+, or bzr+' % + editable_req + ) + + vc_type = url.split('+', 1)[0].lower() + + if not vcs.get_backend(vc_type): + error_message = 'For --editable=%s only ' % editable_req + \ + ', '.join([backend.name + '+URL' for backend in vcs.backends]) + \ + ' is currently supported' + raise InstallationError(error_message) + + package_name = Link(url).egg_fragment + if not package_name: + raise InstallationError( + "Could not detect requirement name for '%s', please specify one " + "with #egg=your_package_name" % editable_req + ) + return package_name, url, None + + +def install_req_from_editable( + editable_req, comes_from=None, isolated=False, options=None, + wheel_cache=None, constraint=False +): + name, url, extras_override = parse_editable(editable_req) + if url.startswith('file:'): + source_dir = url_to_path(url) + else: + source_dir = None + + if name is not None: + try: + req = Requirement(name) + except InvalidRequirement: + raise InstallationError("Invalid requirement: '%s'" % name) + else: + req = None + return InstallRequirement( + req, comes_from, source_dir=source_dir, + editable=True, + link=Link(url), + constraint=constraint, + isolated=isolated, + options=options if options else {}, + wheel_cache=wheel_cache, + extras=extras_override or (), + ) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index ffde0353e3b..17bd372dea0 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -16,6 +16,7 @@ from pip._internal.cli import cmdoptions from pip._internal.download import get_file_content from pip._internal.exceptions import RequirementsFileParseError +from pip._internal.req.constructors import install_req_from_editable from pip._internal.req.req_install import InstallRequirement __all__ = ['parse_requirements'] @@ -159,7 +160,7 @@ def process_line(line, filename, line_number, finder=None, comes_from=None, # yield an editable requirement elif opts.editables: isolated = options.isolated_mode if options else False - yield InstallRequirement.from_editable( + yield install_req_from_editable( opts.editables[0], comes_from=line_comes_from, constraint=constraint, isolated=isolated, wheel_cache=wheel_cache ) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index dc2ee0eddcb..a2d8cc82ee4 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -22,9 +22,7 @@ from pip._internal import wheel from pip._internal.build_env import NoOpBuildEnvironment -from pip._internal.download import ( - is_archive_file, is_url, path_to_url, url_to_path, -) +from pip._internal.download import is_archive_file, is_url, path_to_url from pip._internal.exceptions import InstallationError from pip._internal.locations import ( PIP_DELETE_MARKER_FILENAME, running_under_virtualenv, @@ -149,33 +147,6 @@ def __init__(self, req, comes_from, source_dir=None, editable=False, # Constructors # TODO: Move these out of this class into custom methods. - @classmethod - def from_editable(cls, editable_req, comes_from=None, isolated=False, - options=None, wheel_cache=None, constraint=False): - name, url, extras_override = parse_editable(editable_req) - if url.startswith('file:'): - source_dir = url_to_path(url) - else: - source_dir = None - - if name is not None: - try: - req = Requirement(name) - except InvalidRequirement: - raise InstallationError("Invalid requirement: '%s'" % name) - else: - req = None - return cls( - req, comes_from, source_dir=source_dir, - editable=True, - link=Link(url), - constraint=constraint, - isolated=isolated, - options=options if options else {}, - wheel_cache=wheel_cache, - extras=extras_override or (), - ) - @classmethod def from_req(cls, req, comes_from=None, isolated=False, wheel_cache=None): try: @@ -1029,71 +1000,6 @@ def get_install_args(self, global_options, record_filename, root, prefix, return install_args -def parse_editable(editable_req): - """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): - if not os.path.exists(os.path.join(url_no_extras, 'setup.py')): - raise InstallationError( - "Directory %r is not installable. File 'setup.py' not found." % - url_no_extras - ) - # 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, None - - for version_control in vcs: - if url.lower().startswith('%s:' % version_control): - url = '%s+%s' % (version_control, url) - break - - if '+' not in url: - raise InstallationError( - '%s should either be a path to a local project or a VCS url ' - 'beginning with svn+, git+, hg+, or bzr+' % - editable_req - ) - - vc_type = url.split('+', 1)[0].lower() - - if not vcs.get_backend(vc_type): - error_message = 'For --editable=%s only ' % editable_req + \ - ', '.join([backend.name + '+URL' for backend in vcs.backends]) + \ - ' is currently supported' - raise InstallationError(error_message) - - package_name = Link(url).egg_fragment - if not package_name: - raise InstallationError( - "Could not detect requirement name for '%s', please specify one " - "with #egg=your_package_name" % editable_req - ) - return package_name, url, None - - def deduce_helpful_msg(req): """Returns helpful msg in case requirements file does not exist, or cannot be parsed. diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index fc3c0ad0031..30110f1492b 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -17,8 +17,10 @@ from pip._internal.index import PackageFinder from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req import InstallRequirement, RequirementSet +from pip._internal.req.constructors import ( + install_req_from_editable, parse_editable, +) from pip._internal.req.req_file import process_line -from pip._internal.req.req_install import parse_editable from pip._internal.req.req_tracker import RequirementTracker from pip._internal.resolve import Resolver from pip._internal.utils.misc import read_text_file @@ -85,7 +87,7 @@ def test_environment_marker_extras(self, data): non-wheel installs. """ reqset = RequirementSet() - req = InstallRequirement.from_editable( + req = install_req_from_editable( data.packages.join("LocalEnvironMarker") ) req.is_direct = True @@ -398,7 +400,7 @@ def test_url_preserved_line_req(self): def test_url_preserved_editable_req(self): """Confirm the url is preserved in a editable requirement""" url = 'git+http://foo.com@ref#egg=foo' - req = InstallRequirement.from_editable(url) + req = install_req_from_editable(url) assert req.link.url == url @pytest.mark.parametrize('path', ( @@ -512,7 +514,7 @@ def test_extras_for_editable_path_requirement(self): url = '.[ex1,ex2]' filename = 'filename' comes_from = '-r %s (line %s)' % (filename, 1) - req = InstallRequirement.from_editable(url, comes_from=comes_from) + req = install_req_from_editable(url, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} @@ -520,7 +522,7 @@ def test_extras_for_editable_url_requirement(self): url = 'git+https://url#egg=SomeProject[ex1,ex2]' filename = 'filename' comes_from = '-r %s (line %s)' % (filename, 1) - req = InstallRequirement.from_editable(url, comes_from=comes_from) + req = install_req_from_editable(url, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 3f7c293b54b..664196b0367 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -12,6 +12,7 @@ InstallationError, RequirementsFileParseError, ) from pip._internal.index import PackageFinder +from pip._internal.req.constructors import install_req_from_editable from pip._internal.req.req_file import ( break_args_options, ignore_comments, join_lines, parse_requirements, preprocess, process_line, skip_regex, @@ -218,7 +219,7 @@ def test_yield_editable_requirement(self): line = '-e %s' % url filename = 'filename' comes_from = '-r %s (line %s)' % (filename, 1) - req = InstallRequirement.from_editable(url, comes_from=comes_from) + req = install_req_from_editable(url, comes_from=comes_from) assert repr(list(process_line(line, filename, 1))[0]) == repr(req) def test_yield_editable_constraint(self): @@ -226,7 +227,7 @@ def test_yield_editable_constraint(self): line = '-e %s' % url filename = 'filename' comes_from = '-c %s (line %s)' % (filename, 1) - req = InstallRequirement.from_editable( + req = install_req_from_editable( url, comes_from=comes_from, constraint=True) found_req = list(process_line(line, filename, 1, constraint=True))[0] assert repr(found_req) == repr(req) From a5a07fe61c4861c58fded7420c929d56cde11a89 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 21 Aug 2018 20:37:40 +0530 Subject: [PATCH 2/3] Move InstallRequirement.from_line to constructors module --- src/pip/_internal/cli/base_command.py | 7 +- src/pip/_internal/commands/uninstall.py | 5 +- src/pip/_internal/operations/freeze.py | 7 +- src/pip/_internal/req/constructors.py | 164 +++++++++++++++++++++++- src/pip/_internal/req/req_file.py | 7 +- src/pip/_internal/req/req_install.py | 147 +-------------------- tests/functional/test_uninstall.py | 4 +- tests/unit/test_finder.py | 46 +++---- tests/unit/test_req.py | 56 ++++---- tests/unit/test_req_file.py | 15 ++- tests/unit/test_req_install.py | 3 +- 11 files changed, 240 insertions(+), 221 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 6e1fd5b1af5..549864936d3 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -22,9 +22,10 @@ ) from pip._internal.index import PackageFinder from pip._internal.locations import running_under_virtualenv -from pip._internal.req.constructors import install_req_from_editable +from pip._internal.req.constructors import ( + install_req_from_editable, install_req_from_line, +) from pip._internal.req.req_file import parse_requirements -from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.logging import setup_logging from pip._internal.utils.misc import get_prog, normalize_path from pip._internal.utils.outdated import pip_version_check @@ -209,7 +210,7 @@ def populate_requirement_set(requirement_set, args, options, finder, requirement_set.add_requirement(req_to_add) for req in args: - req_to_add = InstallRequirement.from_line( + req_to_add = install_req_from_line( req, None, isolated=options.isolated_mode, wheel_cache=wheel_cache ) diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index a62bf3674aa..0cd6f54bd86 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -4,7 +4,8 @@ from pip._internal.cli.base_command import Command from pip._internal.exceptions import InstallationError -from pip._internal.req import InstallRequirement, parse_requirements +from pip._internal.req import parse_requirements +from pip._internal.req.constructors import install_req_from_line from pip._internal.utils.misc import protect_pip_from_modification_on_windows @@ -47,7 +48,7 @@ def run(self, options, args): with self._build_session(options) as session: reqs_to_uninstall = {} for name in args: - req = InstallRequirement.from_line( + req = install_req_from_line( name, isolated=options.isolated_mode, ) if req.name: diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 8ae8afe5d94..1ceb7fedba5 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -10,8 +10,9 @@ from pip._vendor.pkg_resources import RequirementParseError from pip._internal.exceptions import InstallationError -from pip._internal.req import InstallRequirement -from pip._internal.req.constructors import install_req_from_editable +from pip._internal.req.constructors import ( + install_req_from_editable, install_req_from_line, +) from pip._internal.req.req_file import COMMENT_RE from pip._internal.utils.deprecation import deprecated from pip._internal.utils.misc import ( @@ -106,7 +107,7 @@ def freeze( wheel_cache=wheel_cache, ) else: - line_req = InstallRequirement.from_line( + line_req = install_req_from_line( COMMENT_RE.sub('', line).strip(), isolated=isolated, wheel_cache=wheel_cache, diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 770c66e37dd..b0e641bd7e6 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -8,18 +8,45 @@ InstallRequirement. """ +import logging import os +import re +import traceback +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 -# XXX: Temporarily importing _strip_extras -from pip._internal.download import path_to_url, url_to_path +from pip._internal.download import ( + is_archive_file, is_url, path_to_url, url_to_path, +) from pip._internal.exceptions import InstallationError from pip._internal.models.link import Link -from pip._internal.req.req_install import InstallRequirement, _strip_extras +from pip._internal.req.req_install import InstallRequirement +from pip._internal.utils.misc import is_installable_dir from pip._internal.vcs import vcs +from pip._internal.wheel import Wheel -__all__ = ["install_req_from_editable", "parse_editable"] +__all__ = [ + "install_req_from_editable", "install_req_from_line", + "parse_editable" +] + +logger = logging.getLogger(__name__) +operators = Specifier._operators.keys() + + +def _strip_extras(path): + 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 parse_editable(editable_req): @@ -87,6 +114,36 @@ def parse_editable(editable_req): return package_name, url, None +def deduce_helpful_msg(req): + """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 = " It does exist." + # Try to parse and check if it is a requirements file. + try: + with open(req, 'r') as fp: + # parse first line only + next(parse_requirements(fp.read())) + msg += " The argument you provided " + \ + "(%s) appears to be a" % (req) + \ + " requirements file. If that is the" + \ + " case, use the '-r' flag to install" + \ + " the packages specified within it." + except RequirementParseError: + logger.debug("Cannot parse '%s' as requirements \ + file" % (req), exc_info=1) + else: + msg += " File '%s' does not exist." % (req) + return msg + + +# ---- The actual constructors follow ---- + + def install_req_from_editable( editable_req, comes_from=None, isolated=False, options=None, wheel_cache=None, constraint=False @@ -114,3 +171,102 @@ def install_req_from_editable( wheel_cache=wheel_cache, extras=extras_override or (), ) + + +def install_req_from_line( + name, comes_from=None, isolated=False, options=None, wheel_cache=None, + constraint=False +): + """Creates an InstallRequirement from a name, which might be a + requirement, directory containing 'setup.py', filename, or URL. + """ + if is_url(name): + marker_sep = '; ' + else: + marker_sep = ';' + if marker_sep in name: + name, markers = name.split(marker_sep, 1) + markers = markers.strip() + if not markers: + markers = None + else: + markers = Marker(markers) + else: + markers = None + name = name.strip() + req = None + path = os.path.normpath(os.path.abspath(name)) + link = None + extras = None + + if is_url(name): + link = Link(name) + else: + p, extras = _strip_extras(path) + looks_like_dir = os.path.isdir(p) and ( + os.path.sep in name or + (os.path.altsep is not None and os.path.altsep in name) or + name.startswith('.') + ) + if looks_like_dir: + if not is_installable_dir(p): + raise InstallationError( + "Directory %r is not installable. Neither 'setup.py' " + "nor 'pyproject.toml' found." % name + ) + link = Link(path_to_url(p)) + elif is_archive_file(p): + if not os.path.isfile(p): + logger.warning( + 'Requirement %r looks like a filename, but the ' + 'file does not exist', + name + ) + link = Link(path_to_url(p)) + + # 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 = "%s==%s" % (wheel.name, wheel.version) + else: + # set the req to the egg fragment. when it's not there, this + # will become an 'unnamed' requirement + req = link.egg_fragment + + # a requirement specifier + else: + req = name + + if extras: + extras = Requirement("placeholder" + extras.lower()).extras + else: + extras = () + if req is not None: + try: + req = Requirement(req) + except InvalidRequirement: + if os.path.sep in req: + add_msg = "It looks like a path." + add_msg += deduce_helpful_msg(req) + elif '=' in req and not any(op in req for op in operators): + add_msg = "= is not a valid operator. Did you mean == ?" + else: + add_msg = traceback.format_exc() + raise InstallationError( + "Invalid requirement: '%s'\n%s" % (req, add_msg) + ) + + return InstallRequirement( + req, comes_from, link=link, markers=markers, + isolated=isolated, + options=options if options else {}, + wheel_cache=wheel_cache, + constraint=constraint, + extras=extras, + ) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 17bd372dea0..e7acf7cb8e3 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -16,8 +16,9 @@ from pip._internal.cli import cmdoptions from pip._internal.download import get_file_content from pip._internal.exceptions import RequirementsFileParseError -from pip._internal.req.constructors import install_req_from_editable -from pip._internal.req.req_install import InstallRequirement +from pip._internal.req.constructors import ( + install_req_from_editable, install_req_from_line, +) __all__ = ['parse_requirements'] @@ -152,7 +153,7 @@ def process_line(line, filename, line_number, finder=None, comes_from=None, 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( + yield install_req_from_line( args_str, line_comes_from, constraint=constraint, isolated=isolated, options=req_options, wheel_cache=wheel_cache ) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index a2d8cc82ee4..c1989a50ae0 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -2,27 +2,21 @@ import logging import os -import re import shutil import sys import sysconfig -import traceback import zipfile from distutils.util import change_root from pip._vendor import pkg_resources, six -from pip._vendor.packaging import specifiers -from pip._vendor.packaging.markers import Marker from pip._vendor.packaging.requirements import InvalidRequirement, Requirement from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import Version from pip._vendor.packaging.version import parse as parse_version from pip._vendor.pep517.wrappers import Pep517HookCaller -from pip._vendor.pkg_resources import RequirementParseError, parse_requirements from pip._internal import wheel from pip._internal.build_env import NoOpBuildEnvironment -from pip._internal.download import is_archive_file, is_url, path_to_url from pip._internal.exceptions import InstallationError from pip._internal.locations import ( PIP_DELETE_MARKER_FILENAME, running_under_virtualenv, @@ -37,31 +31,17 @@ from pip._internal.utils.misc import ( _make_build_dir, ask_path_exists, backup_dir, call_subprocess, display_path, dist_in_site_packages, dist_in_usersite, ensure_dir, - get_installed_version, is_installable_dir, rmtree, + get_installed_version, rmtree, ) from pip._internal.utils.packaging import get_metadata from pip._internal.utils.setuptools_build import SETUPTOOLS_SHIM from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.ui import open_spinner from pip._internal.vcs import vcs -from pip._internal.wheel import Wheel, move_wheel_files +from pip._internal.wheel import move_wheel_files logger = logging.getLogger(__name__) -operators = specifiers.Specifier._operators.keys() - - -def _strip_extras(path): - 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 - class InstallRequirement(object): """ @@ -168,102 +148,6 @@ def from_req(cls, req, comes_from=None, isolated=False, wheel_cache=None): return cls(req, comes_from, isolated=isolated, wheel_cache=wheel_cache) - @classmethod - def from_line( - cls, name, comes_from=None, isolated=False, options=None, - wheel_cache=None, constraint=False): - """Creates an InstallRequirement from a name, which might be a - requirement, directory containing 'setup.py', filename, or URL. - """ - if is_url(name): - marker_sep = '; ' - else: - marker_sep = ';' - if marker_sep in name: - name, markers = name.split(marker_sep, 1) - markers = markers.strip() - if not markers: - markers = None - else: - markers = Marker(markers) - else: - markers = None - name = name.strip() - req = None - path = os.path.normpath(os.path.abspath(name)) - link = None - extras = None - - if is_url(name): - link = Link(name) - else: - p, extras = _strip_extras(path) - looks_like_dir = os.path.isdir(p) and ( - os.path.sep in name or - (os.path.altsep is not None and os.path.altsep in name) or - name.startswith('.') - ) - if looks_like_dir: - if not is_installable_dir(p): - raise InstallationError( - "Directory %r is not installable. Neither 'setup.py' " - "nor 'pyproject.toml' found." % name - ) - link = Link(path_to_url(p)) - elif is_archive_file(p): - if not os.path.isfile(p): - logger.warning( - 'Requirement %r looks like a filename, but the ' - 'file does not exist', - name - ) - link = Link(path_to_url(p)) - - # 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 = "%s==%s" % (wheel.name, wheel.version) - else: - # set the req to the egg fragment. when it's not there, this - # will become an 'unnamed' requirement - req = link.egg_fragment - - # a requirement specifier - else: - req = name - - if extras: - extras = Requirement("placeholder" + extras.lower()).extras - else: - extras = () - if req is not None: - try: - req = Requirement(req) - except InvalidRequirement: - if os.path.sep in req: - add_msg = "It looks like a path." - add_msg += deduce_helpful_msg(req) - elif '=' in req and not any(op in req for op in operators): - add_msg = "= is not a valid operator. Did you mean == ?" - else: - add_msg = traceback.format_exc() - raise InstallationError( - "Invalid requirement: '%s'\n%s" % (req, add_msg)) - return cls( - req, comes_from, link=link, markers=markers, - isolated=isolated, - options=options if options else {}, - wheel_cache=wheel_cache, - constraint=constraint, - extras=extras, - ) - def __str__(self): if self.req: s = str(self.req) @@ -998,30 +882,3 @@ def get_install_args(self, global_options, record_filename, root, prefix, py_ver_str, self.name)] return install_args - - -def deduce_helpful_msg(req): - """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 = " It does exist." - # Try to parse and check if it is a requirements file. - try: - with open(req, 'r') as fp: - # parse first line only - next(parse_requirements(fp.read())) - msg += " The argument you provided " + \ - "(%s) appears to be a" % (req) + \ - " requirements file. If that is the" + \ - " case, use the '-r' flag to install" + \ - " the packages specified within it." - except RequirementParseError: - logger.debug("Cannot parse '%s' as requirements \ - file" % (req), exc_info=1) - else: - msg += " File '%s' does not exist." % (req) - return msg diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index c9ce8b8d347..7fd47b17ecd 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -11,7 +11,7 @@ import pretend import pytest -from pip._internal.req import InstallRequirement +from pip._internal.req.constructors import install_req_from_line from pip._internal.utils.misc import rmtree from tests.lib import assert_all_changes, create_test_package_with_setup from tests.lib.local_repos import local_checkout, local_repo @@ -439,7 +439,7 @@ def test_uninstall_non_local_distutils(caplog, monkeypatch, tmpdir): get_dist = pretend.call_recorder(lambda x: dist) monkeypatch.setattr("pip._vendor.pkg_resources.get_distribution", get_dist) - req = InstallRequirement.from_line("thing") + req = install_req_from_line("thing") req.uninstall() assert os.path.exists(einfo) diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index 51fccfae1f1..969f81e2dc8 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -14,13 +14,13 @@ from pip._internal.index import ( FormatControl, InstallationCandidate, Link, PackageFinder, fmt_ctl_formats, ) -from pip._internal.req import InstallRequirement +from pip._internal.req.constructors import install_req_from_line def test_no_mpkg(data): """Finder skips zipfiles with "macosx10" in the name.""" finder = PackageFinder([data.find_links], [], session=PipSession()) - req = InstallRequirement.from_line("pkgwithmpkg") + req = install_req_from_line("pkgwithmpkg") found = finder.find_requirement(req, False) assert found.url.endswith("pkgwithmpkg-1.0.tar.gz"), found @@ -29,7 +29,7 @@ def test_no_mpkg(data): def test_no_partial_name_match(data): """Finder requires the full project name to match, not just beginning.""" finder = PackageFinder([data.find_links], [], session=PipSession()) - req = InstallRequirement.from_line("gmpy") + req = install_req_from_line("gmpy") found = finder.find_requirement(req, False) assert found.url.endswith("gmpy-1.15.tar.gz"), found @@ -40,7 +40,7 @@ def test_tilde(): session = PipSession() with patch('pip._internal.index.os.path.exists', return_value=True): finder = PackageFinder(['~/python-pkgs'], [], session=session) - req = InstallRequirement.from_line("gmpy") + req = install_req_from_line("gmpy") with pytest.raises(DistributionNotFound): finder.find_requirement(req, False) @@ -53,7 +53,7 @@ def test_duplicates_sort_ok(data): [], session=PipSession(), ) - req = InstallRequirement.from_line("duplicate") + req = install_req_from_line("duplicate") found = finder.find_requirement(req, False) assert found.url.endswith("duplicate-1.0.tar.gz"), found @@ -61,7 +61,7 @@ def test_duplicates_sort_ok(data): def test_finder_detects_latest_find_links(data): """Test PackageFinder detects latest using find-links""" - req = InstallRequirement.from_line('simple', None) + req = install_req_from_line('simple', None) finder = PackageFinder([data.find_links], [], session=PipSession()) link = finder.find_requirement(req, False) assert link.url.endswith("simple-3.0.tar.gz") @@ -69,7 +69,7 @@ def test_finder_detects_latest_find_links(data): def test_incorrect_case_file_index(data): """Test PackageFinder detects latest using wrong case""" - req = InstallRequirement.from_line('dinner', None) + req = install_req_from_line('dinner', None) finder = PackageFinder([], [data.find_links3], session=PipSession()) link = finder.find_requirement(req, False) assert link.url.endswith("Dinner-2.0.tar.gz") @@ -78,7 +78,7 @@ def test_incorrect_case_file_index(data): @pytest.mark.network def test_finder_detects_latest_already_satisfied_find_links(data): """Test PackageFinder detects latest already satisfied using find-links""" - req = InstallRequirement.from_line('simple', None) + req = install_req_from_line('simple', None) # the latest simple in local pkgs is 3.0 latest_version = "3.0" satisfied_by = Mock( @@ -96,7 +96,7 @@ def test_finder_detects_latest_already_satisfied_find_links(data): @pytest.mark.network def test_finder_detects_latest_already_satisfied_pypi_links(): """Test PackageFinder detects latest already satisfied using pypi links""" - req = InstallRequirement.from_line('initools', None) + req = install_req_from_line('initools', None) # the latest initools on pypi is 0.3.1 latest_version = "0.3.1" satisfied_by = Mock( @@ -123,7 +123,7 @@ def test_skip_invalid_wheel_link(self, caplog, data): """ caplog.set_level(logging.DEBUG) - req = InstallRequirement.from_line("invalid") + req = install_req_from_line("invalid") # data.find_links contains "invalid.whl", which is an invalid wheel finder = PackageFinder( [data.find_links], @@ -148,7 +148,7 @@ def test_not_find_wheel_not_supported(self, data, monkeypatch): lambda **kw: [("py1", "none", "any")], ) - req = InstallRequirement.from_line("simple.dist") + req = install_req_from_line("simple.dist") finder = PackageFinder( [data.find_links], [], @@ -169,7 +169,7 @@ def test_find_wheel_supported(self, data, monkeypatch): lambda **kw: [('py2', 'none', 'any')], ) - req = InstallRequirement.from_line("simple.dist") + req = install_req_from_line("simple.dist") finder = PackageFinder( [data.find_links], [], @@ -185,7 +185,7 @@ def test_wheel_over_sdist_priority(self, data): Test wheels have priority over sdists. `test_link_sorting` also covers this at lower level """ - req = InstallRequirement.from_line("priority") + req = install_req_from_line("priority") finder = PackageFinder( [data.find_links], [], @@ -199,7 +199,7 @@ def test_existing_over_wheel_priority(self, data): Test existing install has priority over wheels. `test_link_sorting` also covers this at a lower level """ - req = InstallRequirement.from_line('priority', None) + req = install_req_from_line('priority', None) latest_version = "1.0" satisfied_by = Mock( location="/path", @@ -284,7 +284,7 @@ def test_link_sorting_wheels_with_build_tags(self): def test_finder_priority_file_over_page(data): """Test PackageFinder prefers file links over equivalent page links""" - req = InstallRequirement.from_line('gmpy==1.15', None) + req = install_req_from_line('gmpy==1.15', None) finder = PackageFinder( [data.find_links], ["http://pypi.org/simple/"], @@ -304,7 +304,7 @@ def test_finder_deplink(): """ Test PackageFinder with dependency links only """ - req = InstallRequirement.from_line('gmpy==1.15', None) + req = install_req_from_line('gmpy==1.15', None) finder = PackageFinder( [], [], @@ -323,7 +323,7 @@ def test_finder_priority_page_over_deplink(): """ Test PackageFinder prefers page links over equivalent dependency links """ - req = InstallRequirement.from_line('pip==1.5.6', None) + req = install_req_from_line('pip==1.5.6', None) finder = PackageFinder( [], ["https://pypi.org/simple/"], @@ -346,7 +346,7 @@ def test_finder_priority_page_over_deplink(): def test_finder_priority_nonegg_over_eggfragments(): """Test PackageFinder prefers non-egg links over "#egg=" links""" - req = InstallRequirement.from_line('bar==1.0', None) + req = install_req_from_line('bar==1.0', None) links = ['http://foo/bar.py#egg=bar-1.0', 'http://foo/bar-1.0.tar.gz'] finder = PackageFinder(links, [], session=PipSession()) @@ -377,7 +377,7 @@ def test_finder_only_installs_stable_releases(data): Test PackageFinder only accepts stable versioned releases by default. """ - req = InstallRequirement.from_line("bar", None) + req = install_req_from_line("bar", None) # using a local index (that has pre & dev releases) finder = PackageFinder([], [data.index_url("pre")], session=PipSession()) @@ -431,7 +431,7 @@ def test_finder_installs_pre_releases(data): Test PackageFinder finds pre-releases if asked to. """ - req = InstallRequirement.from_line("bar", None) + req = install_req_from_line("bar", None) # using a local index (that has pre & dev releases) finder = PackageFinder( @@ -471,7 +471,7 @@ def test_finder_installs_dev_releases(data): Test PackageFinder finds dev releases if asked to. """ - req = InstallRequirement.from_line("bar", None) + req = install_req_from_line("bar", None) # using a local index (that has dev releases) finder = PackageFinder( @@ -487,7 +487,7 @@ def test_finder_installs_pre_releases_with_version_spec(): """ Test PackageFinder only accepts stable versioned releases by default. """ - req = InstallRequirement.from_line("bar>=0.0.dev0", None) + req = install_req_from_line("bar>=0.0.dev0", None) links = ["https://foo/bar-1.0.tar.gz", "https://foo/bar-2.0b1.tar.gz"] finder = PackageFinder(links, [], session=PipSession()) @@ -555,7 +555,7 @@ def test_get_index_urls_locations(): finder = PackageFinder( [], ['file://index1/', 'file://index2'], session=PipSession()) locations = finder._get_index_urls_locations( - InstallRequirement.from_line('Complex_Name').name) + install_req_from_line('Complex_Name').name) assert locations == ['file://index1/complex-name/', 'file://index2/complex-name/'] diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 30110f1492b..09941d0f99a 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -18,7 +18,7 @@ from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req import InstallRequirement, RequirementSet from pip._internal.req.constructors import ( - install_req_from_editable, parse_editable, + install_req_from_editable, install_req_from_line, parse_editable, ) from pip._internal.req.req_file import process_line from pip._internal.req.req_tracker import RequirementTracker @@ -68,7 +68,7 @@ def test_no_reuse_existing_build_dir(self, data): os.makedirs(build_dir) open(os.path.join(build_dir, "setup.py"), 'w') reqset = RequirementSet() - req = InstallRequirement.from_line('simple') + req = install_req_from_line('simple') req.is_direct = True reqset.add_requirement(req) finder = PackageFinder([data.find_links], [], session=PipSession()) @@ -341,12 +341,12 @@ def test_url_with_query(self): """InstallRequirement should strip the fragment, but not the query.""" url = 'http://foo.com/?p=bar.git;a=snapshot;h=v0.1;sf=tgz' fragment = '#egg=bar' - req = InstallRequirement.from_line(url + fragment) + req = install_req_from_line(url + fragment) assert req.link.url == url + fragment, req.link def test_unsupported_wheel_link_requirement_raises(self): reqset = RequirementSet() - req = InstallRequirement.from_line( + req = install_req_from_line( 'https://whatever.com/peppercorn-0.4-py2.py3-bogus-any.whl', ) assert req.link is not None @@ -358,7 +358,7 @@ def test_unsupported_wheel_link_requirement_raises(self): def test_unsupported_wheel_local_file_requirement_raises(self, data): reqset = RequirementSet() - req = InstallRequirement.from_line( + req = install_req_from_line( data.packages.join('simple.dist-0.1-py1-none-invalid.whl'), ) assert req.link is not None @@ -369,32 +369,32 @@ def test_unsupported_wheel_local_file_requirement_raises(self, data): reqset.add_requirement(req) def test_installed_version_not_installed(self): - req = InstallRequirement.from_line('simple-0.1-py2.py3-none-any.whl') + req = install_req_from_line('simple-0.1-py2.py3-none-any.whl') assert req.installed_version is None def test_str(self): - req = InstallRequirement.from_line('simple==0.1') + req = install_req_from_line('simple==0.1') assert str(req) == 'simple==0.1' def test_repr(self): - req = InstallRequirement.from_line('simple==0.1') + req = install_req_from_line('simple==0.1') assert repr(req) == ( '' ) def test_invalid_wheel_requirement_raises(self): with pytest.raises(InvalidWheelFilename): - InstallRequirement.from_line('invalid.whl') + install_req_from_line('invalid.whl') def test_wheel_requirement_sets_req_attribute(self): - req = InstallRequirement.from_line('simple-0.1-py2.py3-none-any.whl') + req = install_req_from_line('simple-0.1-py2.py3-none-any.whl') assert isinstance(req.req, Requirement) assert str(req.req) == 'simple==0.1' def test_url_preserved_line_req(self): """Confirm the url is preserved in a non-editable requirement""" url = 'git+http://foo.com@ref#egg=foo' - req = InstallRequirement.from_line(url) + req = install_req_from_line(url) assert req.link.url == url def test_url_preserved_editable_req(self): @@ -409,7 +409,7 @@ def test_url_preserved_editable_req(self): '/path/to/foo.egg-info/'.replace('/', os.path.sep), )) def test_get_dist(self, path): - req = InstallRequirement.from_line('foo') + req = install_req_from_line('foo') req._egg_info_path = path dist = req.get_dist() assert isinstance(dist, pkg_resources.Distribution) @@ -425,14 +425,14 @@ def test_markers(self): # without spaces 'mock3;python_version >= "3"', ): - req = InstallRequirement.from_line(line) + req = install_req_from_line(line) assert req.req.name == 'mock3' assert str(req.req.specifier) == '' assert str(req.markers) == 'python_version >= "3"' def test_markers_semicolon(self): # check that the markers can contain a semicolon - req = InstallRequirement.from_line('semicolon; os_name == "a; b"') + req = install_req_from_line('semicolon; os_name == "a; b"') assert req.req.name == 'semicolon' assert str(req.req.specifier) == '' assert str(req.markers) == 'os_name == "a; b"' @@ -441,14 +441,14 @@ def test_markers_url(self): # test "URL; markers" syntax url = 'http://foo.com/?p=bar.git;a=snapshot;h=v0.1;sf=tgz' line = '%s; python_version >= "3"' % url - req = InstallRequirement.from_line(line) + req = install_req_from_line(line) assert req.link.url == url, req.url assert str(req.markers) == 'python_version >= "3"' # without space, markers are part of the URL url = 'http://foo.com/?p=bar.git;a=snapshot;h=v0.1;sf=tgz' line = '%s;python_version >= "3"' % url - req = InstallRequirement.from_line(line) + req = install_req_from_line(line) assert req.link.url == line, req.url assert req.markers is None @@ -459,7 +459,7 @@ def test_markers_match_from_line(self): 'sys_platform == %r' % sys.platform, ): line = 'name; ' + markers - req = InstallRequirement.from_line(line) + req = install_req_from_line(line) assert str(req.markers) == str(Marker(markers)) assert req.match_markers() @@ -469,7 +469,7 @@ def test_markers_match_from_line(self): 'sys_platform != %r' % sys.platform, ): line = 'name; ' + markers - req = InstallRequirement.from_line(line) + req = install_req_from_line(line) assert str(req.markers) == str(Marker(markers)) assert not req.match_markers() @@ -480,7 +480,7 @@ def test_markers_match(self): 'sys_platform == %r' % sys.platform, ): line = 'name; ' + markers - req = InstallRequirement.from_line(line, comes_from='') + req = install_req_from_line(line, comes_from='') assert str(req.markers) == str(Marker(markers)) assert req.match_markers() @@ -490,7 +490,7 @@ def test_markers_match(self): 'sys_platform != %r' % sys.platform, ): line = 'name; ' + markers - req = InstallRequirement.from_line(line, comes_from='') + req = install_req_from_line(line, comes_from='') assert str(req.markers) == str(Marker(markers)) assert not req.match_markers() @@ -498,7 +498,7 @@ def test_extras_for_line_path_requirement(self): line = 'SomeProject[ex1,ex2]' filename = 'filename' comes_from = '-r %s (line %s)' % (filename, 1) - req = InstallRequirement.from_line(line, comes_from=comes_from) + req = install_req_from_line(line, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} @@ -506,7 +506,7 @@ def test_extras_for_line_url_requirement(self): line = 'git+https://url#egg=SomeProject[ex1,ex2]' filename = 'filename' comes_from = '-r %s (line %s)' % (filename, 1) - req = InstallRequirement.from_line(line, comes_from=comes_from) + req = install_req_from_line(line, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} @@ -528,7 +528,7 @@ def test_extras_for_editable_url_requirement(self): def test_unexisting_path(self): with pytest.raises(InstallationError) as e: - InstallRequirement.from_line( + install_req_from_line( os.path.join('this', 'path', 'does', 'not', 'exist')) err_msg = e.value.args[0] assert "Invalid requirement" in err_msg @@ -536,14 +536,14 @@ def test_unexisting_path(self): def test_single_equal_sign(self): with pytest.raises(InstallationError) as e: - InstallRequirement.from_line('toto=42') + install_req_from_line('toto=42') err_msg = e.value.args[0] assert "Invalid requirement" in err_msg assert "= is not a valid operator. Did you mean == ?" in err_msg def test_traceback(self): with pytest.raises(InstallationError) as e: - InstallRequirement.from_line('toto 42') + install_req_from_line('toto 42') err_msg = e.value.args[0] assert "Invalid requirement" in err_msg assert "\nTraceback " in err_msg @@ -553,7 +553,7 @@ def test_requirement_file(self): with open(req_file_path, 'w') as req_file: req_file.write('pip\nsetuptools') with pytest.raises(InstallationError) as e: - InstallRequirement.from_line(req_file_path) + install_req_from_line(req_file_path) err_msg = e.value.args[0] assert "Invalid requirement" in err_msg assert "It looks like a path. It does exist." in err_msg @@ -610,10 +610,10 @@ def test_parse_editable_local_extras( def test_exclusive_environment_markers(): """Make sure RequirementSet accepts several excluding env markers""" - eq26 = InstallRequirement.from_line( + eq26 = install_req_from_line( "Django>=1.6.10,<1.7 ; python_version == '2.6'") eq26.is_direct = True - ne26 = InstallRequirement.from_line( + ne26 = install_req_from_line( "Django>=1.6.10,<1.8 ; python_version != '2.6'") ne26.is_direct = True diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 664196b0367..1d7c48b7f46 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -12,12 +12,13 @@ InstallationError, RequirementsFileParseError, ) from pip._internal.index import PackageFinder -from pip._internal.req.constructors import install_req_from_editable +from pip._internal.req.constructors import ( + install_req_from_editable, install_req_from_line, +) from pip._internal.req.req_file import ( break_args_options, ignore_comments, join_lines, parse_requirements, preprocess, process_line, skip_regex, ) -from pip._internal.req.req_install import InstallRequirement from tests.lib import requirements_file @@ -193,14 +194,14 @@ def test_yield_line_requirement(self): line = 'SomeProject' filename = 'filename' comes_from = '-r %s (line %s)' % (filename, 1) - req = InstallRequirement.from_line(line, comes_from=comes_from) + req = install_req_from_line(line, comes_from=comes_from) assert repr(list(process_line(line, filename, 1))[0]) == repr(req) def test_yield_line_constraint(self): line = 'SomeProject' filename = 'filename' comes_from = '-c %s (line %s)' % (filename, 1) - req = InstallRequirement.from_line( + req = install_req_from_line( line, comes_from=comes_from, constraint=True) found_req = list(process_line(line, filename, 1, constraint=True))[0] assert repr(found_req) == repr(req) @@ -210,7 +211,7 @@ def test_yield_line_requirement_with_spaces_in_specifier(self): line = 'SomeProject >= 2' filename = 'filename' comes_from = '-r %s (line %s)' % (filename, 1) - req = InstallRequirement.from_line(line, comes_from=comes_from) + req = install_req_from_line(line, comes_from=comes_from) assert repr(list(process_line(line, filename, 1))[0]) == repr(req) assert str(req.req.specifier) == '>=2' @@ -235,7 +236,7 @@ def test_yield_editable_constraint(self): def test_nested_requirements_file(self, monkeypatch): line = '-r another_file' - req = InstallRequirement.from_line('SomeProject') + req = install_req_from_line('SomeProject') import pip._internal.req.req_file def stub_parse_requirements(req_url, finder, comes_from, options, @@ -248,7 +249,7 @@ def stub_parse_requirements(req_url, finder, comes_from, options, def test_nested_constraints_file(self, monkeypatch): line = '-c another_file' - req = InstallRequirement.from_line('SomeProject') + req = install_req_from_line('SomeProject') import pip._internal.req.req_file def stub_parse_requirements(req_url, finder, comes_from, options, diff --git a/tests/unit/test_req_install.py b/tests/unit/test_req_install.py index 17af7d5f526..f4a6e94a183 100644 --- a/tests/unit/test_req_install.py +++ b/tests/unit/test_req_install.py @@ -3,6 +3,7 @@ import pytest +from pip._internal.req.constructors import install_req_from_line from pip._internal.req.req_install import InstallRequirement @@ -36,7 +37,7 @@ def test_forward_slash_results_in_a_link(self, tmpdir): with open(setup_py_path, 'w') as f: f.write('') - requirement = InstallRequirement.from_line( + requirement = install_req_from_line( str(install_dir).replace(os.sep, os.altsep or os.sep) ) From 688bc1ee6fb395e573baf98be3eca7a5ceb27d42 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 21 Aug 2018 20:41:36 +0530 Subject: [PATCH 3/3] Move InstallRequirement.from_req to constructors module --- src/pip/_internal/req/constructors.py | 26 ++++++++++++++++++++++++++ src/pip/_internal/req/req_install.py | 26 +------------------------- src/pip/_internal/resolve.py | 4 ++-- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index b0e641bd7e6..4c4641dc883 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -22,6 +22,7 @@ is_archive_file, is_url, path_to_url, url_to_path, ) from pip._internal.exceptions import InstallationError +from pip._internal.models.index import PyPI, TestPyPI from pip._internal.models.link import Link from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.misc import is_installable_dir @@ -270,3 +271,28 @@ def install_req_from_line( constraint=constraint, extras=extras, ) + + +def install_req_from_req( + req, comes_from=None, isolated=False, wheel_cache=None +): + try: + req = Requirement(req) + except InvalidRequirement: + raise InstallationError("Invalid requirement: '%s'" % req) + + domains_not_allowed = [ + PyPI.file_storage_domain, + TestPyPI.file_storage_domain, + ] + if req.url 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" + "%s depends on %s " % (comes_from.name, req) + ) + + return InstallRequirement( + req, comes_from, isolated=isolated, wheel_cache=wheel_cache + ) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index c1989a50ae0..c2624feed7c 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -9,7 +9,7 @@ from distutils.util import change_root from pip._vendor import pkg_resources, six -from pip._vendor.packaging.requirements import InvalidRequirement, Requirement +from pip._vendor.packaging.requirements import Requirement from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import Version from pip._vendor.packaging.version import parse as parse_version @@ -21,7 +21,6 @@ from pip._internal.locations import ( PIP_DELETE_MARKER_FILENAME, running_under_virtualenv, ) -from pip._internal.models.index import PyPI, TestPyPI from pip._internal.models.link import Link from pip._internal.pyproject import load_pyproject_toml from pip._internal.req.req_uninstall import UninstallPathSet @@ -125,29 +124,6 @@ def __init__(self, req, comes_from, source_dir=None, editable=False, # but after loading this flag should be treated as read only. self.use_pep517 = None - # Constructors - # TODO: Move these out of this class into custom methods. - @classmethod - def from_req(cls, req, comes_from=None, isolated=False, wheel_cache=None): - try: - req = Requirement(req) - except InvalidRequirement: - raise InstallationError("Invalid requirement: '%s'" % req) - - domains_not_allowed = [ - PyPI.file_storage_domain, - TestPyPI.file_storage_domain, - ] - if req.url 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" - "%s depends on %s " % (comes_from.name, req) - ) - - return cls(req, comes_from, isolated=isolated, wheel_cache=wheel_cache) - def __str__(self): if self.req: s = str(self.req) diff --git a/src/pip/_internal/resolve.py b/src/pip/_internal/resolve.py index 8480e48c6f3..2d9f1c56c81 100644 --- a/src/pip/_internal/resolve.py +++ b/src/pip/_internal/resolve.py @@ -18,7 +18,7 @@ BestVersionAlreadyInstalled, DistributionNotFound, HashError, HashErrors, UnsupportedPythonVersion, ) -from pip._internal.req.req_install import InstallRequirement +from pip._internal.req.constructors import install_req_from_req from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import dist_in_usersite, ensure_dir from pip._internal.utils.packaging import check_dist_requires_python @@ -268,7 +268,7 @@ def _resolve_one(self, requirement_set, req_to_install): more_reqs = [] def add_req(subreq, extras_requested): - sub_install_req = InstallRequirement.from_req( + sub_install_req = install_req_from_req( str(subreq), req_to_install, isolated=self.isolated,