Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/importlib_metadata/__init__.py: 67%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

451 statements  

1""" 

2APIs exposing metadata from third-party Python packages. 

3 

4This codebase is shared between importlib.metadata in the stdlib 

5and importlib_metadata in PyPI. See 

6https://github.com/python/importlib_metadata/wiki/Development-Methodology 

7for more detail. 

8""" 

9 

10from __future__ import annotations 

11 

12import abc 

13import collections 

14import email 

15import functools 

16import itertools 

17import operator 

18import os 

19import pathlib 

20import posixpath 

21import re 

22import sys 

23import textwrap 

24import types 

25from collections.abc import Callable, Iterable, Mapping 

26from contextlib import suppress 

27from importlib import import_module 

28from importlib.abc import MetaPathFinder 

29from itertools import starmap 

30from typing import Any, TypeVar 

31 

32from ._collections import FreezableDefaultDict, Pair 

33from ._compat import ( 

34 NullFinder, 

35 install, 

36 localize, 

37) 

38from ._compat._functools import apply, compose 

39from ._functools import method_cache, noop, pass_none, passthrough 

40from ._itertools import always_iterable, bucket, unique_everseen 

41from ._meta import ( 

42 IDistribution, 

43 IPackagePath, 

44 PackageMetadata, 

45 SimplePath, 

46) 

47from .compat import py311 

48from .compat.py314 import lazy_importer 

49 

50__lazy_modules__ = { 

51 'csv', 

52 'inspect', 

53 'json', 

54 # 'zipfile' in the stdlib, which needs no overlay 

55 'zipp.compat.overlay', 

56 f'{__name__}._adapters', 

57} 

58""" 

59Imports to defer until first use (PEP 810). 

60 

61Each of them costs more than the path that reads it is traveled, so 

62deferring keeps that cost off every other caller. Stating it here rather 

63than importing inside those functions leaves the dependencies visible. 

64 

65Python 3.15 honors this declaration itself; earlier versions reach the 

66same end through the context below. 

67 

68The adapters are imported by member rather than as ``from . import 

69_adapters``, which names this package as the module to defer and which 

70no emulation can honor from inside the package itself. 

71""" 

72 

73with lazy_importer(): 

74 import csv 

75 import inspect 

76 import json 

77 

78 from zipp.compat.overlay import zipfile 

79 

80 from ._adapters import Message 

81 

82__all__ = [ 

83 'Distribution', 

84 'DistributionFinder', 

85 'IDistribution', 

86 'IPackagePath', 

87 'MetadataNotFound', 

88 'PackageMetadata', 

89 'PackageNotFoundError', 

90 'PackagePath', 

91 'SimplePath', 

92 'distribution', 

93 'distributions', 

94 'entry_points', 

95 'files', 

96 'metadata', 

97 'packages_distributions', 

98 'requires', 

99 'version', 

100] 

101 

102 

103_D = TypeVar('_D', bound=IDistribution) 

104 

105 

106class PackageNotFoundError(ModuleNotFoundError): 

107 """The package was not found.""" 

108 

109 def __str__(self) -> str: 

110 return f"No package metadata was found for {self.name}" 

111 

112 @property 

113 def name(self) -> str: # type: ignore[override] # make readonly 

114 (name,) = self.args 

115 return name 

116 

117 

118class MetadataNotFound(FileNotFoundError): 

119 """No metadata file is present in the distribution.""" 

120 

121 

122class Sectioned: 

123 """ 

124 A simple entry point config parser for performance 

125 

126 >>> for item in Sectioned.read(Sectioned._sample): 

127 ... print(item) 

128 Pair(name='sec1', value='# comments ignored') 

129 Pair(name='sec1', value='a = 1') 

130 Pair(name='sec1', value='b = 2') 

131 Pair(name='sec2', value='a = 2') 

132 

133 >>> res = Sectioned.section_pairs(Sectioned._sample) 

134 >>> item = next(res) 

135 >>> item.name 

136 'sec1' 

137 >>> item.value 

138 Pair(name='a', value='1') 

139 >>> item = next(res) 

140 >>> item.value 

141 Pair(name='b', value='2') 

142 >>> item = next(res) 

143 >>> item.name 

144 'sec2' 

145 >>> item.value 

146 Pair(name='a', value='2') 

147 >>> list(res) 

148 [] 

149 """ 

150 

151 _sample = textwrap.dedent( 

152 """ 

153 [sec1] 

154 # comments ignored 

155 a = 1 

156 b = 2 

157 

158 [sec2] 

159 a = 2 

160 """ 

161 ).lstrip() 

162 

163 @classmethod 

164 def section_pairs(cls, text): 

165 return ( 

166 section._replace(value=Pair.parse(section.value)) 

167 for section in cls.read(text, filter_=cls.valid) 

168 if section.name is not None 

169 ) 

170 

171 @staticmethod 

172 def read(text, filter_=None): 

173 lines = filter(filter_, map(str.strip, text.splitlines())) 

174 name = None 

175 for value in lines: 

176 section_match = value.startswith('[') and value.endswith(']') 

177 if section_match: 

178 name = value.strip('[]') 

179 continue 

180 yield Pair(name, value) 

181 

182 @staticmethod 

183 def valid(line: str): 

184 return line and not line.startswith('#') 

185 

186 

187class _EntryPointMatch(types.SimpleNamespace): 

188 module: str 

189 attr: str 

190 extras: str 

191 

192 

193class EntryPoint: 

