codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import email.message4import email.parser5import logging6import os7import zipfile8from collections.abc import Collection, Iterable, Iterator, Mapping9from typing import (10 NamedTuple,11)12 13from pip._vendor import pkg_resources14from pip._vendor.packaging.requirements import Requirement15from pip._vendor.packaging.utils import NormalizedName, canonicalize_name16from pip._vendor.packaging.version import Version17from pip._vendor.packaging.version import parse as parse_version18 19from pip._internal.exceptions import InvalidWheel, NoneMetadataError, UnsupportedWheel20from pip._internal.utils.egg_link import egg_link_path_from_location21from pip._internal.utils.misc import display_path, normalize_path22from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file23 24from .base import (25 BaseDistribution,26 BaseEntryPoint,27 BaseEnvironment,28 InfoPath,29 Wheel,30)31 32__all__ = ["NAME", "Distribution", "Environment"]33 34logger = logging.getLogger(__name__)35 36NAME = "pkg_resources"37 38 39class EntryPoint(NamedTuple):40 name: str41 value: str42 group: str43 44 45class InMemoryMetadata:46 """IMetadataProvider that reads metadata files from a dictionary.47 48 This also maps metadata decoding exceptions to our internal exception type.49 """50 51 def __init__(self, metadata: Mapping[str, bytes], wheel_name: str) -> None:52 self._metadata = metadata53 self._wheel_name = wheel_name54 55 def has_metadata(self, name: str) -> bool:56 return name in self._metadata57 58 def get_metadata(self, name: str) -> str:59 try:60 return self._metadata[name].decode()61 except UnicodeDecodeError as e:62 # Augment the default error with the origin of the file.63 raise UnsupportedWheel(64 f"Error decoding metadata for {self._wheel_name}: {e} in {name} file"65 )66 67 def get_metadata_lines(self, name: str) -> Iterable[str]:68 return pkg_resources.yield_lines(self.get_metadata(name))69 70 def metadata_isdir(self, name: str) -> bool:71 return False72 73 def metadata_listdir(self, name: str) -> list[str]:74 return []75 76 def run_script(self, script_name: str, namespace: str) -> None:77 pass78 79 80class Distribution(BaseDistribution):81 def __init__(self, dist: pkg_resources.Distribution) -> None:82 self._dist = dist83 # This is populated lazily, to avoid loading metadata for all possible84 # distributions eagerly.85 self.__extra_mapping: Mapping[NormalizedName, str] | None = None86 87 @property88 def _extra_mapping(self) -> Mapping[NormalizedName, str]:89 if self.__extra_mapping is None:90 self.__extra_mapping = {91 canonicalize_name(extra): extra for extra in self._dist.extras92 }93 94 return self.__extra_mapping95 96 @classmethod97 def from_directory(cls, directory: str) -> BaseDistribution:98 dist_dir = directory.rstrip(os.sep)99 100 # Build a PathMetadata object, from path to metadata. :wink:101 base_dir, dist_dir_name = os.path.split(dist_dir)102 metadata = pkg_resources.PathMetadata(base_dir, dist_dir)103 104 # Determine the correct Distribution object type.105 if dist_dir.endswith(".egg-info"):106 dist_cls = pkg_resources.Distribution107 dist_name = os.path.splitext(dist_dir_name)[0]108 else:109 assert dist_dir.endswith(".dist-info")110 dist_cls = pkg_resources.DistInfoDistribution111 dist_name = os.path.splitext(dist_dir_name)[0].split("-")[0]112 113 dist = dist_cls(base_dir, project_name=dist_name, metadata=metadata)114 return cls(dist)115 116 @classmethod117 def from_metadata_file_contents(118 cls,119 metadata_contents: bytes,120 filename: str,121 project_name: str,122 ) -> BaseDistribution:123 metadata_dict = {124 "METADATA": metadata_contents,125 }126 dist = pkg_resources.DistInfoDistribution(127 location=filename,128 metadata=InMemoryMetadata(metadata_dict, filename),129 project_name=project_name,130 )131 return cls(dist)132 133 @classmethod134 def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:135 try:136 with wheel.as_zipfile() as zf:137 info_dir, _ = parse_wheel(zf, name)138 metadata_dict = {139 path.split("/", 1)[-1]: read_wheel_metadata_file(zf, path)140 for path in zf.namelist()141 if path.startswith(f"{info_dir}/")142 }143 except zipfile.BadZipFile as e:144 raise InvalidWheel(wheel.location, name) from e145 except UnsupportedWheel as e:146 raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")147 dist = pkg_resources.DistInfoDistribution(148 location=wheel.location,149 metadata=InMemoryMetadata(metadata_dict, wheel.location),150 project_name=name,151 )152 return cls(dist)153 154 @property155 def location(self) -> str | None:156 return self._dist.location157 158 @property159 def installed_location(self) -> str | None:160 egg_link = egg_link_path_from_location(self.raw_name)161 if egg_link:162 location = egg_link163 elif self.location:164 location = self.location165 else:166 return None167 return normalize_path(location)168 169 @property170 def info_location(self) -> str | None:171 return self._dist.egg_info172 173 @property174 def installed_by_distutils(self) -> bool:175 # A distutils-installed distribution is provided by FileMetadata. This176 # provider has a "path" attribute not present anywhere else. Not the177 # best introspection logic, but pip has been doing this for a long time.178 try:179 return bool(self._dist._provider.path)180 except AttributeError:181 return False182 183 @property184 def canonical_name(self) -> NormalizedName:185 return canonicalize_name(self._dist.project_name)186 187 @property188 def version(self) -> Version:189 return parse_version(self._dist.version)190 191 @property192 def raw_version(self) -> str:193 return self._dist.version194 195 def is_file(self, path: InfoPath) -> bool:196 return self._dist.has_metadata(str(path))197 198 def iter_distutils_script_names(self) -> Iterator[str]:199 yield from self._dist.metadata_listdir("scripts")200 201 def read_text(self, path: InfoPath) -> str:202 name = str(path)203 if not self._dist.has_metadata(name):204 raise FileNotFoundError(name)205 content = self._dist.get_metadata(name)206 if content is None:207 raise NoneMetadataError(self, name)208 return content209 210 def iter_entry_points(self) -> Iterable[BaseEntryPoint]:211 for group, entries in self._dist.get_entry_map().items():212 for name, entry_point in entries.items():213 name, _, value = str(entry_point).partition("=")214 yield EntryPoint(name=name.strip(), value=value.strip(), group=group)215 216 def _metadata_impl(self) -> email.message.Message:217 """218 :raises NoneMetadataError: if the distribution reports `has_metadata()`219 True but `get_metadata()` returns None.220 """221 if isinstance(self._dist, pkg_resources.DistInfoDistribution):222 metadata_name = "METADATA"223 else:224 metadata_name = "PKG-INFO"225 try:226 metadata = self.read_text(metadata_name)227 except FileNotFoundError:228 if self.location:229 displaying_path = display_path(self.location)230 else:231 displaying_path = repr(self.location)232 logger.warning("No metadata found in %s", displaying_path)233 metadata = ""234 feed_parser = email.parser.FeedParser()235 feed_parser.feed(metadata)236 return feed_parser.close()237 238 def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:239 if extras:240 relevant_extras = set(self._extra_mapping) & set(241 map(canonicalize_name, extras)242 )243 extras = [self._extra_mapping[extra] for extra in relevant_extras]244 return self._dist.requires(extras)245 246 def iter_provided_extras(self) -> Iterable[NormalizedName]:247 return self._extra_mapping.keys()248 249 250class Environment(BaseEnvironment):251 def __init__(self, ws: pkg_resources.WorkingSet) -> None:252 self._ws = ws253 254 @classmethod255 def default(cls) -> BaseEnvironment:256 return cls(pkg_resources.working_set)257 258 @classmethod259 def from_paths(cls, paths: list[str] | None) -> BaseEnvironment:260 return cls(pkg_resources.WorkingSet(paths))261 262 def _iter_distributions(self) -> Iterator[BaseDistribution]:263 for dist in self._ws:264 yield Distribution(dist)265 266 def _search_distribution(self, name: str) -> BaseDistribution | None:267 """Find a distribution matching the ``name`` in the environment.268 269 This searches from *all* distributions available in the environment, to270 match the behavior of ``pkg_resources.get_distribution()``.271 """272 canonical_name = canonicalize_name(name)273 for dist in self.iter_all_distributions():274 if dist.canonical_name == canonical_name:275 return dist276 return None277 278 def get_distribution(self, name: str) -> BaseDistribution | None:279 # Search the distribution by looking through the working set.280 dist = self._search_distribution(name)281 if dist:282 return dist283 284 # If distribution could not be found, call working_set.require to285 # update the working set, and try to find the distribution again.286 # This might happen for e.g. when you install a package twice, once287 # using setup.py develop and again using setup.py install. Now when288 # running pip uninstall twice, the package gets removed from the289 # working set in the first uninstall, so we have to populate the290 # working set again so that pip knows about it and the packages gets291 # picked up and is successfully uninstalled the second time too.292 try:293 # We didn't pass in any version specifiers, so this can never294 # raise pkg_resources.VersionConflict.295 self._ws.require(name)296 except pkg_resources.DistributionNotFound:297 return None298 return self._search_distribution(name)299 