1
1
Fork 0
mirror of https://github.com/pypa/pip synced 2023-12-13 21:30:23 +01:00

Merge pull request #11868 from DefaultRyan/normalize-path-cached

cache normalize_path in req_uninstall and is_local
This commit is contained in:
Stéphane Bidoul 2023-04-11 08:44:18 +02:00 committed by GitHub
commit 55f1251fa2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 45 additions and 11 deletions

4
news/11889.bugfix.rst Normal file
View file

@ -0,0 +1,4 @@
The ``uninstall`` and ``install --force-reinstall`` commands no longer call
``normalize_path()`` repeatedly on the same paths. Instead, these results are
cached for the duration of an uninstall operation, resulting in improved
performance, particularly on Windows.

View file

@ -11,8 +11,9 @@ from pip._internal.metadata import BaseDistribution
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.egg_link import egg_link_path_from_location
from pip._internal.utils.logging import getLogger, indent_log
from pip._internal.utils.misc import ask, is_local, normalize_path, renames, rmtree
from pip._internal.utils.misc import ask, normalize_path, renames, rmtree
from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory
from pip._internal.utils.virtualenv import running_under_virtualenv
logger = getLogger(__name__)
@ -312,6 +313,10 @@ class UninstallPathSet:
self._pth: Dict[str, UninstallPthEntries] = {}
self._dist = dist
self._moved_paths = StashedUninstallPathSet()
# Create local cache of normalize_path results. Creating an UninstallPathSet
# can result in hundreds/thousands of redundant calls to normalize_path with
# the same args, which hurts performance.
self._normalize_path_cached = functools.lru_cache()(normalize_path)
def _permitted(self, path: str) -> bool:
"""
@ -319,14 +324,17 @@ class UninstallPathSet:
remove/modify, False otherwise.
"""
return is_local(path)
# aka is_local, but caching normalized sys.prefix
if not running_under_virtualenv():
return True
return path.startswith(self._normalize_path_cached(sys.prefix))
def add(self, path: str) -> None:
head, tail = os.path.split(path)
# we normalize the head to resolve parent directory symlinks, but not
# the tail, since we only want to uninstall symlinks, not their targets
path = os.path.join(normalize_path(head), os.path.normcase(tail))
path = os.path.join(self._normalize_path_cached(head), os.path.normcase(tail))
if not os.path.exists(path):
return
@ -341,7 +349,7 @@ class UninstallPathSet:
self.add(cache_from_source(path))
def add_pth(self, pth_file: str, entry: str) -> None:
pth_file = normalize_path(pth_file)
pth_file = self._normalize_path_cached(pth_file)
if self._permitted(pth_file):
if pth_file not in self._pth:
self._pth[pth_file] = UninstallPthEntries(pth_file)
@ -531,7 +539,9 @@ class UninstallPathSet:
# above, so this only covers the setuptools-style editable.
with open(develop_egg_link) as fh:
link_pointer = os.path.normcase(fh.readline().strip())
normalized_link_pointer = normalize_path(link_pointer)
normalized_link_pointer = paths_to_remove._normalize_path_cached(
link_pointer
)
assert os.path.samefile(
normalized_link_pointer, normalized_dist_location
), (

View file

@ -21,7 +21,7 @@ from tests.lib import create_file
# Pretend all files are local, so UninstallPathSet accepts files in the tmpdir,
# outside the virtualenv
def mock_is_local(path: str) -> bool:
def mock_permitted(ups: UninstallPathSet, path: str) -> bool:
return True
@ -129,7 +129,11 @@ def test_compressed_listing(tmpdir: Path) -> None:
class TestUninstallPathSet:
def test_add(self, tmpdir: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(pip._internal.req.req_uninstall, "is_local", mock_is_local)
monkeypatch.setattr(
pip._internal.req.req_uninstall.UninstallPathSet,
"_permitted",
mock_permitted,
)
# Fix case for windows tests
file_extant = os.path.normcase(os.path.join(tmpdir, "foo"))
file_nonexistent = os.path.normcase(os.path.join(tmpdir, "nonexistent"))
@ -145,7 +149,11 @@ class TestUninstallPathSet:
assert ups._paths == {file_extant}
def test_add_pth(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(pip._internal.req.req_uninstall, "is_local", mock_is_local)
monkeypatch.setattr(
pip._internal.req.req_uninstall.UninstallPathSet,
"_permitted",
mock_permitted,
)
# Fix case for windows tests
tmpdir = os.path.normcase(tmp_path)
on_windows = sys.platform == "win32"
@ -175,7 +183,11 @@ class TestUninstallPathSet:
@pytest.mark.skipif("sys.platform == 'win32'")
def test_add_symlink(self, tmpdir: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(pip._internal.req.req_uninstall, "is_local", mock_is_local)
monkeypatch.setattr(
pip._internal.req.req_uninstall.UninstallPathSet,
"_permitted",
mock_permitted,
)
f = os.path.join(tmpdir, "foo")
with open(f, "w"):
pass
@ -187,7 +199,11 @@ class TestUninstallPathSet:
assert ups._paths == {foo_link}
def test_compact_shorter_path(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(pip._internal.req.req_uninstall, "is_local", mock_is_local)
monkeypatch.setattr(
pip._internal.req.req_uninstall.UninstallPathSet,
"_permitted",
mock_permitted,
)
monkeypatch.setattr("os.path.exists", lambda p: True)
# This deals with nt/posix path differences
short_path = os.path.normcase(
@ -202,7 +218,11 @@ class TestUninstallPathSet:
def test_detect_symlink_dirs(
self, monkeypatch: pytest.MonkeyPatch, tmpdir: Path
) -> None:
monkeypatch.setattr(pip._internal.req.req_uninstall, "is_local", mock_is_local)
monkeypatch.setattr(
pip._internal.req.req_uninstall.UninstallPathSet,
"_permitted",
mock_permitted,
)
# construct 2 paths:
# tmpdir/dir/file