194 """An entry point as defined by Python packaging conventions. 

195 

196 See `the packaging docs on entry points 

197 <https://packaging.python.org/specifications/entry-points/>`_ 

198 for more information. 

199 

200 >>> ep = EntryPoint( 

201 ... name=None, group=None, value='package.module:attr [extra1, extra2]') 

202 >>> ep.module 

203 'package.module' 

204 >>> ep.attr 

205 'attr' 

206 >>> ep.extras 

207 ['extra1', 'extra2'] 

208 

209 If the value package or module are not valid identifiers, a 

210 ValueError is raised on access. 

211 

212 >>> EntryPoint(name=None, group=None, value='invalid-name').module 

213 Traceback (most recent call last): 

214 ... 

215 ValueError: ('Invalid object reference...invalid-name... 

216 >>> EntryPoint(name=None, group=None, value='invalid-name').attr 

217 Traceback (most recent call last): 

218 ... 

219 ValueError: ('Invalid object reference...invalid-name... 

220 >>> EntryPoint(name=None, group=None, value='invalid-name').extras 

221 Traceback (most recent call last): 

222 ... 

223 ValueError: ('Invalid object reference...invalid-name... 

224 

225 Construction never raises; the value is validated only when 

226 accessed or loaded. 

227 

228 >>> ep = EntryPoint(name=None, group=None, value='invalid-name') 

229 >>> ep.load() 

230 Traceback (most recent call last): 

231 ... 

232 ValueError: ('Invalid object reference...invalid-name... 

233 

234 """ 

235 

236 pattern = re.compile( 

237 r'(?P<module>[\w.]+)\s*' 

238 r'(:\s*(?P<attr>[\w.]+)\s*)?' 

239 r'((?P<extras>\[.*\])\s*)?$' 

240 ) 

241 """ 

242 A regular expression describing the syntax for an entry point, 

243 which might look like: 

244 

245 - module 

246 - package.module 

247 - package.module:attribute 

248 - package.module:object.attribute 

249 - package.module:attr [extra1, extra2] 

250 

251 Other combinations are possible as well. 

252 

253 The expression is lenient about whitespace around the ':', 

254 following the attr, and following any extras. 

255 """ 

256 

257 name: str 

258 value: str 

259 group: str 

260 

261 dist: IDistribution | None = None 

262 

263 def __init__(self, name: str, value: str, group: str) -> None: 

264 vars(self).update(name=name, value=value, group=group) 

265 

266 def load(self) -> Any: 

267 """Load the entry point from its definition. If only a module 

268 is indicated by the value, return that module. Otherwise, 

269 return the named object. 

270 """ 

271 module = import_module(self.module) 

272 attrs = filter(None, (self.attr or '').split('.')) 

273 return functools.reduce(getattr, attrs, module) 

274 

275 @property 

276 def module(self) -> str: 

277 return self._match.module 

278 

279 @property 

280 def attr(self) -> str: 

281 return self._match.attr 

282 

283 @property 

284 def extras(self) -> list[str]: 

285 return re.findall(r'\w+', self._match.extras or '') 

286 

287 @functools.cached_property 

288 def _match(self) -> _EntryPointMatch: 

289 match = self.pattern.match(self.value) 

290 if not match: 

291 raise ValueError( 

292 'Invalid object reference. ' 

293 'See https://packaging.python.org' 

294 '/en/latest/specifications/entry-points/#data-model', 

295 self.value, 

296 ) 

297 return _EntryPointMatch(**match.groupdict()) 

298 

299 def _for(self, dist): 

300 vars(self).update(dist=dist) 

301 return self 

302 

303 def matches(self, **params): 

304 """ 

305 EntryPoint matches the given parameters. 

306 

307 >>> ep = EntryPoint(group='foo', name='bar', value='bing:bong [extra1, extra2]') 

308 >>> ep.matches(group='foo') 

309 True 

310 >>> ep.matches(name='bar', value='bing:bong [extra1, extra2]') 

311 True 

312 >>> ep.matches(group='foo', name='other') 

313 False 

314 >>> ep.matches() 

315 True 

316 >>> ep.matches(extras=['extra1', 'extra2']) 

317 True 

318 >>> ep.matches(module='bing') 

319 True 

320 >>> ep.matches(attr='bong') 

321 True 

322 """ 

323 self._disallow_dist(params) 

324 attrs = (getattr(self, param) for param in params) 

325 return all(map(operator.eq, params.values(), attrs)) 

326 

327 @staticmethod 

328 def _disallow_dist(params): 

329 """ 

330 Querying by dist is not allowed (dist objects are not comparable). 

331 >>> EntryPoint(name='fan', value='fav', group='fag').matches(dist='foo') 

332 Traceback (most recent call last): 

333 ... 

334 ValueError: "dist" is not suitable for matching... 

335 """ 

336 if "dist" in params: 

337 raise ValueError( 

338 '"dist" is not suitable for matching. ' 

339 "Instead, use Distribution.entry_points.select() on a " 

340 "located distribution." 

341 ) 

342 

343 def _key(self): 

344 return self.name, self.value, self.group 

345 

346 def __lt__(self, other): 

347 return self._key() < other._key() 

348 

349 def __eq__(self, other): 

350 return self._key() == other._key() 

351 

352 def __setattr__(self, name, value): 

353 raise AttributeError("EntryPoint objects are immutable.") 

354 

355 def __repr__(self): 

356 return ( 

357 f'EntryPoint(name={self.name!r}, value={self.value!r}, ' 

358 f'group={self.group!r})' 

359 ) 

360 

361 def __hash__(self) -> int: 

362 return hash(self._key()) 

363 

364 

365class EntryPoints(tuple): 

366 """ 

367 An immutable collection of selectable EntryPoint objects. 

368 """ 

