annotate CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/importlib/metadata.py @ 68:5028fdace37b

planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author jpayne
date Tue, 18 Mar 2025 16:23:26 -0400
parents
children
rev   line source
jpayne@68 1 import io
jpayne@68 2 import os
jpayne@68 3 import re
jpayne@68 4 import abc
jpayne@68 5 import csv
jpayne@68 6 import sys
jpayne@68 7 import email
jpayne@68 8 import pathlib
jpayne@68 9 import zipfile
jpayne@68 10 import operator
jpayne@68 11 import functools
jpayne@68 12 import itertools
jpayne@68 13 import collections
jpayne@68 14
jpayne@68 15 from configparser import ConfigParser
jpayne@68 16 from contextlib import suppress
jpayne@68 17 from importlib import import_module
jpayne@68 18 from importlib.abc import MetaPathFinder
jpayne@68 19 from itertools import starmap
jpayne@68 20
jpayne@68 21
jpayne@68 22 __all__ = [
jpayne@68 23 'Distribution',
jpayne@68 24 'DistributionFinder',
jpayne@68 25 'PackageNotFoundError',
jpayne@68 26 'distribution',
jpayne@68 27 'distributions',
jpayne@68 28 'entry_points',
jpayne@68 29 'files',
jpayne@68 30 'metadata',
jpayne@68 31 'requires',
jpayne@68 32 'version',
jpayne@68 33 ]
jpayne@68 34
jpayne@68 35
jpayne@68 36 class PackageNotFoundError(ModuleNotFoundError):
jpayne@68 37 """The package was not found."""
jpayne@68 38
jpayne@68 39
jpayne@68 40 class EntryPoint(
jpayne@68 41 collections.namedtuple('EntryPointBase', 'name value group')):
jpayne@68 42 """An entry point as defined by Python packaging conventions.
jpayne@68 43
jpayne@68 44 See `the packaging docs on entry points
jpayne@68 45 <https://packaging.python.org/specifications/entry-points/>`_
jpayne@68 46 for more information.
jpayne@68 47 """
jpayne@68 48
jpayne@68 49 pattern = re.compile(
jpayne@68 50 r'(?P<module>[\w.]+)\s*'
jpayne@68 51 r'(:\s*(?P<attr>[\w.]+))?\s*'
jpayne@68 52 r'(?P<extras>\[.*\])?\s*$'
jpayne@68 53 )
jpayne@68 54 """
jpayne@68 55 A regular expression describing the syntax for an entry point,
jpayne@68 56 which might look like:
jpayne@68 57
jpayne@68 58 - module
jpayne@68 59 - package.module
jpayne@68 60 - package.module:attribute
jpayne@68 61 - package.module:object.attribute
jpayne@68 62 - package.module:attr [extra1, extra2]
jpayne@68 63
jpayne@68 64 Other combinations are possible as well.
jpayne@68 65
jpayne@68 66 The expression is lenient about whitespace around the ':',
jpayne@68 67 following the attr, and following any extras.
jpayne@68 68 """
jpayne@68 69
jpayne@68 70 def load(self):
jpayne@68 71 """Load the entry point from its definition. If only a module
jpayne@68 72 is indicated by the value, return that module. Otherwise,
jpayne@68 73 return the named object.
jpayne@68 74 """
jpayne@68 75 match = self.pattern.match(self.value)
jpayne@68 76 module = import_module(match.group('module'))
jpayne@68 77 attrs = filter(None, (match.group('attr') or '').split('.'))
jpayne@68 78 return functools.reduce(getattr, attrs, module)
jpayne@68 79
jpayne@68 80 @property
jpayne@68 81 def extras(self):
jpayne@68 82 match = self.pattern.match(self.value)
jpayne@68 83 return list(re.finditer(r'\w+', match.group('extras') or ''))
jpayne@68 84
jpayne@68 85 @classmethod
jpayne@68 86 def _from_config(cls, config):
jpayne@68 87 return [
jpayne@68 88 cls(name, value, group)
jpayne@68 89 for group in config.sections()
jpayne@68 90 for name, value in config.items(group)
jpayne@68 91 ]
jpayne@68 92
jpayne@68 93 @classmethod
jpayne@68 94 def _from_text(cls, text):
jpayne@68 95 config = ConfigParser(delimiters='=')
jpayne@68 96 # case sensitive: https://stackoverflow.com/q/1611799/812183
jpayne@68 97 config.optionxform = str
jpayne@68 98 try:
jpayne@68 99 config.read_string(text)
jpayne@68 100 except AttributeError: # pragma: nocover
jpayne@68 101 # Python 2 has no read_string
jpayne@68 102 config.readfp(io.StringIO(text))
jpayne@68 103 return EntryPoint._from_config(config)
jpayne@68 104
jpayne@68 105 def __iter__(self):
jpayne@68 106 """
jpayne@68 107 Supply iter so one may construct dicts of EntryPoints easily.
jpayne@68 108 """
jpayne@68 109 return iter((self.name, self))
jpayne@68 110
jpayne@68 111 def __reduce__(self):
jpayne@68 112 return (
jpayne@68 113 self.__class__,
jpayne@68 114 (self.name, self.value, self.group),
jpayne@68 115 )
jpayne@68 116
jpayne@68 117
jpayne@68 118 class PackagePath(pathlib.PurePosixPath):
jpayne@68 119 """A reference to a path in a package"""
jpayne@68 120
jpayne@68 121 def read_text(self, encoding='utf-8'):
jpayne@68 122 with self.locate().open(encoding=encoding) as stream:
jpayne@68 123 return stream.read()
jpayne@68 124
jpayne@68 125 def read_binary(self):
jpayne@68 126 with self.locate().open('rb') as stream:
jpayne@68 127 return stream.read()
jpayne@68 128
jpayne@68 129 def locate(self):
jpayne@68 130 """Return a path-like object for this path"""
jpayne@68 131 return self.dist.locate_file(self)
jpayne@68 132
jpayne@68 133
jpayne@68 134 class FileHash:
jpayne@68 135 def __init__(self, spec):
jpayne@68 136 self.mode, _, self.value = spec.partition('=')
jpayne@68 137
jpayne@68 138 def __repr__(self):
jpayne@68 139 return '<FileHash mode: {} value: {}>'.format(self.mode, self.value)
jpayne@68 140
jpayne@68 141
jpayne@68 142 class Distribution:
jpayne@68 143 """A Python distribution package."""
jpayne@68 144
jpayne@68 145 @abc.abstractmethod
jpayne@68 146 def read_text(self, filename):
jpayne@68 147 """Attempt to load metadata file given by the name.
jpayne@68 148
jpayne@68 149 :param filename: The name of the file in the distribution info.
jpayne@68 150 :return: The text if found, otherwise None.
jpayne@68 151 """
jpayne@68 152
jpayne@68 153 @abc.abstractmethod
jpayne@68 154 def locate_file(self, path):
jpayne@68 155 """
jpayne@68 156 Given a path to a file in this distribution, return a path
jpayne@68 157 to it.
jpayne@68 158 """
jpayne@68 159
jpayne@68 160 @classmethod
jpayne@68 161 def from_name(cls, name):
jpayne@68 162 """Return the Distribution for the given package name.
jpayne@68 163
jpayne@68 164 :param name: The name of the distribution package to search for.
jpayne@68 165 :return: The Distribution instance (or subclass thereof) for the named
jpayne@68 166 package, if found.
jpayne@68 167 :raises PackageNotFoundError: When the named package's distribution
jpayne@68 168 metadata cannot be found.
jpayne@68 169 """
jpayne@68 170 for resolver in cls._discover_resolvers():
jpayne@68 171 dists = resolver(DistributionFinder.Context(name=name))
jpayne@68 172 dist = next(dists, None)
jpayne@68 173 if dist is not None:
jpayne@68 174 return dist
jpayne@68 175 else:
jpayne@68 176 raise PackageNotFoundError(name)
jpayne@68 177
jpayne@68 178 @classmethod
jpayne@68 179 def discover(cls, **kwargs):
jpayne@68 180 """Return an iterable of Distribution objects for all packages.
jpayne@68 181
jpayne@68 182 Pass a ``context`` or pass keyword arguments for constructing
jpayne@68 183 a context.
jpayne@68 184
jpayne@68 185 :context: A ``DistributionFinder.Context`` object.
jpayne@68 186 :return: Iterable of Distribution objects for all packages.
jpayne@68 187 """
jpayne@68 188 context = kwargs.pop('context', None)
jpayne@68 189 if context and kwargs:
jpayne@68 190 raise ValueError("cannot accept context and kwargs")
jpayne@68 191 context = context or DistributionFinder.Context(**kwargs)
jpayne@68 192 return itertools.chain.from_iterable(
jpayne@68 193 resolver(context)
jpayne@68 194 for resolver in cls._discover_resolvers()
jpayne@68 195 )
jpayne@68 196
jpayne@68 197 @staticmethod
jpayne@68 198 def at(path):
jpayne@68 199 """Return a Distribution for the indicated metadata path
jpayne@68 200
jpayne@68 201 :param path: a string or path-like object
jpayne@68 202 :return: a concrete Distribution instance for the path
jpayne@68 203 """
jpayne@68 204 return PathDistribution(pathlib.Path(path))
jpayne@68 205
jpayne@68 206 @staticmethod
jpayne@68 207 def _discover_resolvers():
jpayne@68 208 """Search the meta_path for resolvers."""
jpayne@68 209 declared = (
jpayne@68 210 getattr(finder, 'find_distributions', None)
jpayne@68 211 for finder in sys.meta_path
jpayne@68 212 )
jpayne@68 213 return filter(None, declared)
jpayne@68 214
jpayne@68 215 @property
jpayne@68 216 def metadata(self):
jpayne@68 217 """Return the parsed metadata for this Distribution.
jpayne@68 218
jpayne@68 219 The returned object will have keys that name the various bits of
jpayne@68 220 metadata. See PEP 566 for details.
jpayne@68 221 """
jpayne@68 222 text = (
jpayne@68 223 self.read_text('METADATA')
jpayne@68 224 or self.read_text('PKG-INFO')
jpayne@68 225 # This last clause is here to support old egg-info files. Its
jpayne@68 226 # effect is to just end up using the PathDistribution's self._path
jpayne@68 227 # (which points to the egg-info file) attribute unchanged.
jpayne@68 228 or self.read_text('')
jpayne@68 229 )
jpayne@68 230 return email.message_from_string(text)
jpayne@68 231
jpayne@68 232 @property
jpayne@68 233 def version(self):
jpayne@68 234 """Return the 'Version' metadata for the distribution package."""
jpayne@68 235 return self.metadata['Version']
jpayne@68 236
jpayne@68 237 @property
jpayne@68 238 def entry_points(self):
jpayne@68 239 return EntryPoint._from_text(self.read_text('entry_points.txt'))
jpayne@68 240
jpayne@68 241 @property
jpayne@68 242 def files(self):
jpayne@68 243 """Files in this distribution.
jpayne@68 244
jpayne@68 245 :return: List of PackagePath for this distribution or None
jpayne@68 246
jpayne@68 247 Result is `None` if the metadata file that enumerates files
jpayne@68 248 (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
jpayne@68 249 missing.
jpayne@68 250 Result may be empty if the metadata exists but is empty.
jpayne@68 251 """
jpayne@68 252 file_lines = self._read_files_distinfo() or self._read_files_egginfo()
jpayne@68 253
jpayne@68 254 def make_file(name, hash=None, size_str=None):
jpayne@68 255 result = PackagePath(name)
jpayne@68 256 result.hash = FileHash(hash) if hash else None
jpayne@68 257 result.size = int(size_str) if size_str else None
jpayne@68 258 result.dist = self
jpayne@68 259 return result
jpayne@68 260
jpayne@68 261 return file_lines and list(starmap(make_file, csv.reader(file_lines)))
jpayne@68 262
jpayne@68 263 def _read_files_distinfo(self):
jpayne@68 264 """
jpayne@68 265 Read the lines of RECORD
jpayne@68 266 """
jpayne@68 267 text = self.read_text('RECORD')
jpayne@68 268 return text and text.splitlines()
jpayne@68 269
jpayne@68 270 def _read_files_egginfo(self):
jpayne@68 271 """
jpayne@68 272 SOURCES.txt might contain literal commas, so wrap each line
jpayne@68 273 in quotes.
jpayne@68 274 """
jpayne@68 275 text = self.read_text('SOURCES.txt')
jpayne@68 276 return text and map('"{}"'.format, text.splitlines())
jpayne@68 277
jpayne@68 278 @property
jpayne@68 279 def requires(self):
jpayne@68 280 """Generated requirements specified for this Distribution"""
jpayne@68 281 reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
jpayne@68 282 return reqs and list(reqs)
jpayne@68 283
jpayne@68 284 def _read_dist_info_reqs(self):
jpayne@68 285 return self.metadata.get_all('Requires-Dist')
jpayne@68 286
jpayne@68 287 def _read_egg_info_reqs(self):
jpayne@68 288 source = self.read_text('requires.txt')
jpayne@68 289 return source and self._deps_from_requires_text(source)
jpayne@68 290
jpayne@68 291 @classmethod
jpayne@68 292 def _deps_from_requires_text(cls, source):
jpayne@68 293 section_pairs = cls._read_sections(source.splitlines())
jpayne@68 294 sections = {
jpayne@68 295 section: list(map(operator.itemgetter('line'), results))
jpayne@68 296 for section, results in
jpayne@68 297 itertools.groupby(section_pairs, operator.itemgetter('section'))
jpayne@68 298 }
jpayne@68 299 return cls._convert_egg_info_reqs_to_simple_reqs(sections)
jpayne@68 300
jpayne@68 301 @staticmethod
jpayne@68 302 def _read_sections(lines):
jpayne@68 303 section = None
jpayne@68 304 for line in filter(None, lines):
jpayne@68 305 section_match = re.match(r'\[(.*)\]$', line)
jpayne@68 306 if section_match:
jpayne@68 307 section = section_match.group(1)
jpayne@68 308 continue
jpayne@68 309 yield locals()
jpayne@68 310
jpayne@68 311 @staticmethod
jpayne@68 312 def _convert_egg_info_reqs_to_simple_reqs(sections):
jpayne@68 313 """
jpayne@68 314 Historically, setuptools would solicit and store 'extra'
jpayne@68 315 requirements, including those with environment markers,
jpayne@68 316 in separate sections. More modern tools expect each
jpayne@68 317 dependency to be defined separately, with any relevant
jpayne@68 318 extras and environment markers attached directly to that
jpayne@68 319 requirement. This method converts the former to the
jpayne@68 320 latter. See _test_deps_from_requires_text for an example.
jpayne@68 321 """
jpayne@68 322 def make_condition(name):
jpayne@68 323 return name and 'extra == "{name}"'.format(name=name)
jpayne@68 324
jpayne@68 325 def parse_condition(section):
jpayne@68 326 section = section or ''
jpayne@68 327 extra, sep, markers = section.partition(':')
jpayne@68 328 if extra and markers:
jpayne@68 329 markers = '({markers})'.format(markers=markers)
jpayne@68 330 conditions = list(filter(None, [markers, make_condition(extra)]))
jpayne@68 331 return '; ' + ' and '.join(conditions) if conditions else ''
jpayne@68 332
jpayne@68 333 for section, deps in sections.items():
jpayne@68 334 for dep in deps:
jpayne@68 335 yield dep + parse_condition(section)
jpayne@68 336
jpayne@68 337
jpayne@68 338 class DistributionFinder(MetaPathFinder):
jpayne@68 339 """
jpayne@68 340 A MetaPathFinder capable of discovering installed distributions.
jpayne@68 341 """
jpayne@68 342
jpayne@68 343 class Context:
jpayne@68 344 """
jpayne@68 345 Keyword arguments presented by the caller to
jpayne@68 346 ``distributions()`` or ``Distribution.discover()``
jpayne@68 347 to narrow the scope of a search for distributions
jpayne@68 348 in all DistributionFinders.
jpayne@68 349
jpayne@68 350 Each DistributionFinder may expect any parameters
jpayne@68 351 and should attempt to honor the canonical
jpayne@68 352 parameters defined below when appropriate.
jpayne@68 353 """
jpayne@68 354
jpayne@68 355 name = None
jpayne@68 356 """
jpayne@68 357 Specific name for which a distribution finder should match.
jpayne@68 358 A name of ``None`` matches all distributions.
jpayne@68 359 """
jpayne@68 360
jpayne@68 361 def __init__(self, **kwargs):
jpayne@68 362 vars(self).update(kwargs)
jpayne@68 363
jpayne@68 364 @property
jpayne@68 365 def path(self):
jpayne@68 366 """
jpayne@68 367 The path that a distribution finder should search.
jpayne@68 368
jpayne@68 369 Typically refers to Python package paths and defaults
jpayne@68 370 to ``sys.path``.
jpayne@68 371 """
jpayne@68 372 return vars(self).get('path', sys.path)
jpayne@68 373
jpayne@68 374 @property
jpayne@68 375 def pattern(self):
jpayne@68 376 return '.*' if self.name is None else re.escape(self.name)
jpayne@68 377
jpayne@68 378 @abc.abstractmethod
jpayne@68 379 def find_distributions(self, context=Context()):
jpayne@68 380 """
jpayne@68 381 Find distributions.
jpayne@68 382
jpayne@68 383 Return an iterable of all Distribution instances capable of
jpayne@68 384 loading the metadata for packages matching the ``context``,
jpayne@68 385 a DistributionFinder.Context instance.
jpayne@68 386 """
jpayne@68 387
jpayne@68 388
jpayne@68 389 class MetadataPathFinder(DistributionFinder):
jpayne@68 390 @classmethod
jpayne@68 391 def find_distributions(cls, context=DistributionFinder.Context()):
jpayne@68 392 """
jpayne@68 393 Find distributions.
jpayne@68 394
jpayne@68 395 Return an iterable of all Distribution instances capable of
jpayne@68 396 loading the metadata for packages matching ``context.name``
jpayne@68 397 (or all names if ``None`` indicated) along the paths in the list
jpayne@68 398 of directories ``context.path``.
jpayne@68 399 """
jpayne@68 400 found = cls._search_paths(context.pattern, context.path)
jpayne@68 401 return map(PathDistribution, found)
jpayne@68 402
jpayne@68 403 @classmethod
jpayne@68 404 def _search_paths(cls, pattern, paths):
jpayne@68 405 """Find metadata directories in paths heuristically."""
jpayne@68 406 return itertools.chain.from_iterable(
jpayne@68 407 cls._search_path(path, pattern)
jpayne@68 408 for path in map(cls._switch_path, paths)
jpayne@68 409 )
jpayne@68 410
jpayne@68 411 @staticmethod
jpayne@68 412 def _switch_path(path):
jpayne@68 413 PYPY_OPEN_BUG = False
jpayne@68 414 if not PYPY_OPEN_BUG or os.path.isfile(path): # pragma: no branch
jpayne@68 415 with suppress(Exception):
jpayne@68 416 return zipfile.Path(path)
jpayne@68 417 return pathlib.Path(path)
jpayne@68 418
jpayne@68 419 @classmethod
jpayne@68 420 def _matches_info(cls, normalized, item):
jpayne@68 421 template = r'{pattern}(-.*)?\.(dist|egg)-info'
jpayne@68 422 manifest = template.format(pattern=normalized)
jpayne@68 423 return re.match(manifest, item.name, flags=re.IGNORECASE)
jpayne@68 424
jpayne@68 425 @classmethod
jpayne@68 426 def _matches_legacy(cls, normalized, item):
jpayne@68 427 template = r'{pattern}-.*\.egg[\\/]EGG-INFO'
jpayne@68 428 manifest = template.format(pattern=normalized)
jpayne@68 429 return re.search(manifest, str(item), flags=re.IGNORECASE)
jpayne@68 430
jpayne@68 431 @classmethod
jpayne@68 432 def _search_path(cls, root, pattern):
jpayne@68 433 if not root.is_dir():
jpayne@68 434 return ()
jpayne@68 435 normalized = pattern.replace('-', '_')
jpayne@68 436 return (item for item in root.iterdir()
jpayne@68 437 if cls._matches_info(normalized, item)
jpayne@68 438 or cls._matches_legacy(normalized, item))
jpayne@68 439
jpayne@68 440
jpayne@68 441 class PathDistribution(Distribution):
jpayne@68 442 def __init__(self, path):
jpayne@68 443 """Construct a distribution from a path to the metadata directory.
jpayne@68 444
jpayne@68 445 :param path: A pathlib.Path or similar object supporting
jpayne@68 446 .joinpath(), __div__, .parent, and .read_text().
jpayne@68 447 """
jpayne@68 448 self._path = path
jpayne@68 449
jpayne@68 450 def read_text(self, filename):
jpayne@68 451 with suppress(FileNotFoundError, IsADirectoryError, KeyError,
jpayne@68 452 NotADirectoryError, PermissionError):
jpayne@68 453 return self._path.joinpath(filename).read_text(encoding='utf-8')
jpayne@68 454 read_text.__doc__ = Distribution.read_text.__doc__
jpayne@68 455
jpayne@68 456 def locate_file(self, path):
jpayne@68 457 return self._path.parent / path
jpayne@68 458
jpayne@68 459
jpayne@68 460 def distribution(distribution_name):
jpayne@68 461 """Get the ``Distribution`` instance for the named package.
jpayne@68 462
jpayne@68 463 :param distribution_name: The name of the distribution package as a string.
jpayne@68 464 :return: A ``Distribution`` instance (or subclass thereof).
jpayne@68 465 """
jpayne@68 466 return Distribution.from_name(distribution_name)
jpayne@68 467
jpayne@68 468
jpayne@68 469 def distributions(**kwargs):
jpayne@68 470 """Get all ``Distribution`` instances in the current environment.
jpayne@68 471
jpayne@68 472 :return: An iterable of ``Distribution`` instances.
jpayne@68 473 """
jpayne@68 474 return Distribution.discover(**kwargs)
jpayne@68 475
jpayne@68 476
jpayne@68 477 def metadata(distribution_name):
jpayne@68 478 """Get the metadata for the named package.
jpayne@68 479
jpayne@68 480 :param distribution_name: The name of the distribution package to query.
jpayne@68 481 :return: An email.Message containing the parsed metadata.
jpayne@68 482 """
jpayne@68 483 return Distribution.from_name(distribution_name).metadata
jpayne@68 484
jpayne@68 485
jpayne@68 486 def version(distribution_name):
jpayne@68 487 """Get the version string for the named package.
jpayne@68 488
jpayne@68 489 :param distribution_name: The name of the distribution package to query.
jpayne@68 490 :return: The version string for the package as defined in the package's
jpayne@68 491 "Version" metadata key.
jpayne@68 492 """
jpayne@68 493 return distribution(distribution_name).version
jpayne@68 494
jpayne@68 495
jpayne@68 496 def entry_points():
jpayne@68 497 """Return EntryPoint objects for all installed packages.
jpayne@68 498
jpayne@68 499 :return: EntryPoint objects for all installed packages.
jpayne@68 500 """
jpayne@68 501 eps = itertools.chain.from_iterable(
jpayne@68 502 dist.entry_points for dist in distributions())
jpayne@68 503 by_group = operator.attrgetter('group')
jpayne@68 504 ordered = sorted(eps, key=by_group)
jpayne@68 505 grouped = itertools.groupby(ordered, by_group)
jpayne@68 506 return {
jpayne@68 507 group: tuple(eps)
jpayne@68 508 for group, eps in grouped
jpayne@68 509 }
jpayne@68 510
jpayne@68 511
jpayne@68 512 def files(distribution_name):
jpayne@68 513 """Return a list of files for the named package.
jpayne@68 514
jpayne@68 515 :param distribution_name: The name of the distribution package to query.
jpayne@68 516 :return: List of files composing the distribution.
jpayne@68 517 """
jpayne@68 518 return distribution(distribution_name).files
jpayne@68 519
jpayne@68 520
jpayne@68 521 def requires(distribution_name):
jpayne@68 522 """
jpayne@68 523 Return a list of requirements for the named package.
jpayne@68 524
jpayne@68 525 :return: An iterator of requirements, suitable for
jpayne@68 526 packaging.requirement.Requirement.
jpayne@68 527 """
jpayne@68 528 return distribution(distribution_name).requires