Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
keewenaw
GitHub Repository: keewenaw/ethereum-wallet-cracker
Path: blob/main/test/lib/python3.9/site-packages/pip/_internal/metadata/base.py
4805 views
1
import csv
2
import email.message
3
import json
4
import logging
5
import pathlib
6
import re
7
import zipfile
8
from typing import (
9
IO,
10
TYPE_CHECKING,
11
Collection,
12
Container,
13
Iterable,
14
Iterator,
15
List,
16
Optional,
17
Tuple,
18
Union,
19
)
20
21
from pip._vendor.packaging.requirements import Requirement
22
from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
23
from pip._vendor.packaging.utils import NormalizedName
24
from pip._vendor.packaging.version import LegacyVersion, Version
25
26
from pip._internal.exceptions import NoneMetadataError
27
from pip._internal.locations import site_packages, user_site
28
from pip._internal.models.direct_url import (
29
DIRECT_URL_METADATA_NAME,
30
DirectUrl,
31
DirectUrlValidationError,
32
)
33
from pip._internal.utils.compat import stdlib_pkgs # TODO: Move definition here.
34
from pip._internal.utils.egg_link import egg_link_path_from_sys_path
35
from pip._internal.utils.misc import is_local, normalize_path
36
from pip._internal.utils.urls import url_to_path
37
38
if TYPE_CHECKING:
39
from typing import Protocol
40
else:
41
Protocol = object
42
43
DistributionVersion = Union[LegacyVersion, Version]
44
45
InfoPath = Union[str, pathlib.PurePath]
46
47
logger = logging.getLogger(__name__)
48
49
50
class BaseEntryPoint(Protocol):
51
@property
52
def name(self) -> str:
53
raise NotImplementedError()
54
55
@property
56
def value(self) -> str:
57
raise NotImplementedError()
58
59
@property
60
def group(self) -> str:
61
raise NotImplementedError()
62
63
64
def _convert_installed_files_path(
65
entry: Tuple[str, ...],
66
info: Tuple[str, ...],
67
) -> str:
68
"""Convert a legacy installed-files.txt path into modern RECORD path.
69
70
The legacy format stores paths relative to the info directory, while the
71
modern format stores paths relative to the package root, e.g. the
72
site-packages directory.
73
74
:param entry: Path parts of the installed-files.txt entry.
75
:param info: Path parts of the egg-info directory relative to package root.
76
:returns: The converted entry.
77
78
For best compatibility with symlinks, this does not use ``abspath()`` or
79
``Path.resolve()``, but tries to work with path parts:
80
81
1. While ``entry`` starts with ``..``, remove the equal amounts of parts
82
from ``info``; if ``info`` is empty, start appending ``..`` instead.
83
2. Join the two directly.
84
"""
85
while entry and entry[0] == "..":
86
if not info or info[-1] == "..":
87
info += ("..",)
88
else:
89
info = info[:-1]
90
entry = entry[1:]
91
return str(pathlib.Path(*info, *entry))
92
93
94
class BaseDistribution(Protocol):
95
@classmethod
96
def from_directory(cls, directory: str) -> "BaseDistribution":
97
"""Load the distribution from a metadata directory.
98
99
:param directory: Path to a metadata directory, e.g. ``.dist-info``.
100
"""
101
raise NotImplementedError()
102
103
@classmethod
104
def from_wheel(cls, wheel: "Wheel", name: str) -> "BaseDistribution":
105
"""Load the distribution from a given wheel.
106
107
:param wheel: A concrete wheel definition.
108
:param name: File name of the wheel.
109
110
:raises InvalidWheel: Whenever loading of the wheel causes a
111
:py:exc:`zipfile.BadZipFile` exception to be thrown.
112
:raises UnsupportedWheel: If the wheel is a valid zip, but malformed
113
internally.
114
"""
115
raise NotImplementedError()
116
117
def __repr__(self) -> str:
118
return f"{self.raw_name} {self.version} ({self.location})"
119
120
def __str__(self) -> str:
121
return f"{self.raw_name} {self.version}"
122
123
@property
124
def location(self) -> Optional[str]:
125
"""Where the distribution is loaded from.
126
127
A string value is not necessarily a filesystem path, since distributions
128
can be loaded from other sources, e.g. arbitrary zip archives. ``None``
129
means the distribution is created in-memory.
130
131
Do not canonicalize this value with e.g. ``pathlib.Path.resolve()``. If
132
this is a symbolic link, we want to preserve the relative path between
133
it and files in the distribution.
134
"""
135
raise NotImplementedError()
136
137
@property
138
def editable_project_location(self) -> Optional[str]:
139
"""The project location for editable distributions.
140
141
This is the directory where pyproject.toml or setup.py is located.
142
None if the distribution is not installed in editable mode.
143
"""
144
# TODO: this property is relatively costly to compute, memoize it ?
145
direct_url = self.direct_url
146
if direct_url:
147
if direct_url.is_local_editable():
148
return url_to_path(direct_url.url)
149
else:
150
# Search for an .egg-link file by walking sys.path, as it was
151
# done before by dist_is_editable().
152
egg_link_path = egg_link_path_from_sys_path(self.raw_name)
153
if egg_link_path:
154
# TODO: get project location from second line of egg_link file
155
# (https://github.com/pypa/pip/issues/10243)
156
return self.location
157
return None
158
159
@property
160
def installed_location(self) -> Optional[str]:
161
"""The distribution's "installed" location.
162
163
This should generally be a ``site-packages`` directory. This is
164
usually ``dist.location``, except for legacy develop-installed packages,
165
where ``dist.location`` is the source code location, and this is where
166
the ``.egg-link`` file is.
167
168
The returned location is normalized (in particular, with symlinks removed).
169
"""
170
raise NotImplementedError()
171
172
@property
173
def info_location(self) -> Optional[str]:
174
"""Location of the .[egg|dist]-info directory or file.
175
176
Similarly to ``location``, a string value is not necessarily a
177
filesystem path. ``None`` means the distribution is created in-memory.
178
179
For a modern .dist-info installation on disk, this should be something
180
like ``{location}/{raw_name}-{version}.dist-info``.
181
182
Do not canonicalize this value with e.g. ``pathlib.Path.resolve()``. If
183
this is a symbolic link, we want to preserve the relative path between
184
it and other files in the distribution.
185
"""
186
raise NotImplementedError()
187
188
@property
189
def installed_by_distutils(self) -> bool:
190
"""Whether this distribution is installed with legacy distutils format.
191
192
A distribution installed with "raw" distutils not patched by setuptools
193
uses one single file at ``info_location`` to store metadata. We need to
194
treat this specially on uninstallation.
195
"""
196
info_location = self.info_location
197
if not info_location:
198
return False
199
return pathlib.Path(info_location).is_file()
200
201
@property
202
def installed_as_egg(self) -> bool:
203
"""Whether this distribution is installed as an egg.
204
205
This usually indicates the distribution was installed by (older versions
206
of) easy_install.
207
"""
208
location = self.location
209
if not location:
210
return False
211
return location.endswith(".egg")
212
213
@property
214
def installed_with_setuptools_egg_info(self) -> bool:
215
"""Whether this distribution is installed with the ``.egg-info`` format.
216
217
This usually indicates the distribution was installed with setuptools
218
with an old pip version or with ``single-version-externally-managed``.
219
220
Note that this ensure the metadata store is a directory. distutils can
221
also installs an ``.egg-info``, but as a file, not a directory. This
222
property is *False* for that case. Also see ``installed_by_distutils``.
223
"""
224
info_location = self.info_location
225
if not info_location:
226
return False
227
if not info_location.endswith(".egg-info"):
228
return False
229
return pathlib.Path(info_location).is_dir()
230
231
@property
232
def installed_with_dist_info(self) -> bool:
233
"""Whether this distribution is installed with the "modern format".
234
235
This indicates a "modern" installation, e.g. storing metadata in the
236
``.dist-info`` directory. This applies to installations made by
237
setuptools (but through pip, not directly), or anything using the
238
standardized build backend interface (PEP 517).
239
"""
240
info_location = self.info_location
241
if not info_location:
242
return False
243
if not info_location.endswith(".dist-info"):
244
return False
245
return pathlib.Path(info_location).is_dir()
246
247
@property
248
def canonical_name(self) -> NormalizedName:
249
raise NotImplementedError()
250
251
@property
252
def version(self) -> DistributionVersion:
253
raise NotImplementedError()
254
255
@property
256
def setuptools_filename(self) -> str:
257
"""Convert a project name to its setuptools-compatible filename.
258
259
This is a copy of ``pkg_resources.to_filename()`` for compatibility.
260
"""
261
return self.raw_name.replace("-", "_")
262
263
@property
264
def direct_url(self) -> Optional[DirectUrl]:
265
"""Obtain a DirectUrl from this distribution.
266
267
Returns None if the distribution has no `direct_url.json` metadata,
268
or if `direct_url.json` is invalid.
269
"""
270
try:
271
content = self.read_text(DIRECT_URL_METADATA_NAME)
272
except FileNotFoundError:
273
return None
274
try:
275
return DirectUrl.from_json(content)
276
except (
277
UnicodeDecodeError,
278
json.JSONDecodeError,
279
DirectUrlValidationError,
280
) as e:
281
logger.warning(
282
"Error parsing %s for %s: %s",
283
DIRECT_URL_METADATA_NAME,
284
self.canonical_name,
285
e,
286
)
287
return None
288
289
@property
290
def installer(self) -> str:
291
try:
292
installer_text = self.read_text("INSTALLER")
293
except (OSError, ValueError, NoneMetadataError):
294
return "" # Fail silently if the installer file cannot be read.
295
for line in installer_text.splitlines():
296
cleaned_line = line.strip()
297
if cleaned_line:
298
return cleaned_line
299
return ""
300
301
@property
302
def editable(self) -> bool:
303
return bool(self.editable_project_location)
304
305
@property
306
def local(self) -> bool:
307
"""If distribution is installed in the current virtual environment.
308
309
Always True if we're not in a virtualenv.
310
"""
311
if self.installed_location is None:
312
return False
313
return is_local(self.installed_location)
314
315
@property
316
def in_usersite(self) -> bool:
317
if self.installed_location is None or user_site is None:
318
return False
319
return self.installed_location.startswith(normalize_path(user_site))
320
321
@property
322
def in_site_packages(self) -> bool:
323
if self.installed_location is None or site_packages is None:
324
return False
325
return self.installed_location.startswith(normalize_path(site_packages))
326
327
def is_file(self, path: InfoPath) -> bool:
328
"""Check whether an entry in the info directory is a file."""
329
raise NotImplementedError()
330
331
def iter_distutils_script_names(self) -> Iterator[str]:
332
"""Find distutils 'scripts' entries metadata.
333
334
If 'scripts' is supplied in ``setup.py``, distutils records those in the
335
installed distribution's ``scripts`` directory, a file for each script.
336
"""
337
raise NotImplementedError()
338
339
def read_text(self, path: InfoPath) -> str:
340
"""Read a file in the info directory.
341
342
:raise FileNotFoundError: If ``path`` does not exist in the directory.
343
:raise NoneMetadataError: If ``path`` exists in the info directory, but
344
cannot be read.
345
"""
346
raise NotImplementedError()
347
348
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
349
raise NotImplementedError()
350
351
@property
352
def metadata(self) -> email.message.Message:
353
"""Metadata of distribution parsed from e.g. METADATA or PKG-INFO.
354
355
This should return an empty message if the metadata file is unavailable.
356
357
:raises NoneMetadataError: If the metadata file is available, but does
358
not contain valid metadata.
359
"""
360
raise NotImplementedError()
361
362
@property
363
def metadata_version(self) -> Optional[str]:
364
"""Value of "Metadata-Version:" in distribution metadata, if available."""
365
return self.metadata.get("Metadata-Version")
366
367
@property
368
def raw_name(self) -> str:
369
"""Value of "Name:" in distribution metadata."""
370
# The metadata should NEVER be missing the Name: key, but if it somehow
371
# does, fall back to the known canonical name.
372
return self.metadata.get("Name", self.canonical_name)
373
374
@property
375
def requires_python(self) -> SpecifierSet:
376
"""Value of "Requires-Python:" in distribution metadata.
377
378
If the key does not exist or contains an invalid value, an empty
379
SpecifierSet should be returned.
380
"""
381
value = self.metadata.get("Requires-Python")
382
if value is None:
383
return SpecifierSet()
384
try:
385
# Convert to str to satisfy the type checker; this can be a Header object.
386
spec = SpecifierSet(str(value))
387
except InvalidSpecifier as e:
388
message = "Package %r has an invalid Requires-Python: %s"
389
logger.warning(message, self.raw_name, e)
390
return SpecifierSet()
391
return spec
392
393
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
394
"""Dependencies of this distribution.
395
396
For modern .dist-info distributions, this is the collection of
397
"Requires-Dist:" entries in distribution metadata.
398
"""
399
raise NotImplementedError()
400
401
def iter_provided_extras(self) -> Iterable[str]:
402
"""Extras provided by this distribution.
403
404
For modern .dist-info distributions, this is the collection of
405
"Provides-Extra:" entries in distribution metadata.
406
"""
407
raise NotImplementedError()
408
409
def _iter_declared_entries_from_record(self) -> Optional[Iterator[str]]:
410
try:
411
text = self.read_text("RECORD")
412
except FileNotFoundError:
413
return None
414
# This extra Path-str cast normalizes entries.
415
return (str(pathlib.Path(row[0])) for row in csv.reader(text.splitlines()))
416
417
def _iter_declared_entries_from_legacy(self) -> Optional[Iterator[str]]:
418
try:
419
text = self.read_text("installed-files.txt")
420
except FileNotFoundError:
421
return None
422
paths = (p for p in text.splitlines(keepends=False) if p)
423
root = self.location
424
info = self.info_location
425
if root is None or info is None:
426
return paths
427
try:
428
info_rel = pathlib.Path(info).relative_to(root)
429
except ValueError: # info is not relative to root.
430
return paths
431
if not info_rel.parts: # info *is* root.
432
return paths
433
return (
434
_convert_installed_files_path(pathlib.Path(p).parts, info_rel.parts)
435
for p in paths
436
)
437
438
def iter_declared_entries(self) -> Optional[Iterator[str]]:
439
"""Iterate through file entires declared in this distribution.
440
441
For modern .dist-info distributions, this is the files listed in the
442
``RECORD`` metadata file. For legacy setuptools distributions, this
443
comes from ``installed-files.txt``, with entries normalized to be
444
compatible with the format used by ``RECORD``.
445
446
:return: An iterator for listed entries, or None if the distribution
447
contains neither ``RECORD`` nor ``installed-files.txt``.
448
"""
449
return (
450
self._iter_declared_entries_from_record()
451
or self._iter_declared_entries_from_legacy()
452
)
453
454
455
class BaseEnvironment:
456
"""An environment containing distributions to introspect."""
457
458
@classmethod
459
def default(cls) -> "BaseEnvironment":
460
raise NotImplementedError()
461
462
@classmethod
463
def from_paths(cls, paths: Optional[List[str]]) -> "BaseEnvironment":
464
raise NotImplementedError()
465
466
def get_distribution(self, name: str) -> Optional["BaseDistribution"]:
467
"""Given a requirement name, return the installed distributions.
468
469
The name may not be normalized. The implementation must canonicalize
470
it for lookup.
471
"""
472
raise NotImplementedError()
473
474
def _iter_distributions(self) -> Iterator["BaseDistribution"]:
475
"""Iterate through installed distributions.
476
477
This function should be implemented by subclass, but never called
478
directly. Use the public ``iter_distribution()`` instead, which
479
implements additional logic to make sure the distributions are valid.
480
"""
481
raise NotImplementedError()
482
483
def iter_all_distributions(self) -> Iterator[BaseDistribution]:
484
"""Iterate through all installed distributions without any filtering."""
485
for dist in self._iter_distributions():
486
# Make sure the distribution actually comes from a valid Python
487
# packaging distribution. Pip's AdjacentTempDirectory leaves folders
488
# e.g. ``~atplotlib.dist-info`` if cleanup was interrupted. The
489
# valid project name pattern is taken from PEP 508.
490
project_name_valid = re.match(
491
r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$",
492
dist.canonical_name,
493
flags=re.IGNORECASE,
494
)
495
if not project_name_valid:
496
logger.warning(
497
"Ignoring invalid distribution %s (%s)",
498
dist.canonical_name,
499
dist.location,
500
)
501
continue
502
yield dist
503
504
def iter_installed_distributions(
505
self,
506
local_only: bool = True,
507
skip: Container[str] = stdlib_pkgs,
508
include_editables: bool = True,
509
editables_only: bool = False,
510
user_only: bool = False,
511
) -> Iterator[BaseDistribution]:
512
"""Return a list of installed distributions.
513
514
This is based on ``iter_all_distributions()`` with additional filtering
515
options. Note that ``iter_installed_distributions()`` without arguments
516
is *not* equal to ``iter_all_distributions()``, since some of the
517
configurations exclude packages by default.
518
519
:param local_only: If True (default), only return installations
520
local to the current virtualenv, if in a virtualenv.
521
:param skip: An iterable of canonicalized project names to ignore;
522
defaults to ``stdlib_pkgs``.
523
:param include_editables: If False, don't report editables.
524
:param editables_only: If True, only report editables.
525
:param user_only: If True, only report installations in the user
526
site directory.
527
"""
528
it = self.iter_all_distributions()
529
if local_only:
530
it = (d for d in it if d.local)
531
if not include_editables:
532
it = (d for d in it if not d.editable)
533
if editables_only:
534
it = (d for d in it if d.editable)
535
if user_only:
536
it = (d for d in it if d.in_usersite)
537
return (d for d in it if d.canonical_name not in skip)
538
539
540
class Wheel(Protocol):
541
location: str
542
543
def as_zipfile(self) -> zipfile.ZipFile:
544
raise NotImplementedError()
545
546
547
class FilesystemWheel(Wheel):
548
def __init__(self, location: str) -> None:
549
self.location = location
550
551
def as_zipfile(self) -> zipfile.ZipFile:
552
return zipfile.ZipFile(self.location, allowZip64=True)
553
554
555
class MemoryWheel(Wheel):
556
def __init__(self, location: str, stream: IO[bytes]) -> None:
557
self.location = location
558
self.stream = stream
559
560
def as_zipfile(self) -> zipfile.ZipFile:
561
return zipfile.ZipFile(self.stream, allowZip64=True)
562
563