369 

370 __slots__ = () 

371 

372 def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] # Work with str instead of int 

373 """ 

374 Get the EntryPoint in self matching name. 

375 """ 

376 try: 

377 return next(iter(self.select(name=name))) 

378 except StopIteration: 

379 raise KeyError(name) 

380 

381 def __repr__(self): 

382 """ 

383 Repr with classname and tuple constructor to 

384 signal that we deviate from regular tuple behavior. 

385 """ 

386 return f'{self.__class__.__name__}({tuple(self)!r})' 

387 

388 def select(self, **params) -> EntryPoints: 

389 """ 

390 Select entry points from self that match the 

391 given parameters (typically group and/or name). 

392 """ 

393 return EntryPoints(ep for ep in self if ep.matches(**params)) 

394 

395 @property 

396 def names(self) -> set[str]: 

397 """ 

398 Return the set of all names of all entry points. 

399 """ 

400 return {ep.name for ep in self} 

401 

402 @property 

403 def groups(self) -> set[str]: 

404 """ 

405 Return the set of all groups of all entry points. 

406 """ 

407 return {ep.group for ep in self} 

408 

409 @classmethod 

410 def _from_text_for(cls, text, dist): 

411 return cls(ep._for(dist) for ep in cls._from_text(text)) 

412 

413 @staticmethod 

414 def _from_text(text): 

415 return ( 

416 EntryPoint(name=item.value.name, value=item.value.value, group=item.name) 

417 for item in Sectioned.section_pairs(text or '') 

418 ) 

419 

420 

421class PackagePath(pathlib.PurePosixPath): 

422 """A reference to a path in a package""" 

423 

424 hash: FileHash | None 

425 size: int 

426 dist: IDistribution 

427 

428 def read_text(self, encoding: str = 'utf-8') -> str: 

429 return self.locate().read_text(encoding=encoding) 

430 

431 def read_binary(self) -> bytes: 

432 return self.locate().read_bytes() 

433 

434 def locate(self) -> SimplePath: 

435 """Return a path-like object for this path""" 

436 return self.dist.locate_file(self) 

437 

438 

439class FileHash: 

440 def __init__(self, spec: str) -> None: 

441 self.mode, _, self.value = spec.partition('=') 

442 

443 def __repr__(self) -> str: 

444 return f'<FileHash mode: {self.mode} value: {self.value}>' 

445 

446 

447def _identify_dist( 

448 accessor: Callable[[Distribution], str], 

449) -> Callable[[Distribution], str]: 

450 """ 

451 Decorate a required metadata accessor to identify the distribution 

452 in the error raised when its field is absent. 

453 """ 

454 

455 @functools.wraps(accessor) 

456 def wrapper(self: Distribution) -> str: 

457 try: 

458 return accessor(self) 

459 except KeyError as exc: 

460 (field,) = exc.args 

461 raise KeyError(f'Metadata field {field} not present in {self}') from None 

462 

463 return wrapper 

464 

465 

466class Distribution(metaclass=abc.ABCMeta): 

467 """ 

468 An abstract Python distribution package. 

469 

470 Custom providers may derive from this class and define 

471 the abstract methods to provide a concrete implementation 

472 for their environment. Some providers may opt to override 

473 the default implementation of some properties to bypass 

474 the file-reading mechanism. 

475 """ 

476 

477 @abc.abstractmethod 

478 def read_text(self, filename) -> str | None: 

479 """Attempt to load metadata file given by the name. 

480 

481 Python distribution metadata is organized by blobs of text 

482 typically represented as "files" in the metadata directory 

483 (e.g. package-1.0.dist-info). These files include things 

484 like: 

485 

486 - METADATA: The distribution metadata including fields 

487 like Name and Version and Description. 

488 - entry_points.txt: A series of entry points as defined in 

489 `the entry points spec <https://packaging.python.org/en/latest/specifications/entry-points/#file-format>`_. 

490 - RECORD: A record of files according to 

491 `this recording spec <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#the-record-file>`_. 

492 

493 A package may provide any set of files, including those 

494 not listed here or none at all. 

495 

496 :param filename: The name of the file in the distribution info. 

497 :return: The text if found, otherwise None. 

498 """ 

499 

500 @abc.abstractmethod 

501 def locate_file(self, path: str | os.PathLike[str]) -> SimplePath: 

502 """ 

503 Given a path to a file in this distribution, return a SimplePath 

504 to it. 

505 

506 This method is used by callers of ``Distribution.files()`` to 

507 locate files within the distribution. If it's possible for a 

508 Distribution to represent files in the distribution as 

509 ``SimplePath`` objects, it should implement this method 

510 to resolve such objects. 

511 

512 Some Distribution providers may elect not to resolve SimplePath 

513 objects within the distribution by raising a 

514 NotImplementedError, but consumers of such a Distribution would 

515 be unable to invoke ``Distribution.files()``. 

516 """ 

517 

518 @classmethod 

519 @apply(localize.dist) 

520 def from_name(cls, name: str) -> Distribution: 

521 """Return the Distribution for the given package name. 

522 

523 :param name: The name of the distribution package to search for. 

524 :return: The Distribution instance (or subclass thereof) for the named 

525 package, if found. 

526 :raises PackageNotFoundError: When the named package's distribution 

527 metadata cannot be found. 

528 :raises ValueError: When an invalid value is supplied for name. 

529 """ 

530 if not name: 

531 raise ValueError("A distribution name is required.") 

532 try: 

533 return next(iter(cls._prefer_valid(cls.discover(name=name)))) 

534 except StopIteration: 

