annotate CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/importlib/metadata.py @ 69:33d812a61356

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