/
niceSOFT
/
pip
Обзор
Документация
Войти
/
niceSOFT
/
pip
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
tests/unit/test_req.py
1 123 строки
44 KB
Stéphane Bidoul
refactor: Replace per req permit_editable_wheels by a preparer and builder flag (#14206)
27 июл 2026, 06:26
Не верифицирован
27 июл 2026, 06:26
6294f20
Код
Авторство
О чём код?
from __future__ import annotations import contextlib import email.message import os import shutil import sys import tempfile from collections.abc import Iterator from functools import partial from pathlib import Path from typing import cast from unittest import mock import pytest from pip._vendor.packaging.markers import Marker from pip._vendor.packaging.requirements import Requirement from pip._internal.build_env import ( BuildIsolationMode, SubprocessBuildEnvironmentInstaller, ) from pip._internal.cache import WheelCache from pip._internal.commands import create_command from pip._internal.commands.install import InstallCommand from pip._internal.exceptions import ( CommandError, HashErrors, InstallationError, InvalidWheelFilename, PreviousBuildDirError, ) from pip._internal.index.package_finder import PackageFinder from pip._internal.models.direct_url import ArchiveInfo, DirectUrl from pip._internal.models.link import Link from pip._internal.network.session import PipSession from pip._internal.operations.build.build_tracker import get_build_tracker from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req import InstallRequirement, RequirementSet from pip._internal.req.constructors import ( _get_url_from_path, _looks_like_path, install_req_drop_extras, install_req_extend_extras, install_req_from_editable, install_req_from_line, install_req_from_parsed_requirement, install_req_from_req_string, parse_editable, ) from pip._internal.req.req_file import ( ParsedLine, get_line_parser, handle_requirement_line, ) from pip._internal.resolution.legacy.resolver import Resolver from pip._internal.utils.urls import path_to_url from tests.lib import TestData, make_test_finder, requirements_file, wheel def get_processed_req_from_line( line: str, fname: str = "file", lineno: int = 1 ) -> InstallRequirement: line_parser = get_line_parser(None) args_str, opts = line_parser(line) parsed_line = ParsedLine( fname, lineno, args_str, opts, False, ) parsed_req = handle_requirement_line(parsed_line) assert parsed_req is not None req = install_req_from_parsed_requirement(parsed_req) req.user_supplied = True return req class TestRequirementSet: """RequirementSet tests""" def setup_method(self) -> None: self.tempdir = tempfile.mkdtemp() def teardown_method(self) -> None: shutil.rmtree(self.tempdir, ignore_errors=True) @contextlib.contextmanager def _basic_resolver( self, finder: PackageFinder, require_hashes: bool = False, wheel_cache: WheelCache | None = None, build_isolation: BuildIsolationMode = "virtual", ) -> Iterator[Resolver]: make_install_req = partial( install_req_from_req_string, isolated=False, ) session = PipSession() with get_build_tracker() as tracker: installer = SubprocessBuildEnvironmentInstaller(finder) preparer = RequirementPreparer( build_dir=os.path.join(self.tempdir, "build"), src_dir=os.path.join(self.tempdir, "src"), download_dir=None, build_isolation=build_isolation, build_isolation_installer=installer, check_build_deps=False, build_tracker=tracker, session=session, progress_bar="on", finder=finder, require_hashes=require_hashes, use_user_site=False, lazy_wheel=False, verbosity=0, legacy_resolver=True, allow_editables=True, ) yield Resolver( preparer=preparer, make_install_req=make_install_req, finder=finder, wheel_cache=wheel_cache, use_user_site=False, upgrade_strategy="to-satisfy-only", ignore_dependencies=False, only_dependencies=False, ignore_installed=False, ignore_requires_python=False, force_reinstall=False, ) def test_no_reuse_existing_build_dir(self, data: TestData) -> None: """Test prepare_files raise exception with previous build dir""" build_dir = os.path.join(self.tempdir, "build", "simple") os.makedirs(build_dir) with open(os.path.join(build_dir, "setup.py"), "w"): pass reqset = RequirementSet() req = install_req_from_line("simple") req.user_supplied = True reqset.add_named_requirement(req) finder = make_test_finder(find_links=[data.find_links]) with self._basic_resolver(finder) as resolver: with pytest.raises( PreviousBuildDirError, match=( r"pip can't proceed with [\s\S]*{req}[\s\S]*{build_dir_esc}".format( build_dir_esc=build_dir.replace("\\", "\\\\"), req=req ) ), ): resolver.resolve(reqset.all_requirements, True) def test_environment_marker_extras(self, data: TestData) -> None: """ Test that the environment marker extras are used with non-wheel installs. """ reqset = RequirementSet() req = install_req_from_editable( os.fspath(data.packages.joinpath("LocalEnvironMarker")), ) req.user_supplied = True reqset.add_unnamed_requirement(req) finder = make_test_finder(find_links=[data.find_links]) with self._basic_resolver(finder, build_isolation="off") as resolver: reqset = resolver.resolve(reqset.all_requirements, True) assert not reqset.has_requirement("simple") def test_missing_hash_with_require_hashes(self, data: TestData) -> None: """Setting --require-hashes explicitly should raise errors if hashes are missing. """ reqset = RequirementSet() reqset.add_named_requirement( get_processed_req_from_line("simple==1.0", lineno=1) ) finder = make_test_finder(find_links=[data.find_links]) with self._basic_resolver(finder, require_hashes=True) as resolver: with pytest.raises( HashErrors, match=( r"Hashes are required in --require-hashes mode, but they are " r"missing .*\n" r" simple==1.0 --hash=sha256:393043e672415891885c9a2a0929b1" r"af95fb866d6ca016b42d2e6ce53619b653$" ), ): resolver.resolve(reqset.all_requirements, True) def test_missing_hash_with_require_hashes_in_reqs_file( self, data: TestData, tmpdir: Path ) -> None: """--require-hashes in a requirements file should make its way to the RequirementSet. """ finder = make_test_finder(find_links=[data.find_links]) session = finder._link_collector.session command = cast(InstallCommand, create_command("install")) with requirements_file("--require-hashes", tmpdir) as reqs_file: options, args = command.parse_args(["-r", os.fspath(reqs_file)]) command.get_requirements(args, options, finder, session) assert options.require_hashes def test_auto_require_hashes(self, data: TestData, tmpdir: Path) -> None: """A requirement with hashes auto-enables --require-hashes""" finder = make_test_finder(find_links=[data.find_links]) session = finder._link_collector.session command = cast(InstallCommand, create_command("install")) reqs = ( r"simple==1.0 --hash=sha256:393043e672415891885c9a2a0929b1" r"af95fb866d6ca016b42d2e6ce53619b653$" ) with requirements_file(reqs, tmpdir) as reqs_file: options, args = command.parse_args(["-r", os.fspath(reqs_file)]) command.get_requirements(args, options, finder, session) assert options.require_hashes def test_no_auto_require_hashes(self, data: TestData, tmpdir: Path) -> None: """A requirement with hashes does not auto-enable --require-hashes when --no-require-hashes is set""" finder = make_test_finder(find_links=[data.find_links]) session = finder._link_collector.session command = cast(InstallCommand, create_command("install")) reqs = ( r"simple==1.0 --hash=sha256:393043e672415891885c9a2a0929b1" r"af95fb866d6ca016b42d2e6ce53619b653$" ) with requirements_file("--no-require-hashes\n" + reqs, tmpdir) as reqs_file: options, args = command.parse_args(["-r", os.fspath(reqs_file)]) command.get_requirements(args, options, finder, session) assert not options.require_hashes def test_require_hashes_no_require_hashes_in_reqs_file( self, data: TestData, tmpdir: Path ) -> None: finder = make_test_finder(find_links=[data.find_links]) session = finder._link_collector.session command = cast(InstallCommand, create_command("install")) reqs = ( r"simple==1.0 --hash=sha256:393043e672415891885c9a2a0929b1" r"af95fb866d6ca016b42d2e6ce53619b653$" ) with requirements_file( "--require-hashes\n--no-require-hashes\n" + reqs, tmpdir ) as reqs_file: options, args = command.parse_args(["-r", os.fspath(reqs_file)]) with pytest.raises( CommandError, match="--require-hashes and --no-require-hashes are mutually exclusive", ): command.get_requirements(args, options, finder, session) def test_unsupported_hashes(self, data: TestData) -> None: """VCS and dir links should raise errors when --require-hashes is on. In addition, complaints about the type of requirement (VCS or dir) should trump the presence or absence of a hash. """ reqset = RequirementSet() reqset.add_unnamed_requirement( get_processed_req_from_line( "git+git://github.com/pypa/pip-test-package --hash=sha256:123", lineno=1, ) ) dir_path = data.packages.joinpath("FSPkg") reqset.add_unnamed_requirement( get_processed_req_from_line( path_to_url(str(dir_path)), lineno=2, ) ) finder = make_test_finder(find_links=[data.find_links]) with self._basic_resolver(finder, require_hashes=True) as resolver: with pytest.raises( HashErrors, match=( r"Can't verify hashes for these requirements because we don't " r"have a way to hash version control repositories:\n" r" git\+git://github\.com/pypa/pip-test-package \(from -r " r"file \(line 1\)\)\n" r"Can't verify hashes for these file:// requirements because " r"they point to directories:\n" r" file://.*/data/packages/FSPkg " r"\(from -r file \(line 2\)\)" ), ): resolver.resolve(reqset.all_requirements, True) def test_unpinned_hash_checking(self, data: TestData) -> None: """Make sure prepare_files() raises an error when a requirement is not version-pinned in hash-checking mode. """ reqset = RequirementSet() # Test that there must be exactly 1 specifier: reqset.add_named_requirement( get_processed_req_from_line( "simple --hash=sha256:a90427ae31f5d1d0d7ec06ee97d9fcf2d0fc9a786985" "250c1c83fd68df5911dd", lineno=1, ) ) # Test that the operator must be ==: reqset.add_named_requirement( get_processed_req_from_line( "simple2>1.0 --hash=sha256:3ad45e1e9aa48b4462af0" "123f6a7e44a9115db1ef945d4d92c123dfe21815a06", lineno=2, ) ) finder = make_test_finder(find_links=[data.find_links]) with self._basic_resolver(finder, require_hashes=True) as resolver: with pytest.raises( HashErrors, # Make sure all failing requirements are listed: match=( r"versions pinned with ==. These do not:\n" r" simple .* \(from -r file \(line 1\)\)\n" r" simple2>1.0 .* \(from -r file \(line 2\)\)" ), ): resolver.resolve(reqset.all_requirements, True) def test_hash_mismatch(self, data: TestData) -> None: """A hash mismatch should raise an error.""" file_url = data.packages.joinpath("simple-1.0.tar.gz").resolve().as_uri() reqset = RequirementSet() reqset.add_unnamed_requirement( get_processed_req_from_line( f"{file_url} --hash=sha256:badbad", lineno=1, ) ) finder = make_test_finder(find_links=[data.find_links]) with self._basic_resolver(finder, require_hashes=True) as resolver: with pytest.raises( HashErrors, match=( r"THESE PACKAGES DO NOT MATCH THE HASHES.*\n" r" file:///.*/data/packages/simple-1\.0\.tar\.gz .*:\n" r" Expected sha256 badbad\n" r" Got 393043e672415891885c9a2a0929b1af95fb" r"866d6ca016b42d2e6ce53619b653$" ), ): resolver.resolve(reqset.all_requirements, True) def test_unhashed_deps_on_require_hashes(self, data: TestData) -> None: """Make sure unhashed, unpinned, or otherwise unrepeatable dependencies get complained about when --require-hashes is on.""" reqset = RequirementSet() finder = make_test_finder(find_links=[data.find_links]) reqset.add_named_requirement( get_processed_req_from_line( "TopoRequires2==0.0.1 " # requires TopoRequires "--hash=sha256:eaf9a01242c9f2f42cf2bd82a6a848cd" "e3591d14f7896bdbefcf48543720c970", lineno=1, ) ) with self._basic_resolver( finder, require_hashes=True, build_isolation="off" ) as resolver: with pytest.raises( HashErrors, match=( r"In --require-hashes mode, all requirements must have their " r"versions pinned.*\n" r" TopoRequires from .*$" ), ): resolver.resolve(reqset.all_requirements, True) def test_hashed_deps_on_require_hashes(self) -> None: """Make sure hashed dependencies get installed when --require-hashes is on. (We actually just check that no "not all dependencies are hashed!" error gets raised while preparing; there is no reason to expect installation to then fail, as the code paths are the same as ever.) """ reqset = RequirementSet() reqset.add_named_requirement( get_processed_req_from_line( "TopoRequires2==0.0.1 " # requires TopoRequires "--hash=sha256:eaf9a01242c9f2f42cf2bd82a6a848cd" "e3591d14f7896bdbefcf48543720c970", lineno=1, ) ) reqset.add_named_requirement( get_processed_req_from_line( "TopoRequires==0.0.1 " "--hash=sha256:d6dd1e22e60df512fdcf3640ced3039b3b02a56ab2cee81ebcb" "3d0a6d4e8bfa6", lineno=2, ) ) def test_download_info_find_links(self, data: TestData) -> None: """Test that download_info is set for requirements via find_links.""" finder = make_test_finder(find_links=[data.find_links]) with self._basic_resolver(finder, build_isolation="off") as resolver: ireq = get_processed_req_from_line("simple") reqset = resolver.resolve([ireq], True) assert len(reqset.all_requirements) == 1 req = reqset.all_requirements[0] assert req.download_info assert req.download_info.archive_info assert req.download_info.archive_info.hashes @pytest.mark.network def test_download_info_index_url(self) -> None: """Test that download_info is set for requirements via index.""" finder = make_test_finder(index_urls=["https://pypi.org/simple"]) with self._basic_resolver(finder) as resolver: ireq = get_processed_req_from_line("initools") reqset = resolver.resolve([ireq], True) assert len(reqset.all_requirements) == 1 req = reqset.all_requirements[0] assert req.download_info assert req.download_info.archive_info @pytest.mark.network def test_download_info_web_archive(self) -> None: """Test that download_info is set for requirements from a web archive.""" finder = make_test_finder() with self._basic_resolver(finder, build_isolation="off") as resolver: ireq = get_processed_req_from_line( "pip-test-package @ " "https://github.com/pypa/pip-test-package/tarball/0.1.1" ) reqset = resolver.resolve([ireq], True) assert len(reqset.all_requirements) == 1 req = reqset.all_requirements[0] assert req.download_info assert ( req.download_info.url == "https://github.com/pypa/pip-test-package/tarball/0.1.1" ) assert req.download_info.archive_info assert req.download_info.archive_info.hashes == { "sha256": ( "ad977496000576e1b6c41f6449a9897087ce9da6db4f15b603fe8372af4bf3c6" ) } def test_download_info_archive_legacy_cache( self, tmp_path: Path, shared_data: TestData ) -> None: """Test download_info hash is not set for an archive with legacy cache entry.""" url = shared_data.packages.joinpath("simple-1.0.tar.gz").as_uri() finder = make_test_finder() wheel_cache = WheelCache(str(tmp_path / "cache")) cache_entry_dir = wheel_cache.get_path_for_link(Link(url)) Path(cache_entry_dir).mkdir(parents=True) wheel.make_wheel(name="simple", version="1.0").save_to_dir(cache_entry_dir) with self._basic_resolver(finder, wheel_cache=wheel_cache) as resolver: ireq = get_processed_req_from_line(f"simple @ {url}") reqset = resolver.resolve([ireq], True) assert len(reqset.all_requirements) == 1 req = reqset.all_requirements[0] assert req.is_wheel_from_cache assert req.cached_wheel_source_link assert req.download_info assert req.download_info.url == url assert req.download_info.archive_info assert not req.download_info.archive_info.hashes def test_download_info_archive_cache_with_origin( self, tmp_path: Path, shared_data: TestData ) -> None: """Test download_info hash is set for a web archive with cache entry that has origin.json.""" url = shared_data.packages.joinpath("simple-1.0.tar.gz").as_uri() hash = "ad977496000576e1b6c41f6449a9897087ce9da6db4f15b603fe8372af4bf3c6" finder = make_test_finder() wheel_cache = WheelCache(str(tmp_path / "cache")) cache_entry_dir = wheel_cache.get_path_for_link(Link(url)) Path(cache_entry_dir).mkdir(parents=True) Path(cache_entry_dir).joinpath("origin.json").write_text( DirectUrl( url=url, archive_info=ArchiveInfo(hashes={"sha256": hash}) ).to_json() ) wheel.make_wheel(name="simple", version="1.0").save_to_dir(cache_entry_dir) with self._basic_resolver(finder, wheel_cache=wheel_cache) as resolver: ireq = get_processed_req_from_line(f"simple @ {url}") reqset = resolver.resolve([ireq], True) assert len(reqset.all_requirements) == 1 req = reqset.all_requirements[0] assert req.is_wheel_from_cache assert req.cached_wheel_source_link assert req.download_info assert req.download_info.url == url assert req.download_info.archive_info assert req.download_info.archive_info.hashes == {"sha256": hash} def test_download_info_archive_cache_with_invalid_origin( self, tmp_path: Path, shared_data: TestData, caplog: pytest.LogCaptureFixture ) -> None: """Test an invalid origin.json is ignored.""" url = shared_data.packages.joinpath("simple-1.0.tar.gz").as_uri() finder = make_test_finder() wheel_cache = WheelCache(str(tmp_path / "cache")) cache_entry_dir = wheel_cache.get_path_for_link(Link(url)) Path(cache_entry_dir).mkdir(parents=True) Path(cache_entry_dir).joinpath("origin.json").write_text("{") # invalid json wheel.make_wheel(name="simple", version="1.0").save_to_dir(cache_entry_dir) with self._basic_resolver(finder, wheel_cache=wheel_cache) as resolver: ireq = get_processed_req_from_line(f"simple @ {url}") reqset = resolver.resolve([ireq], True) assert len(reqset.all_requirements) == 1 req = reqset.all_requirements[0] assert req.is_wheel_from_cache assert any( "Ignoring invalid cache entry origin file" in x for x in caplog.messages ) def test_download_info_local_wheel(self, data: TestData) -> None: """Test that download_info is set for requirements from a local wheel.""" finder = make_test_finder() with self._basic_resolver(finder) as resolver: ireq = get_processed_req_from_line( f"{data.packages}/simplewheel-1.0-py2.py3-none-any.whl" ) reqset = resolver.resolve([ireq], True) assert len(reqset.all_requirements) == 1 req = reqset.all_requirements[0] assert req.download_info assert req.download_info.url.startswith("file://") assert req.download_info.archive_info assert req.download_info.archive_info.hashes == { "sha256": ( "e63aa139caee941ec7f33f057a5b987708c2128238357cf905429846a2008718" ) } def test_download_info_local_dir(self, data: TestData) -> None: """Test that download_info is set for requirements from a local dir.""" finder = make_test_finder() with self._basic_resolver(finder, build_isolation="off") as resolver: ireq_url = data.packages.joinpath("FSPkg").as_uri() ireq = get_processed_req_from_line(f"FSPkg @ {ireq_url}") reqset = resolver.resolve([ireq], True) assert len(reqset.all_requirements) == 1 req = reqset.all_requirements[0] assert req.download_info assert req.download_info.url.startswith("file://") assert req.download_info.dir_info def test_download_info_local_editable_dir(self, data: TestData) -> None: """Test that download_info is set for requirements from a local editable dir.""" finder = make_test_finder() with self._basic_resolver(finder, build_isolation="off") as resolver: ireq_url = data.packages.joinpath("FSPkg").as_uri() ireq = get_processed_req_from_line(f"-e {ireq_url}#egg=FSPkg") reqset = resolver.resolve([ireq], True) assert len(reqset.all_requirements) == 1 req = reqset.all_requirements[0] assert req.download_info assert req.download_info.url.startswith("file://") assert req.download_info.dir_info assert req.download_info.dir_info.editable @pytest.mark.network def test_download_info_vcs(self) -> None: """Test that download_info is set for requirements from git.""" finder = make_test_finder() with self._basic_resolver(finder, build_isolation="off") as resolver: ireq = get_processed_req_from_line( "pip-test-package @ git+https://github.com/pypa/pip-test-package" ) reqset = resolver.resolve([ireq], True) assert len(reqset.all_requirements) == 1 req = reqset.all_requirements[0] assert req.download_info assert req.download_info.vcs_info assert req.download_info.url == "https://github.com/pypa/pip-test-package" assert req.download_info.vcs_info.vcs == "git" class TestInstallRequirement: def setup_method(self) -> None: self.tempdir = tempfile.mkdtemp() def teardown_method(self) -> None: shutil.rmtree(self.tempdir, ignore_errors=True) def test_url_with_query(self) -> None: """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 = install_req_from_line(url + fragment) assert req.link is not None assert req.link.url == url + fragment, req.link def test_pep440_wheel_link_requirement(self) -> None: url = "https://whatever.com/test-0.4-py2.py3-bogus-any.whl" line = "test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl" req = install_req_from_line(line) parts = str(req.req).split("@", 1) assert len(parts) == 2 assert parts[0].strip() == "test" assert parts[1].strip() == url def test_pep440_url_link_requirement(self) -> None: url = "git+http://foo.com@ref#egg=foo" line = "foo @ git+http://foo.com@ref#egg=foo" req = install_req_from_line(line) parts = str(req.req).split("@", 1) assert len(parts) == 2 assert parts[0].strip() == "foo" assert parts[1].strip() == url def test_url_with_authentication_link_requirement(self) -> None: url = "https://what@whatever.com/test-0.4-py2.py3-bogus-any.whl" line = "https://what@whatever.com/test-0.4-py2.py3-bogus-any.whl" req = install_req_from_line(line) assert req.link is not None assert req.link.is_wheel assert req.link.scheme == "https" assert req.link.url == url def test_str(self) -> None: req = install_req_from_line("simple==0.1") assert str(req) == "simple==0.1" def test_repr(self) -> None: req = install_req_from_line("simple==0.1") assert repr(req) == ("<InstallRequirement object: simple==0.1 editable=False>") def test_invalid_wheel_requirement_raises(self) -> None: with pytest.raises(InvalidWheelFilename): install_req_from_line("invalid.whl") def test_wheel_requirement_sets_req_attribute(self) -> None: 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) -> None: """Confirm the url is preserved in a non-editable requirement""" url = "git+http://foo.com@ref#egg=foo" req = install_req_from_line(url) assert req.link is not None assert req.link.url == url def test_url_preserved_editable_req(self) -> None: """Confirm the url is preserved in a editable requirement""" url = "git+http://foo.com@ref#egg=foo" req = install_req_from_editable(url) assert req.link is not None assert req.link.url == url def test_markers(self) -> None: for line in ( # recommended syntax 'mock3; python_version >= "3"', # with more spaces 'mock3 ; python_version >= "3" ', # without spaces 'mock3;python_version >= "3"', ): req = install_req_from_line(line) assert req.req is not None assert req.req.name == "mock3" assert str(req.req.specifier) == "" assert str(req.markers) == 'python_version >= "3"' def test_markers_semicolon(self) -> None: # check that the markers can contain a semicolon req = install_req_from_line('semicolon; os_name == "a; b"') assert req.req is not None assert req.req.name == "semicolon" assert str(req.req.specifier) == "" assert str(req.markers) == 'os_name == "a; b"' def test_markers_invalid(self) -> None: with pytest.raises(InstallationError) as e: install_req_from_line('name; python_version == "1"; python_version == "2"') assert "Invalid requirement" in e.value.args[0] def test_markers_url(self) -> None: # test "URL; markers" syntax url = "http://foo.com/?p=bar.git;a=snapshot;h=v0.1;sf=tgz" line = f'{url}; python_version >= "3"' req = install_req_from_line(line) assert req.link is not None assert req.link.url == url, req.link.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 = f'{url};python_version >= "3"' req = install_req_from_line(line) assert req.link is not None assert req.link.url == line, req.link.url assert req.markers is None def test_markers_match_from_line(self) -> None: # match for markers in ( 'python_version >= "1.0"', f"sys_platform == {sys.platform!r}", ): line = "name; " + markers req = install_req_from_line(line) assert str(req.markers) == str(Marker(markers)) assert req.match_markers() # don't match for markers in ( 'python_version >= "5.0"', f"sys_platform != {sys.platform!r}", ): line = "name; " + markers req = install_req_from_line(line) assert str(req.markers) == str(Marker(markers)) assert not req.match_markers() def test_markers_match(self) -> None: # match for markers in ( 'python_version >= "1.0"', f"sys_platform == {sys.platform!r}", ): line = "name; " + markers req = install_req_from_line(line, comes_from="") assert str(req.markers) == str(Marker(markers)) assert req.match_markers() # don't match for markers in ( 'python_version >= "5.0"', f"sys_platform != {sys.platform!r}", ): line = "name; " + markers req = install_req_from_line(line, comes_from="") assert str(req.markers) == str(Marker(markers)) assert not req.match_markers() def test_extras_for_line_path_requirement(self) -> None: line = "SomeProject[ex1,ex2]" filename = "filename" comes_from = f"-r {filename} (line 1)" req = install_req_from_line(line, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {"ex1", "ex2"} def test_extras_for_line_url_requirement(self) -> None: line = "SomeProject[ex1,ex2] @ git+https://url" filename = "filename" comes_from = f"-r {filename} (line 1)" req = install_req_from_line(line, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {"ex1", "ex2"} def test_extras_for_editable_path_requirement(self) -> None: url = ".[ex1,ex2]" filename = "filename" comes_from = f"-r {filename} (line 1)" req = install_req_from_editable(url, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {"ex1", "ex2"} def test_extras_for_editable_url_requirement(self) -> None: url = "SomeProject[ex1,ex2] @ git+https://url" filename = "filename" comes_from = f"-r {filename} (line 1)" req = install_req_from_editable(url, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {"ex1", "ex2"} def test_unexisting_path(self) -> None: with pytest.raises(InstallationError) as e: install_req_from_line(os.path.join("this", "path", "does", "not", "exist")) err_msg = e.value.args[0] assert "Invalid requirement" in err_msg assert "It looks like a path." in err_msg def test_single_equal_sign(self) -> None: with pytest.raises(InstallationError) as e: 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_unidentifiable_name(self) -> None: test_name = "-" with pytest.raises(InstallationError) as e: install_req_from_line(test_name) err_msg = e.value.args[0] assert err_msg.startswith(f"Invalid requirement: '{test_name}'") def test_requirement_file(self) -> None: req_file_path = os.path.join(self.tempdir, "test.txt") with open(req_file_path, "w") as req_file: req_file.write("pip\nsetuptools") with pytest.raises(InstallationError) as e: 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. The path does exist." in err_msg assert "appears to be a requirements file." in err_msg assert "If that is the case, use the '-r' flag to install" in err_msg @pytest.mark.parametrize( "inp, out", [ ("pkg", "pkg"), ("pkg==1.0", "pkg==1.0"), ("pkg ; python_version<='3.6'", "pkg"), ("pkg[ext]", "pkg"), ("pkg [ ext1, ext2 ]", "pkg"), ("pkg [ ext1, ext2 ] @ https://example.com/", "pkg @ https://example.com/"), ("pkg [ext] == 1.0; python_version<='3.6'", "pkg==1.0"), ("pkg-all.allowed_chars0 ~= 2.0", "pkg-all.allowed_chars0~=2.0"), ("pkg-all.allowed_chars0 [ext] ~= 2.0", "pkg-all.allowed_chars0~=2.0"), ("simple-0.1-py2.py3-none-any.whl [ext]", "simple==0.1"), ], ) def test_install_req_drop_extras(self, inp: str, out: str) -> None: """ Test behavior of install_req_drop_extras """ req = install_req_from_line(inp) without_extras = install_req_drop_extras(req) assert not without_extras.extras assert str(without_extras.req) == out # if there are no extras they should be the same object, # otherwise they may be a copy due to cache if req.extras: assert req is not without_extras assert req.req is not without_extras.req # comes_from should point to original assert without_extras.comes_from is req # all else should be the same assert without_extras.link == req.link assert without_extras.markers == req.markers assert without_extras.isolated == req.isolated assert without_extras.hash_options == req.hash_options assert without_extras.constraint == req.constraint assert without_extras.config_settings == req.config_settings assert without_extras.user_supplied == req.user_supplied @pytest.mark.parametrize( "inp, extras, out", [ ("pkg", set(), "pkg"), ("pkg==1.0", set(), "pkg==1.0"), ("pkg[ext]", set(), "pkg[ext]"), ("pkg", {"ext"}, "pkg[ext]"), ("pkg==1.0", {"ext"}, "pkg[ext]==1.0"), ("pkg==1.0", {"ext1", "ext2"}, "pkg[ext1,ext2]==1.0"), ("pkg; python_version<='3.6'", {"ext"}, "pkg[ext]"), ("pkg[ext1,ext2]==1.0", {"ext2", "ext3"}, "pkg[ext1,ext2,ext3]==1.0"), ( "pkg-all.allowed_chars0 [ ext1 ] @ https://example.com/", {"ext2"}, "pkg-all.allowed_chars0[ext1,ext2] @ https://example.com/", ), ], ) def test_install_req_extend_extras( self, inp: str, extras: set[str], out: str ) -> None: """ Test behavior of install_req_extend_extras """ req = install_req_from_line(inp) extended = install_req_extend_extras(req, extras) assert str(extended.req) == out assert extended.req is not None assert set(extended.extras) == set(extended.req.extras) # if extras is not a subset of req.extras then the extended # requirement object should not be the same, otherwise they # might be a copy due to cache if not extras.issubset(req.extras): assert req is not extended assert req.req is not extended.req # all else should be the same assert extended.link == req.link assert extended.markers == req.markers assert extended.isolated == req.isolated assert extended.hash_options == req.hash_options assert extended.constraint == req.constraint assert extended.config_settings == req.config_settings assert extended.user_supplied == req.user_supplied @pytest.mark.parametrize( "req_str, expected", [ ( 'foo[extra] @ svn+http://foo ; os_name == "nt"', ('foo ; os_name == "nt"', "svn+http://foo", {"extra"}), ), ( "foo @ svn+http://foo", ("foo", "svn+http://foo", set()), ), ], ) def test_parse_editable_pep508( req_str: str, expected: tuple[str, str, set[str]] ) -> None: assert parse_editable(req_str) == expected @mock.patch("pip._internal.req.req_install.os.path.abspath") @mock.patch("pip._internal.req.req_install.os.path.exists") @mock.patch("pip._internal.req.req_install.os.path.isdir") def test_parse_editable_local( isdir_mock: mock.Mock, exists_mock: mock.Mock, abspath_mock: mock.Mock ) -> None: exists_mock.return_value = isdir_mock.return_value = True # mocks needed to support path operations on windows tests abspath_mock.return_value = "/some/path" assert parse_editable(".") == (None, "file:///some/path", set()) abspath_mock.return_value = "/some/path/foo" assert parse_editable("foo") == ( None, "file:///some/path/foo", set(), ) def test_parse_editable_explicit_vcs() -> None: assert parse_editable("svn+https://foo#egg=foo") == ( "foo", "svn+https://foo#egg=foo", set(), ) def test_parse_editable_vcs_extras() -> None: assert parse_editable("foo[extras] @ svn+https://foo") == ( "foo", "svn+https://foo", {"extras"}, ) @mock.patch("pip._internal.req.req_install.os.path.abspath") @mock.patch("pip._internal.req.req_install.os.path.exists") @mock.patch("pip._internal.req.req_install.os.path.isdir") def test_parse_editable_local_extras( isdir_mock: mock.Mock, exists_mock: mock.Mock, abspath_mock: mock.Mock ) -> None: exists_mock.return_value = isdir_mock.return_value = True abspath_mock.return_value = "/some/path" assert parse_editable(".[extras]") == ( None, "file:///some/path", {"extras"}, ) abspath_mock.return_value = "/some/path/foo" assert parse_editable("foo[bar,baz]") == ( None, "file:///some/path/foo", {"bar", "baz"}, ) def test_mismatched_versions(caplog: pytest.LogCaptureFixture) -> None: req = InstallRequirement( req=Requirement("simplewheel==2.0"), comes_from=None, ) req.source_dir = "/tmp/somewhere" # make req believe it has been unpacked # Monkeypatch! metadata = email.message.Message() metadata["name"] = "simplewheel" metadata["version"] = "1.0" req._metadata = metadata req.assert_source_matches_version() assert caplog.records[-1].message == ( "Requested simplewheel==2.0, but installing version 1.0" ) @pytest.mark.parametrize( "args, expected", [ # Test UNIX-like paths (("/path/to/installable"), True), # Test relative paths (("./path/to/installable"), True), # Test current path (("."), True), # Test url paths (("https://whatever.com/test-0.4-py2.py3-bogus-any.whl"), True), # Test pep440 paths (("test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl"), True), # Test wheel (("simple-0.1-py2.py3-none-any.whl"), False), ], ) def test_looks_like_path(args: str, expected: bool) -> None: assert _looks_like_path(args) == expected @pytest.mark.skipif( not sys.platform.startswith("win"), reason="Test only available on Windows" ) @pytest.mark.parametrize( "args, expected", [ # Test relative paths ((".\\path\\to\\installable"), True), (("relative\\path"), True), # Test absolute paths (("C:\\absolute\\path"), True), ], ) def test_looks_like_path_win(args: str, expected: bool) -> None: assert _looks_like_path(args) == expected @pytest.mark.parametrize( "args, mock_returns, expected", [ # Test pep440 urls ( ( "/path/to/foo @ git+http://foo.com@ref#egg=foo", "foo @ git+http://foo.com@ref#egg=foo", ), (False, False), None, ), # Test pep440 urls without spaces ( ( "/path/to/foo@git+http://foo.com@ref#egg=foo", "foo @ git+http://foo.com@ref#egg=foo", ), (False, False), None, ), # Test pep440 wheel ( ( "/path/to/test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl", "test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl", ), (False, False), None, ), # Test name is not a file (("/path/to/simple==0.1", "simple==0.1"), (False, False), None), ], ) @mock.patch("pip._internal.req.req_install.os.path.isdir") @mock.patch("pip._internal.req.req_install.os.path.isfile") def test_get_url_from_path( isdir_mock: mock.Mock, isfile_mock: mock.Mock, args: tuple[str, str], mock_returns: tuple[bool, bool], expected: None, ) -> None: isdir_mock.return_value = mock_returns[0] isfile_mock.return_value = mock_returns[1] assert _get_url_from_path(*args) is expected @mock.patch("pip._internal.req.req_install.os.path.isdir") @mock.patch("pip._internal.req.req_install.os.path.isfile") def test_get_url_from_path__archive_file( isdir_mock: mock.Mock, isfile_mock: mock.Mock ) -> None: isdir_mock.return_value = False isfile_mock.return_value = True name = "simple-0.1-py2.py3-none-any.whl" url = Path(f"/path/to/{name}").resolve(strict=False).as_uri() assert _get_url_from_path(f"/path/to/{name}", name) == url @mock.patch("pip._internal.req.req_install.os.path.isdir") @mock.patch("pip._internal.req.req_install.os.path.isfile") def test_get_url_from_path__installable_dir( isdir_mock: mock.Mock, isfile_mock: mock.Mock ) -> None: isdir_mock.return_value = True isfile_mock.return_value = True name = "some/setuptools/project" url = Path(f"/path/to/{name}").resolve(strict=False).as_uri() assert _get_url_from_path(f"/path/to/{name}", name) == url @mock.patch("pip._internal.req.req_install.os.path.isdir") def test_get_url_from_path__installable_error(isdir_mock: mock.Mock) -> None: isdir_mock.return_value = True name = "some/setuptools/project" path = os.path.join("/path/to/" + name) with pytest.raises(InstallationError) as e: _get_url_from_path(path, name) err_msg = e.value.args[0] assert "Neither 'setup.py' nor 'pyproject.toml' found" in err_msg