535 raise PackageNotFoundError(name) from None 

536 

537 @classmethod 

538 @apply(functools.partial(map, localize.dist)) 

539 def discover( 

540 cls, *, context: DistributionFinder.Context | None = None, **kwargs 

541 ) -> Iterable[Distribution]: 

542 """Return an iterable of Distribution objects for all packages. 

543 

544 Pass a ``context`` or pass keyword arguments for constructing 

545 a context. 

546 

547 :context: A ``DistributionFinder.Context`` object. 

548 :return: Iterable of Distribution objects for packages matching 

549 the context. 

550 """ 

551 if context and kwargs: 

552 raise ValueError("cannot accept context and kwargs") 

553 context = context or DistributionFinder.Context(**kwargs) 

554 return itertools.chain.from_iterable( 

555 resolver(context) for resolver in cls._discover_resolvers() 

556 ) 

557 

558 @staticmethod 

559 def _prefer_valid(dists: Iterable[_D]) -> Iterable[_D]: 

560 """ 

561 Prefer (move to the front) distributions that have metadata. 

562 

563 Ref python/importlib_resources#489. 

564 """ 

565 

566 buckets = bucket(dists, _safe_has_metadata) 

567 return itertools.chain(buckets[True], buckets[False]) 

568 

569 @staticmethod 

570 def at(path: str | os.PathLike[str]) -> Distribution: 

571 """Return a Distribution for the indicated metadata path. 

572 

573 :param path: a string or path-like object 

574 :return: a concrete Distribution instance for the path 

575 """ 

576 return PathDistribution(pathlib.Path(path)) 

577 

578 @staticmethod 

579 def _discover_resolvers(): 

580 """Search the meta_path for resolvers (MetadataPathFinders).""" 

581 declared = ( 

582 getattr(finder, 'find_distributions', None) for finder in sys.meta_path 

583 ) 

584 return filter(None, declared) 

585 

586 @property 

587 @apply(localize.message) 

588 def metadata(self) -> PackageMetadata: 

589 """Return the parsed metadata for this Distribution. 

590 

591 The returned object will have keys that name the various bits of 

592 metadata per the 

593 `Core metadata specifications <https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata>`_. 

594 

595 Custom providers may provide the METADATA file or override this 

596 property. 

597 

598 :raises MetadataNotFound: If no metadata file is present. 

599 """ 

600 

601 text = self._metadata_text() 

602 return self._assemble_message(self._ensure_metadata_present(text)) 

603 

604 def _metadata_text(self) -> str | None: 

605 """ 

606 The metadata text, if any. 

607 

608 Separate from ``metadata`` so that ``_has_metadata`` can name 

609 the same files without repeating them and without paying to 

610 parse what it finds. 

611 """ 

612 return ( 

613 self.read_text('METADATA') 

614 or self.read_text('PKG-INFO') 

615 # This last clause is here to support old egg-info files. Its 

616 # effect is to just end up using the PathDistribution's self._path 

617 # (which points to the egg-info file) attribute unchanged. 

618 or self.read_text('') 

619 ) 

620 

621 def _has_metadata(self) -> bool: 

622 """ 

623 Whether this distribution supplies metadata. 

624 

625 Reads the metadata, as the interface offers nothing cheaper: 

626 ``locate_file`` resolves against the package root and not the 

627 metadata, and every other member loads the metadata or parses 

628 it. A subclass able to detect the metadata without reading it 

629 should override this (python/importlib_metadata#534). 

630 """ 

631 return bool(self._metadata_text()) 

632 

633 @staticmethod 

634 def _assemble_message(text: str) -> PackageMetadata: 

635 return Message(email.message_from_string(text)) 

636 

637 def _ensure_metadata_present(self, text: str | None) -> str: 

638 if text is not None: 

639 return text 

640 

641 raise MetadataNotFound('No package metadata was found.') 

642 

643 @property 

644 @_identify_dist 

645 def name(self) -> str: 

646 """Return the 'Name' metadata for the distribution package.""" 

647 return self.metadata['Name'] 

648 

649 @property 

650 def _normalized_name(self): 

651 """Return a normalized version of the name.""" 

652 return Prepared.normalize(self.name) 

653 

654 @property 

655 @_identify_dist 

656 def version(self) -> str: 

657 """Return the 'Version' metadata for the distribution package.""" 

658 return self.metadata['Version'] 

659 

660 @property 

661 def entry_points(self) -> EntryPoints: 

662 """ 

663 Return EntryPoints for this distribution. 

664 

665 Custom providers may provide the ``entry_points.txt`` file 

666 or override this property. 

667 """ 

668 return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) 

669 

670 @property 

671 @apply(pass_none(compose(list, functools.partial(map, localize.package_path)))) 

672 def files(self) -> list[IPackagePath] | None: 

673 """Files in this distribution. 

674 

675 :return: List of PackagePath-like objects for this distribution or None 

676 

677 Result is `None` if the metadata file that enumerates files 

678 (i.e. RECORD for dist-info, or installed-files.txt or 

679 SOURCES.txt for egg-info) is missing. 

680 Result may be empty if the metadata exists but is empty. 

681 

682 Custom providers are recommended to provide a "RECORD" file (in 

683 ``read_text``) or override this property to allow for callers to be 

684 able to resolve filenames provided by the package. 

685 """ 

686 

687 def make_file(name, hash=None, size_str=None): 

688 result = PackagePath(name) 

689 result.hash = FileHash(hash) if hash else None 

690 result.size = int(size_str) if size_str else None 

691 result.dist = self 

692 return result 

693 

694 @pass_none 

695 def make_files(lines): 

