mirror of https://github.com/pypa/pip
Try to cover Path interface differences
This commit is contained in:
parent
e3952f8357
commit
846d8e5965
|
@ -339,13 +339,11 @@ class BaseDistribution(Protocol):
|
|||
"""Check whether an entry in the info directory is a file."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def iterdir(self, path: InfoPath) -> Iterator[pathlib.PurePosixPath]:
|
||||
"""Iterate through a directory in the info directory.
|
||||
def iter_distutils_script_names(self) -> Iterator[str]:
|
||||
"""Find distutils 'scripts' entries metadata.
|
||||
|
||||
Each item yielded would be a path relative to the info directory.
|
||||
|
||||
:raise FileNotFoundError: If ``path`` does not exist in the directory.
|
||||
:raise NotADirectoryError: If ``path`` does not point to a directory.
|
||||
If 'scripts' is supplied in ``setup.py``, distutils records those in the
|
||||
installed distribution's ``scripts`` directory, a file for each script.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
import contextlib
|
||||
import email.message
|
||||
import importlib.metadata
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import zipfile
|
||||
from typing import Collection, Iterable, Iterator, List, Mapping, Optional, Sequence
|
||||
from typing import (
|
||||
Collection,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Protocol,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
from pip._vendor.packaging.requirements import Requirement
|
||||
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
||||
|
@ -23,7 +32,21 @@ from .base import (
|
|||
)
|
||||
|
||||
|
||||
class _WheelDistribution(importlib.metadata.Distribution):
|
||||
class BasePath(Protocol):
|
||||
"""A protocol that various path objects conform.
|
||||
|
||||
This exists because importlib.metadata uses both ``pathlib.Path`` and
|
||||
``zipfile.Path``, and we need a common base for type hints (Union does not
|
||||
work well since ``zipfile.Path`` is too new for our linter setup).
|
||||
|
||||
This does not mean to be exhaustive, but only contains things that present
|
||||
in both classes *that we need*.
|
||||
"""
|
||||
|
||||
name: str
|
||||
|
||||
|
||||
class WheelDistribution(importlib.metadata.Distribution):
|
||||
"""Distribution read from a wheel.
|
||||
|
||||
Although ``importlib.metadata.PathDistribution`` accepts ``zipfile.Path``,
|
||||
|
@ -48,7 +71,7 @@ class _WheelDistribution(importlib.metadata.Distribution):
|
|||
zf: zipfile.ZipFile,
|
||||
name: str,
|
||||
location: str,
|
||||
) -> "_WheelDistribution":
|
||||
) -> "WheelDistribution":
|
||||
info_dir, _ = parse_wheel(zf, name)
|
||||
paths = (
|
||||
(name, pathlib.PurePosixPath(name.split("/", 1)[-1]))
|
||||
|
@ -80,8 +103,8 @@ class Distribution(BaseDistribution):
|
|||
def __init__(
|
||||
self,
|
||||
dist: importlib.metadata.Distribution,
|
||||
location: pathlib.PurePath,
|
||||
info_location: Optional[pathlib.PurePath],
|
||||
location: BasePath,
|
||||
info_location: Optional[BasePath],
|
||||
) -> None:
|
||||
self._dist = dist
|
||||
self._location = location
|
||||
|
@ -98,7 +121,7 @@ class Distribution(BaseDistribution):
|
|||
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
|
||||
try:
|
||||
with wheel.as_zipfile() as zf:
|
||||
dist = _WheelDistribution.from_zipfile(zf, name, wheel.location)
|
||||
dist = WheelDistribution.from_zipfile(zf, name, wheel.location)
|
||||
except zipfile.BadZipFile as e:
|
||||
raise InvalidWheel(wheel.location, name) from e
|
||||
except UnsupportedWheel as e:
|
||||
|
@ -115,21 +138,21 @@ class Distribution(BaseDistribution):
|
|||
return None
|
||||
return str(self._info_location)
|
||||
|
||||
def _get_dist_name(self) -> str:
|
||||
def _get_dist_normalized_name(self) -> NormalizedName:
|
||||
# The 'name' attribute is only available in Python 3.10 or later. We are
|
||||
# only targeting that, but Mypy does not know this.
|
||||
return self._dist.name # type: ignore[attr-defined]
|
||||
return canonicalize_name(self._dist.name) # type: ignore[attr-defined]
|
||||
|
||||
@property
|
||||
def canonical_name(self) -> NormalizedName:
|
||||
# Try to get the name from the metadata directory name. This is much
|
||||
# faster than reading metadata.
|
||||
if self._info_location is None:
|
||||
name = self._get_dist_name()
|
||||
elif self._info_location.suffix in (".dist-info", ".egg-info"):
|
||||
name, _, _ = self._info_location.stem.partition("-")
|
||||
else:
|
||||
name = self._get_dist_name()
|
||||
return self._get_dist_normalized_name()
|
||||
stem, suffix = os.path.splitext(self._info_location.name)
|
||||
if suffix not in (".dist-info", ".egg-info"):
|
||||
return self._get_dist_normalized_name()
|
||||
name, _, _ = stem.partition("-")
|
||||
return canonicalize_name(name)
|
||||
|
||||
@property
|
||||
|
@ -139,25 +162,11 @@ class Distribution(BaseDistribution):
|
|||
def is_file(self, path: InfoPath) -> bool:
|
||||
return self._dist.read_text(str(path)) is not None
|
||||
|
||||
def iterdir(self, path: InfoPath) -> Iterator[pathlib.PurePosixPath]:
|
||||
# We have an optimized implementation for wheels.
|
||||
with contextlib.suppress(AttributeError):
|
||||
return self._dist.iterdir(path) # type: ignore [attr-defined]
|
||||
|
||||
# This is not actually based on anything concrete to iterate on.
|
||||
if self._info_location is None:
|
||||
raise FileNotFoundError(path)
|
||||
|
||||
# Just use pathlib if this is actually on the filesystem.
|
||||
info_root = self._info_location
|
||||
full_path = info_root.joinpath(path)
|
||||
if isinstance(full_path, pathlib.Path):
|
||||
return (
|
||||
pathlib.PurePosixPath(p.relative_to(info_root).as_posix())
|
||||
for p in full_path.iterdir()
|
||||
)
|
||||
|
||||
raise FileNotFoundError(path) # Nothing else is implemetned for now.
|
||||
def iter_distutils_script_names(self) -> Iterator[str]:
|
||||
if not isinstance(self._info_location, pathlib.Path):
|
||||
return
|
||||
for child in self._info_location.joinpath("scripts").iterdir():
|
||||
yield child.name
|
||||
|
||||
def read_text(self, path: InfoPath) -> str:
|
||||
content = self._dist.read_text(str(path))
|
||||
|
@ -205,7 +214,7 @@ class Distribution(BaseDistribution):
|
|||
)
|
||||
|
||||
|
||||
def _get_info_location(d: importlib.metadata.Distribution) -> Optional[pathlib.Path]:
|
||||
def _get_info_location(d: importlib.metadata.Distribution) -> Optional[BasePath]:
|
||||
"""Find the path to the distribution's metadata directory.
|
||||
|
||||
HACK: This relies on importlib.metadata's private ``_path`` attribute. Not
|
||||
|
|
|
@ -2,9 +2,8 @@ import email.message
|
|||
import email.parser
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import zipfile
|
||||
from typing import Collection, Generator, Iterable, List, Mapping, NamedTuple, Optional
|
||||
from typing import Collection, Iterable, Iterator, List, Mapping, NamedTuple, Optional
|
||||
|
||||
from pip._vendor import pkg_resources
|
||||
from pip._vendor.packaging.requirements import Requirement
|
||||
|
@ -142,14 +141,8 @@ class Distribution(BaseDistribution):
|
|||
def is_file(self, path: InfoPath) -> bool:
|
||||
return self._dist.has_metadata(str(path))
|
||||
|
||||
def iterdir(self, path: InfoPath) -> Generator[pathlib.PurePosixPath, None, None]:
|
||||
name = str(path)
|
||||
if not self._dist.has_metadata(name):
|
||||
raise FileNotFoundError(name)
|
||||
if not self._dist.isdir(name):
|
||||
raise NotADirectoryError(name)
|
||||
for child in self._dist.metadata_listdir(name):
|
||||
yield pathlib.PurePosixPath(path, child)
|
||||
def iter_distutils_script_names(self) -> Iterator[str]:
|
||||
yield from self._dist.metadata_listdir("scripts")
|
||||
|
||||
def read_text(self, path: InfoPath) -> str:
|
||||
name = str(path)
|
||||
|
@ -244,6 +237,6 @@ class Environment(BaseEnvironment):
|
|||
return None
|
||||
return self._search_distribution(name)
|
||||
|
||||
def _iter_distributions(self) -> Generator[BaseDistribution, None, None]:
|
||||
def _iter_distributions(self) -> Iterator[BaseDistribution]:
|
||||
for dist in self._ws:
|
||||
yield Distribution(dist)
|
||||
|
|
|
@ -558,10 +558,10 @@ class UninstallPathSet:
|
|||
|
||||
# find distutils scripts= scripts
|
||||
try:
|
||||
for script in dist.iterdir("scripts"):
|
||||
paths_to_remove.add(os.path.join(bin_dir, script.name))
|
||||
for script in dist.iter_distutils_script_names():
|
||||
paths_to_remove.add(os.path.join(bin_dir, script))
|
||||
if WINDOWS:
|
||||
paths_to_remove.add(os.path.join(bin_dir, f"{script.name}.bat"))
|
||||
paths_to_remove.add(os.path.join(bin_dir, f"{script}.bat"))
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
pass
|
||||
|
||||
|
|
Loading…
Reference in New Issue