696 return starmap(make_file, csv.reader(lines)) 

697 

698 @pass_none 

699 def skip_missing_files(package_paths): 

700 return list(filter(lambda path: path.locate().exists(), package_paths)) 

701 

702 return skip_missing_files( 

703 make_files( 

704 self._read_files_distinfo() 

705 or self._read_files_egginfo_installed() 

706 or self._read_files_egginfo_sources() 

707 ) 

708 ) 

709 

710 def _read_files_distinfo(self): 

711 """ 

712 Read the lines of RECORD. 

713 """ 

714 text = self.read_text('RECORD') 

715 return text and text.splitlines() 

716 

717 def _read_files_egginfo_installed(self): 

718 """ 

719 Read installed-files.txt and return lines in a similar 

720 CSV-parsable format as RECORD: each file must be placed 

721 relative to the site-packages directory and must also be 

722 quoted (since file names can contain literal commas). 

723 

724 This file is written when the package is installed by pip, 

725 but it might not be written for other installation methods. 

726 Assume the file is accurate if it exists. 

727 """ 

728 text = self.read_text('installed-files.txt') 

729 # Prepend the .egg-info/ subdir to the lines in this file. 

730 # But this subdir is only available from PathDistribution's 

731 # self._path. 

732 subdir = getattr(self, '_path', None) 

733 if not text or not subdir: 

734 return 

735 

736 paths = ( 

737 py311 

738 .relative_fix((subdir / name).resolve()) 

739 .relative_to(self.locate_file('').resolve(), walk_up=True) 

740 .as_posix() 

741 for name in text.splitlines() 

742 ) 

743 return map('"{}"'.format, paths) 

744 

745 def _read_files_egginfo_sources(self): 

746 """ 

747 Read SOURCES.txt and return lines in a similar CSV-parsable 

748 format as RECORD: each file name must be quoted (since it 

749 might contain literal commas). 

750 

751 Note that SOURCES.txt is not a reliable source for what 

752 files are installed by a package. This file is generated 

753 for a source archive, and the files that are present 

754 there (e.g. setup.py) may not correctly reflect the files 

755 that are present after the package has been installed. 

756 """ 

757 text = self.read_text('SOURCES.txt') 

758 return text and map('"{}"'.format, text.splitlines()) 

759 

760 @property 

761 def requires(self) -> list[str] | None: 

762 """Generated requirements specified for this Distribution""" 

763 reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs() 

764 return reqs and list(reqs) 

765 

766 def _read_dist_info_reqs(self): 

767 return self.metadata.get_all('Requires-Dist') 

768 

769 def _read_egg_info_reqs(self): 

770 source = self.read_text('requires.txt') 

771 return pass_none(self._deps_from_requires_text)(source) 

772 

773 @classmethod 

774 def _deps_from_requires_text(cls, source): 

775 return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source)) 

776 

777 @staticmethod 

778 def _convert_egg_info_reqs_to_simple_reqs(sections): 

779 """ 

780 Historically, setuptools would solicit and store 'extra' 

781 requirements, including those with environment markers, 

782 in separate sections. More modern tools expect each 

783 dependency to be defined separately, with any relevant 

784 extras and environment markers attached directly to that 

785 requirement. This method converts the former to the 

786 latter. See _test_deps_from_requires_text for an example. 

787 """ 

788 

789 def make_condition(name): 

790 return name and f'extra == "{name}"' 

791 

792 def quoted_marker(section): 

793 section = section or '' 

794 extra, _sep, markers = section.partition(':') 

795 if extra and markers: 

796 markers = f'({markers})' 

797 conditions = list(filter(None, [markers, make_condition(extra)])) 

798 return '; ' + ' and '.join(conditions) if conditions else '' 

799 

800 def url_req_space(req): 

801 """ 

802 PEP 508 requires a space between the url_spec and the quoted_marker. 

803 Ref python/importlib_metadata#357. 

804 """ 

805 # '@' is uniquely indicative of a url_req. 

806 return ' ' * ('@' in req) 

807 

808 for section in sections: 

809 space = url_req_space(section.value) 

810 yield section.value + space + quoted_marker(section.name) 

811 

812 @property 

813 def origin(self): 

814 return self._load_json('direct_url.json') 

815 

816 def _load_json(self, filename): 

817 return pass_none(json.loads)( 

818 self.read_text(filename), 

819 object_hook=lambda data: types.SimpleNamespace(**data), 

820 ) 

821 

822 

823class DistributionFinder(MetaPathFinder): 

824 """ 

825 A MetaPathFinder capable of discovering installed distributions. 

826 

827 Custom providers should implement this interface in order to 

828 supply metadata. 

829 """ 

830 

831 class Context: 

832 """ 

833 Keyword arguments presented by the caller to 

834 ``distributions()`` or ``Distribution.discover()`` 

835 to narrow the scope of a search for distributions 

836 in all DistributionFinders. 

837 

838 Each DistributionFinder may expect any parameters 

839 and should attempt to honor the canonical 

840 parameters defined below when appropriate. 

841 

842 This mechanism gives a custom provider a means to 

843 solicit additional details from the caller beyond 

844 "name" and "path" when searching distributions. 

845 For example, imagine a provider that exposes suites 

846 of packages in either a "public" or "private" ``realm``. 

847 A caller may wish to query only for distributions in 

848 a particular realm and could call 

849 ``distributions(realm="private")`` to signal to the 

850 custom provider to only include distributions from that 

851 realm. 

852 """ 

853 

854 name = None 

855 """ 

856 Specific name for which a distribution finder should match. 

857 A name of ``None`` matches all distributions. 

858 """ 

859 

860 def __init__(self, **kwargs): 

861 vars(self).update(kwargs) 

862 

863 @property 

864 def path(self) -> list[str]: 

865 """ 

866 The sequence of directory path that a distribution finder 

867 should search. 

868 

869 Typically refers to Python installed package paths such as 

870 "site-packages" directories and defaults to ``sys.path``. 

871 """ 

872 return vars(self).get('path', sys.path) 

873 

874 @abc.abstractmethod 

875 def find_distributions(self, context=Context()) -> Iterable[Distribution]: 

876 """ 

877 Find distributions. 

878 

879 Return an iterable of all Distribution instances capable of 

880 loading the metadata for packages matching the ``context``, 

881 a DistributionFinder.Context instance. 

882 """ 

883 

884 

885@passthrough 

886def _clear_after_fork(cached): 

887 """Ensure ``func`` clears cached state after ``fork`` when supported. 

888 

889 ``FastPath`` caches zip-backed ``pathlib.Path`` objects that retain a 

890 reference to the parent's open ``ZipFile`` handle. Re-using a cached 

891 instance in a forked child can therefore resurrect invalid file pointers 

892 and trigger ``BadZipFile``/``OSError`` failures (python/importlib_metadata#520). 

893 Registering ``cache_clear`` with ``os.register_at_fork`` keeps each process 

894 on its own cache. 

895 """ 

896 getattr(os, 'register_at_fork', noop)(after_in_child=cached.cache_clear) 

897 

898 

899class FastPath: 

900 """ 

901 Micro-optimized class for searching a root for children. 

902 

903 Root is a path on the file system that may contain metadata 

904 directories either as natural directories or within a zip file. 

905 

906 >>> FastPath('').children() 

907 ['...'] 

908 

909 FastPath objects are cached and recycled for any given root. 

910 

911 >>> FastPath('foobar') is FastPath('foobar') 

912 True 

913 """ 

914 

915 @_clear_after_fork # type: ignore[misc] 

916 @functools.lru_cache 

917 def __new__(cls, root): 

918 return super().__new__(cls) 

919 

920 def __init__(self, root): 

921 self.root = root 

922 

923 def joinpath(self, child): 

924 return pathlib.Path(self.root, child) 

925 

926 def children(self): 

927 with suppress(Exception): 

928 return os.listdir(self.root or '.') 

929 with suppress(Exception): 

930 return self.zip_children() 

931 return [] 

932 

933 def zip_children(self): 

934 zip_path = zipfile.Path(self.root) 

935 names = zip_path.root.namelist() 

936 self.joinpath = zip_path.joinpath 

937 

938 return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names) 

939 

940 def search(self, name): 

941 return self.lookup(self.mtime).search(name) 

942 

943 @property 

944 def mtime(self): 

945 with suppress(OSError): 

946 return os.stat(self.root).st_mtime 

947 self.lookup.cache_clear() 

948 

949 @method_cache 

950 def lookup(self, mtime): 

951 return Lookup(self) 

952 

953 

954class Lookup: 

955 """ 

956 A micro-optimized class for searching a (fast) path for metadata. 

957 """ 

958 

959 def __init__(self, path: FastPath): 

960 """ 

961 Calculate all of the children representing metadata. 

962 

963 From the children in the path, calculate early all of the 

964 children that appear to represent metadata (infos) or legacy 

965 metadata (eggs). 

966 """ 

967 

968 base = os.path.basename(path.root).lower() 

969 base_is_egg = base.endswith(".egg") 

970 self.infos = FreezableDefaultDict(list) 

971 self.eggs = FreezableDefaultDict(list) 

972 

973 for child in path.children(): 

974 low = child.lower() 

975 if low.endswith((".dist-info", ".egg-info")): 

976 # rpartition is faster than splitext and suitable for this purpose. 

977 name = low.rpartition(".")[0].partition("-")[0] 

978 normalized = Prepared.normalize(name) 

979 self.infos[normalized].append(path.joinpath(child)) 

980 elif base_is_egg and low == "egg-info": 

981 name = base.rpartition(".")[0].partition("-")[0] 

982 legacy_normalized = Prepared.legacy_normalize(name) 

983 self.eggs[legacy_normalized].append(path.joinpath(child)) 

984 

985 self.infos.freeze() 

986 self.eggs.freeze() 

987 

988 def search(self, prepared: Prepared): 

989 """ 

990 Yield all infos and eggs matching the Prepared query. 

991 """ 

992 infos = ( 

993 self.infos[prepared.normalized] 

994 if prepared 

995 else itertools.chain.from_iterable(self.infos.values()) 

996 ) 

997 eggs = ( 

998 self.eggs[prepared.legacy_normalized] 

999 if prepared 

1000 else itertools.chain.from_iterable(self.eggs.values()) 

1001 ) 

1002 return itertools.chain(infos, eggs) 

1003 

1004 

1005class Prepared: 

1006 """ 

1007 A prepared search query for metadata on a possibly-named package. 

1008 

1009 Pre-calculates the normalization to prevent repeated operations. 

1010 

1011 >>> none = Prepared(None) 

1012 >>> none.normalized 

1013 >>> none.legacy_normalized 

1014 >>> bool(none) 

1015 False 

1016 >>> sample = Prepared('Sample__Pkg-name.foo') 

1017 >>> sample.normalized 

1018 'sample_pkg_name_foo' 

1019 >>> sample.legacy_normalized 

1020 'sample__pkg_name.foo' 

1021 >>> bool(sample) 

1022 True 

1023 """ 

1024 

1025 normalized = None 

1026 legacy_normalized = None 

1027 

1028 def __init__(self, name: str | None): 

1029 self.name = name 

1030 if name is None: 

1031 return 

1032 self.normalized = self.normalize(name) 

1033 self.legacy_normalized = self.legacy_normalize(name) 

1034 

1035 @staticmethod 

1036 def normalize(name): 

1037 """ 

1038 PEP 503 normalization plus dashes as underscores. 

1039 """ 

1040 return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_') 

1041 

1042 @staticmethod 

1043 def legacy_normalize(name): 

1044 """ 

1045 Normalize the package name as found in the convention in 

1046 older packaging tools versions and specs. 

1047 """ 

1048 return name.lower().replace('-', '_') 

1049 

1050 def __bool__(self): 

1051 return bool(self.name) 

1052 

1053 

1054class MetadataPathFinder(NullFinder, DistributionFinder): 

1055 """A degenerate finder for distribution packages on the file system. 

1056 

1057 This finder supplies only a find_distributions() method for versions 

1058 of Python that do not have a PathFinder find_distributions(). 

1059 """ 

1060 

1061 @classmethod 

1062 def find_distributions( 

1063 cls, context=DistributionFinder.Context() 

1064 ) -> Iterable[PathDistribution]: 

1065 """ 

1066 Find distributions. 

1067 

1068 Return an iterable of all Distribution instances capable of 

1069 loading the metadata for packages matching ``context.name`` 

1070 (or all names if ``None`` indicated) along the paths in the list 

1071 of directories ``context.path``. 

1072 """ 

1073 found = cls._search_paths(context.name, context.path) 

1074 return map(PathDistribution, found) 

1075 

1076 @classmethod 

1077 def _search_paths(cls, name, paths): 

1078 """Find metadata directories in paths heuristically.""" 

1079 prepared = Prepared(name) 

1080 return itertools.chain.from_iterable( 

1081 path.search(prepared) for path in map(FastPath, paths) 

1082 ) 

1083 

1084 @classmethod 

1085 def invalidate_caches(cls) -> None: 

1086 FastPath.__new__.cache_clear() 

1087 

1088 

1089class PathDistribution(Distribution): 

1090 def __init__(self, path: SimplePath) -> None: 

1091 """Construct a distribution. 

1092 

1093 :param path: SimplePath indicating the metadata directory. 

1094 """ 

1095 self._path = path 

1096 

1097 def __str__(self) -> str: 

1098 """ 

1099 Render as the metadata path, identifying the distribution 

1100 even when its metadata cannot name it. 

1101 

1102 >>> str(Distribution.at('foo-1.0.dist-info')) 

1103 'foo-1.0.dist-info' 

1104 """ 

1105 return str(self._path) 

1106 

1107 def read_text(self, filename: str | os.PathLike[str]) -> str | None: 

1108 with suppress( 

1109 FileNotFoundError, 

1110 IsADirectoryError, 

1111 KeyError, 

1112 NotADirectoryError, 

1113 PermissionError, 

1114 ): 

1115 return self._path.joinpath(filename).read_text(encoding='utf-8') 

1116 

1117 return None 

1118 

1119 read_text.__doc__ = Distribution.read_text.__doc__ 

1120 

1121 def _has_metadata(self) -> bool: 

1122 """ 

1123 Detect the metadata file without opening it 

1124 (python/importlib_metadata#534). 

1125 

1126 The last clause is for an old-style egg-info file, whose 

1127 metadata is the distribution's own path; a metadata directory 

1128 is not itself metadata. ``SimplePath`` doesn't name 

1129 ``is_file``, but only these legacy paths reach that clause and 

1130 they arrive as a ``pathlib.Path`` or a ``zipfile.Path``, both 

1131 of which supply it. 

1132 """ 

1133 return ( 

1134 self._path.joinpath('METADATA').exists() 

1135 or self._path.joinpath('PKG-INFO').exists() 

1136 or self._path.is_file() # type: ignore[attr-defined] 

1137 ) 

1138 

1139 def locate_file(self, path: str | os.PathLike[str]) -> SimplePath: 

1140 return self._path.parent / path 

1141 

1142 @property 

1143 def _normalized_name(self): 

1144 """ 

1145 Performance optimization: where possible, resolve the 

1146 normalized name from the file system path. 

1147 """ 

1148 stem = os.path.basename(str(self._path)) 

1149 return ( 

1150 pass_none(Prepared.normalize)(self._name_from_stem(stem)) 

1151 or super()._normalized_name 

1152 ) 

1153 

1154 @staticmethod 

1155 def _name_from_stem(stem): 

1156 """ 

1157 >>> PathDistribution._name_from_stem('foo-3.0.egg-info') 

1158 'foo' 

1159 >>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info') 

1160 'CherryPy' 

1161 >>> PathDistribution._name_from_stem('face.egg-info') 

1162 'face' 

1163 >>> PathDistribution._name_from_stem('foo.bar') 

1164 """ 

1165 filename, ext = os.path.splitext(stem) 

1166 if ext not in ('.dist-info', '.egg-info'): 

1167 return 

1168 name, _sep, _rest = filename.partition('-') 

1169 return name 

1170 

1171 

1172def distribution(distribution_name: str) -> IDistribution: 

1173 """Get the ``Distribution`` instance for the named package. 

1174 

1175 :param distribution_name: The name of the distribution package as a string. 

1176 :return: A ``Distribution`` instance (or subclass thereof). 

1177 """ 

1178 return Distribution.from_name(distribution_name) 

1179 

1180 

1181def distributions(**kwargs) -> Iterable[IDistribution]: 

1182 """Get all ``Distribution`` instances in the current environment. 

1183 

1184 :return: An iterable of ``Distribution`` instances. 

1185 """ 

1186 return Distribution.discover(**kwargs) 

1187 

1188 

1189def metadata(distribution_name: str) -> PackageMetadata: 

1190 """Get the metadata for the named package. 

1191 

1192 :param distribution_name: The name of the distribution package to query. 

1193 :return: A PackageMetadata containing the parsed metadata. 

1194 :raises MetadataNotFound: If no metadata file is present in the distribution. 

1195 """ 

1196 return Distribution.from_name(distribution_name).metadata 

1197 

1198 

1199def version(distribution_name: str) -> str: 

1200 """Get the version string for the named package. 

1201 

1202 :param distribution_name: The name of the distribution package to query. 

1203 :return: The version string for the package as defined in the package's 

1204 "Version" metadata key. 

1205 """ 

1206 return distribution(distribution_name).version 

1207 

1208 

1209def _safe_has_metadata(dist: IDistribution) -> bool: 

1210 """ 

1211 ``dist._has_metadata()``, for a distribution that may not supply it. 

1212 

1213 A provider's own ``Distribution`` subclass derives from 

1214 ``importlib.metadata`` and is passed through unadapted (#486), so 

1215 it has none; read its metadata as the default would. It supplies 

1216 no ``_metadata_text`` either, hence the unbound call. 

1217 

1218 Such a distribution cannot be asked for ``metadata`` behind a trap 

1219 instead. The ``MetadataNotFound`` it raises is the one from 

1220 ``importlib.metadata``, a different class from this package's, 

1221 which no trap of ours would catch. 

1222 """ 

1223 with suppress(AttributeError): 

1224 return dist._has_metadata() # type: ignore[attr-defined] 

1225 return bool(Distribution._metadata_text(dist)) # type: ignore[arg-type] 

1226 

1227 

1228_unique = functools.partial( 

1229 unique_everseen, 

1230 key=operator.attrgetter('_normalized_name'), 

1231) 

1232""" 

1233Wrapper for ``distributions`` to return unique distributions by name. 

1234""" 

1235 

1236 

1237def entry_points(**params) -> EntryPoints: 

1238 """Return EntryPoint objects for all installed packages. 

1239 

1240 Pass selection parameters (group or name) to filter the 

1241 result to entry points matching those properties (see 

1242 EntryPoints.select()). 

1243 

1244 :return: EntryPoints for all installed packages. 

1245 """ 

1246 eps = itertools.chain.from_iterable( 

1247 dist.entry_points 

1248 for dist in _unique(Distribution._prefer_valid(distributions())) 

1249 ) 

1250 return EntryPoints(eps).select(**params) 

1251 

1252 

1253def files(distribution_name: str) -> list[IPackagePath] | None: 

1254 """Return a list of files for the named package. 

1255 

1256 :param distribution_name: The name of the distribution package to query. 

1257 :return: List of files composing the distribution. 

1258 """ 

1259 return distribution(distribution_name).files 

1260 

1261 

1262def requires(distribution_name: str) -> list[str] | None: 

1263 """ 

1264 Return a list of requirements for the named package. 

1265 

1266 :return: An iterable of requirements, suitable for 

1267 packaging.requirement.Requirement. 

1268 """ 

1269 return distribution(distribution_name).requires 

1270 

1271 

1272def packages_distributions() -> Mapping[str, list[str]]: 

1273 """ 

1274 Return a mapping of top-level packages to their 

1275 distributions. 

1276 

1277 >>> import collections.abc 

1278 >>> pkgs = packages_distributions() 

1279 >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values()) 

1280 True 

1281 """ 

1282 pkg_to_dist = collections.defaultdict(list) 

1283 for dist in distributions(): 

1284 for pkg in _top_level_declared(dist) or _top_level_inferred(dist): 

1285 pkg_to_dist[pkg].append(dist.metadata['Name']) 

1286 return dict(pkg_to_dist) 

1287 

1288 

1289def _top_level_declared(dist): 

1290 return (dist.read_text('top_level.txt') or '').split() 

1291 

1292 

1293def _topmost(name: IPackagePath) -> str | None: 

1294 """ 

1295 Return the top-most parent as long as there is a parent. 

1296 """ 

1297 top, *rest = name.parts 

1298 return top if rest else None 

1299 

1300 

1301def _get_toplevel_name(name: IPackagePath) -> str: 

1302 """ 

1303 Infer a possibly importable module name from a name presumed on 

1304 sys.path. 

1305 

1306 >>> _get_toplevel_name(PackagePath('foo.py')) 

1307 'foo' 

1308 >>> _get_toplevel_name(PackagePath('foo')) 

1309 'foo' 

1310 >>> _get_toplevel_name(PackagePath('foo.pyc')) 

1311 'foo' 

1312 >>> _get_toplevel_name(PackagePath('foo/__init__.py')) 

1313 'foo' 

1314 >>> _get_toplevel_name(PackagePath('foo.pth')) 

1315 'foo.pth' 

1316 >>> _get_toplevel_name(PackagePath('foo.dist-info')) 

1317 'foo.dist-info' 

1318 """ 

1319 return _topmost(name) or inspect.getmodulename(name) or str(name) 

1320 

1321 

1322def _top_level_inferred(dist): 

1323 opt_names = set(map(_get_toplevel_name, always_iterable(dist.files))) 

1324 

1325 def importable_name(name): 

1326 return '.' not in name 

1327 

1328 return filter(importable_name, opt_names) 

1329 

1330 

1331install(MetadataPathFinder)