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

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

403 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 contextlib import suppress 

26from importlib import import_module 

27from importlib.abc import MetaPathFinder 

28from itertools import starmap 

29from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast 

30 

31from . import _meta 

32from ._collections import FreezableDefaultDict, Pair 

33from ._compat import ( 

34 NullFinder, 

35 install, 

36) 

37from ._functools import method_cache, pass_none 

38from ._itertools import always_iterable, bucket, unique_everseen 

39from ._meta import PackageMetadata, SimplePath 

40from .compat import py39, py311 

41 

42__all__ = [ 

43 'Distribution', 

44 'DistributionFinder', 

45 'PackageMetadata', 

46 'PackageNotFoundError', 

47 'SimplePath', 

48 'distribution', 

49 'distributions', 

50 'entry_points', 

51 'files', 

52 'metadata', 

53 'packages_distributions', 

54 'requires', 

55 'version', 

56] 

57 

58 

59class PackageNotFoundError(ModuleNotFoundError): 

60 """The package was not found.""" 

61 

62 def __str__(self) -> str: 

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

64 

65 @property 

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

67 (name,) = self.args 

68 return name 

69 

70 

71class Sectioned: 

72 """ 

73 A simple entry point config parser for performance 

74 

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

76 ... print(item) 

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

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

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

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

81 

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

83 >>> item = next(res) 

84 >>> item.name 

85 'sec1' 

86 >>> item.value 

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

88 >>> item = next(res) 

89 >>> item.value 

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

91 >>> item = next(res) 

92 >>> item.name 

93 'sec2' 

94 >>> item.value 

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

96 >>> list(res) 

97 [] 

98 """ 

99 

100 _sample = textwrap.dedent( 

101 """ 

102 [sec1] 

103 # comments ignored 

104 a = 1 

105 b = 2 

106 

107 [sec2] 

108 a = 2 

109 """ 

110 ).lstrip() 

111 

112 @classmethod 

113 def section_pairs(cls, text): 

114 return ( 

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

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

117 if section.name is not None 

118 ) 

119 

120 @staticmethod 

121 def read(text, filter_=None): 

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

123 name = None 

124 for value in lines: 

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

126 if section_match: 

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

128 continue 

129 yield Pair(name, value) 

130 

131 @staticmethod 

132 def valid(line: str): 

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

134 

135 

136class EntryPoint: 

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

138 

139 See `the packaging docs on entry points 

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

141 for more information. 

142 

143 >>> ep = EntryPoint( 

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

145 >>> ep.module 

146 'package.module' 

147 >>> ep.attr 

148 'attr' 

149 >>> ep.extras 

150 ['extra1', 'extra2'] 

151 """ 

152 

153 pattern = re.compile( 

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

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

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

157 ) 

158 """ 

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

160 which might look like: 

161 

162 - module 

163 - package.module 

164 - package.module:attribute 

165 - package.module:object.attribute 

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

167 

168 Other combinations are possible as well. 

169 

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

171 following the attr, and following any extras. 

172 """ 

173 

174 name: str 

175 value: str 

176 group: str 

177 

178 dist: Optional[Distribution] = None 

179 

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

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

182 

183 def load(self) -> Any: 

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

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

186 return the named object. 

187 """ 

188 match = cast(Match, self.pattern.match(self.value)) 

189 module = import_module(match.group('module')) 

190 attrs = filter(None, (match.group('attr') or '').split('.')) 

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

192 

193 @property 

194 def module(self) -> str: 

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

196 assert match is not None 

197 return match.group('module') 

198 

199 @property 

200 def attr(self) -> str: 

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

202 assert match is not None 

203 return match.group('attr') 

204 

205 @property 

206 def extras(self) -> List[str]: 

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

208 assert match is not None 

209 return re.findall(r'\w+', match.group('extras') or '') 

210 

211 def _for(self, dist): 

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

213 return self 

214 

215 def matches(self, **params): 

216 """ 

217 EntryPoint matches the given parameters. 

218 

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

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

221 True 

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

223 True 

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

225 False 

226 >>> ep.matches() 

227 True 

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

229 True 

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

231 True 

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

233 True 

234 """ 

235 self._disallow_dist(params) 

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

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

238 

239 @staticmethod 

240 def _disallow_dist(params): 

241 """ 

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

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

244 Traceback (most recent call last): 

245 ... 

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

247 """ 

248 if "dist" in params: 

249 raise ValueError( 

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

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

252 "located distribution." 

253 ) 

254 

255 def _key(self): 

256 return self.name, self.value, self.group 

257 

258 def __lt__(self, other): 

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

260 

261 def __eq__(self, other): 

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

263 

264 def __setattr__(self, name, value): 

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

266 

267 def __repr__(self): 

268 return ( 

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

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

271 ) 

272 

273 def __hash__(self) -> int: 

274 return hash(self._key()) 

275 

276 

277class EntryPoints(tuple): 

278 """ 

279 An immutable collection of selectable EntryPoint objects. 

280 """ 

281 

282 __slots__ = () 

283 

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

285 """ 

286 Get the EntryPoint in self matching name. 

287 """ 

288 try: 

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

290 except StopIteration: 

291 raise KeyError(name) 

292 

293 def __repr__(self): 

294 """ 

295 Repr with classname and tuple constructor to 

296 signal that we deviate from regular tuple behavior. 

297 """ 

298 return '%s(%r)' % (self.__class__.__name__, tuple(self)) 

299 

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

301 """ 

302 Select entry points from self that match the 

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

304 """ 

305 return EntryPoints(ep for ep in self if py39.ep_matches(ep, **params)) 

306 

307 @property 

308 def names(self) -> Set[str]: 

309 """ 

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

311 """ 

312 return {ep.name for ep in self} 

313 

314 @property 

315 def groups(self) -> Set[str]: 

316 """ 

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

318 """ 

319 return {ep.group for ep in self} 

320 

321 @classmethod 

322 def _from_text_for(cls, text, dist): 

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

324 

325 @staticmethod 

326 def _from_text(text): 

327 return ( 

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

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

330 ) 

331 

332 

333class PackagePath(pathlib.PurePosixPath): 

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

335 

336 hash: Optional[FileHash] 

337 size: int 

338 dist: Distribution 

339 

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

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

342 

343 def read_binary(self) -> bytes: 

344 return self.locate().read_bytes() 

345 

346 def locate(self) -> SimplePath: 

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

348 return self.dist.locate_file(self) 

349 

350 

351class FileHash: 

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

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

354 

355 def __repr__(self) -> str: 

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

357 

358 

359class Distribution(metaclass=abc.ABCMeta): 

360 """ 

361 An abstract Python distribution package. 

362 

363 Custom providers may derive from this class and define 

364 the abstract methods to provide a concrete implementation 

365 for their environment. Some providers may opt to override 

366 the default implementation of some properties to bypass 

367 the file-reading mechanism. 

368 """ 

369 

370 @abc.abstractmethod 

371 def read_text(self, filename) -> Optional[str]: 

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

373 

374 Python distribution metadata is organized by blobs of text 

375 typically represented as "files" in the metadata directory 

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

377 like: 

378 

379 - METADATA: The distribution metadata including fields 

380 like Name and Version and Description. 

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

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

383 - RECORD: A record of files according to 

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

385 

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

387 not listed here or none at all. 

388 

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

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

391 """ 

392 

393 @abc.abstractmethod 

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

395 """ 

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

397 to it. 

398 

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

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

401 Distribution to represent files in the distribution as 

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

403 to resolve such objects. 

404 

405 Some Distribution providers may elect not to resolve SimplePath 

406 objects within the distribution by raising a 

407 NotImplementedError, but consumers of such a Distribution would 

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

409 """ 

410 

411 @classmethod 

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

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

414 

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

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

417 package, if found. 

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

419 metadata cannot be found. 

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

421 """ 

422 if not name: 

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

424 try: 

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

426 except StopIteration: 

427 raise PackageNotFoundError(name) 

428 

429 @classmethod 

430 def discover( 

431 cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs 

432 ) -> Iterable[Distribution]: 

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

434 

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

436 a context. 

437 

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

439 :return: Iterable of Distribution objects for packages matching 

440 the context. 

441 """ 

442 if context and kwargs: 

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

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

445 return itertools.chain.from_iterable( 

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

447 ) 

448 

449 @staticmethod 

450 def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]: 

451 """ 

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

453 

454 Ref python/importlib_resources#489. 

455 """ 

456 buckets = bucket(dists, lambda dist: bool(dist.metadata)) 

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

458 

459 @staticmethod 

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

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

462 

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

464 :return: a concrete Distribution instance for the path 

465 """ 

466 return PathDistribution(pathlib.Path(path)) 

467 

468 @staticmethod 

469 def _discover_resolvers(): 

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

471 declared = ( 

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

473 ) 

474 return filter(None, declared) 

475 

476 @property 

477 def metadata(self) -> _meta.PackageMetadata: 

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

479 

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

481 metadata per the 

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

483 

484 Custom providers may provide the METADATA file or override this 

485 property. 

486 """ 

487 # deferred for performance (python/cpython#109829) 

488 from . import _adapters 

489 

490 opt_text = ( 

491 self.read_text('METADATA') 

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

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

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

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

496 or self.read_text('') 

497 ) 

498 text = cast(str, opt_text) 

499 return _adapters.Message(email.message_from_string(text)) 

500 

501 @property 

502 def name(self) -> str: 

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

504 return self.metadata['Name'] 

505 

506 @property 

507 def _normalized_name(self): 

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

509 return Prepared.normalize(self.name) 

510 

511 @property 

512 def version(self) -> str: 

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

514 return self.metadata['Version'] 

515 

516 @property 

517 def entry_points(self) -> EntryPoints: 

518 """ 

519 Return EntryPoints for this distribution. 

520 

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

522 or override this property. 

523 """ 

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

525 

526 @property 

527 def files(self) -> Optional[List[PackagePath]]: 

528 """Files in this distribution. 

529 

530 :return: List of PackagePath for this distribution or None 

531 

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

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

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

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

536 

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

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

539 able to resolve filenames provided by the package. 

540 """ 

541 

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

543 result = PackagePath(name) 

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

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

546 result.dist = self 

547 return result 

548 

549 @pass_none 

550 def make_files(lines): 

551 # Delay csv import, since Distribution.files is not as widely used 

552 # as other parts of importlib.metadata 

553 import csv 

554 

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

556 

557 @pass_none 

558 def skip_missing_files(package_paths): 

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

560 

561 return skip_missing_files( 

562 make_files( 

563 self._read_files_distinfo() 

564 or self._read_files_egginfo_installed() 

565 or self._read_files_egginfo_sources() 

566 ) 

567 ) 

568 

569 def _read_files_distinfo(self): 

570 """ 

571 Read the lines of RECORD. 

572 """ 

573 text = self.read_text('RECORD') 

574 return text and text.splitlines() 

575 

576 def _read_files_egginfo_installed(self): 

577 """ 

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

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

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

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

582 

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

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

585 Assume the file is accurate if it exists. 

586 """ 

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

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

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

590 # self._path. 

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

592 if not text or not subdir: 

593 return 

594 

595 paths = ( 

596 py311.relative_fix((subdir / name).resolve()) 

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

598 .as_posix() 

599 for name in text.splitlines() 

600 ) 

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

602 

603 def _read_files_egginfo_sources(self): 

604 """ 

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

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

607 might contain literal commas). 

608 

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

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

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

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

613 that are present after the package has been installed. 

614 """ 

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

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

617 

618 @property 

619 def requires(self) -> Optional[List[str]]: 

620 """Generated requirements specified for this Distribution""" 

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

622 return reqs and list(reqs) 

623 

624 def _read_dist_info_reqs(self): 

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

626 

627 def _read_egg_info_reqs(self): 

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

629 return pass_none(self._deps_from_requires_text)(source) 

630 

631 @classmethod 

632 def _deps_from_requires_text(cls, source): 

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

634 

635 @staticmethod 

636 def _convert_egg_info_reqs_to_simple_reqs(sections): 

637 """ 

638 Historically, setuptools would solicit and store 'extra' 

639 requirements, including those with environment markers, 

640 in separate sections. More modern tools expect each 

641 dependency to be defined separately, with any relevant 

642 extras and environment markers attached directly to that 

643 requirement. This method converts the former to the 

644 latter. See _test_deps_from_requires_text for an example. 

645 """ 

646 

647 def make_condition(name): 

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

649 

650 def quoted_marker(section): 

651 section = section or '' 

652 extra, sep, markers = section.partition(':') 

653 if extra and markers: 

654 markers = f'({markers})' 

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

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

657 

658 def url_req_space(req): 

659 """ 

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

661 Ref python/importlib_metadata#357. 

662 """ 

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

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

665 

666 for section in sections: 

667 space = url_req_space(section.value) 

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

669 

670 @property 

671 def origin(self): 

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

673 

674 def _load_json(self, filename): 

675 # Deferred for performance (python/importlib_metadata#503) 

676 import json 

677 

678 return pass_none(json.loads)( 

679 self.read_text(filename), 

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

681 ) 

682 

683 

684class DistributionFinder(MetaPathFinder): 

685 """ 

686 A MetaPathFinder capable of discovering installed distributions. 

687 

688 Custom providers should implement this interface in order to 

689 supply metadata. 

690 """ 

691 

692 class Context: 

693 """ 

694 Keyword arguments presented by the caller to 

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

696 to narrow the scope of a search for distributions 

697 in all DistributionFinders. 

698 

699 Each DistributionFinder may expect any parameters 

700 and should attempt to honor the canonical 

701 parameters defined below when appropriate. 

702 

703 This mechanism gives a custom provider a means to 

704 solicit additional details from the caller beyond 

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

706 For example, imagine a provider that exposes suites 

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

708 A caller may wish to query only for distributions in 

709 a particular realm and could call 

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

711 custom provider to only include distributions from that 

712 realm. 

713 """ 

714 

715 name = None 

716 """ 

717 Specific name for which a distribution finder should match. 

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

719 """ 

720 

721 def __init__(self, **kwargs): 

722 vars(self).update(kwargs) 

723 

724 @property 

725 def path(self) -> List[str]: 

726 """ 

727 The sequence of directory path that a distribution finder 

728 should search. 

729 

730 Typically refers to Python installed package paths such as 

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

732 """ 

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

734 

735 @abc.abstractmethod 

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

737 """ 

738 Find distributions. 

739 

740 Return an iterable of all Distribution instances capable of 

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

742 a DistributionFinder.Context instance. 

743 """ 

744 

745 

746class FastPath: 

747 """ 

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

749 

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

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

752 

753 >>> FastPath('').children() 

754 ['...'] 

755 

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

757 

758 >>> FastPath('foobar') is FastPath('foobar') 

759 True 

760 """ 

761 

762 @functools.lru_cache() # type: ignore[misc] 

763 def __new__(cls, root): 

764 return super().__new__(cls) 

765 

766 def __init__(self, root): 

767 self.root = root 

768 

769 def joinpath(self, child): 

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

771 

772 def children(self): 

773 with suppress(Exception): 

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

775 with suppress(Exception): 

776 return self.zip_children() 

777 return [] 

778 

779 def zip_children(self): 

780 # deferred for performance (python/importlib_metadata#502) 

781 from zipp.compat.overlay import zipfile 

782 

783 zip_path = zipfile.Path(self.root) 

784 names = zip_path.root.namelist() 

785 self.joinpath = zip_path.joinpath 

786 

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

788 

789 def search(self, name): 

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

791 

792 @property 

793 def mtime(self): 

794 with suppress(OSError): 

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

796 self.lookup.cache_clear() 

797 

798 @method_cache 

799 def lookup(self, mtime): 

800 return Lookup(self) 

801 

802 

803class Lookup: 

804 """ 

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

806 """ 

807 

808 def __init__(self, path: FastPath): 

809 """ 

810 Calculate all of the children representing metadata. 

811 

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

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

814 metadata (eggs). 

815 """ 

816 

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

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

819 self.infos = FreezableDefaultDict(list) 

820 self.eggs = FreezableDefaultDict(list) 

821 

822 for child in path.children(): 

823 low = child.lower() 

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

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

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

827 normalized = Prepared.normalize(name) 

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

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

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

831 legacy_normalized = Prepared.legacy_normalize(name) 

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

833 

834 self.infos.freeze() 

835 self.eggs.freeze() 

836 

837 def search(self, prepared: Prepared): 

838 """ 

839 Yield all infos and eggs matching the Prepared query. 

840 """ 

841 infos = ( 

842 self.infos[prepared.normalized] 

843 if prepared 

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

845 ) 

846 eggs = ( 

847 self.eggs[prepared.legacy_normalized] 

848 if prepared 

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

850 ) 

851 return itertools.chain(infos, eggs) 

852 

853 

854class Prepared: 

855 """ 

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

857 

858 Pre-calculates the normalization to prevent repeated operations. 

859 

860 >>> none = Prepared(None) 

861 >>> none.normalized 

862 >>> none.legacy_normalized 

863 >>> bool(none) 

864 False 

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

866 >>> sample.normalized 

867 'sample_pkg_name_foo' 

868 >>> sample.legacy_normalized 

869 'sample__pkg_name.foo' 

870 >>> bool(sample) 

871 True 

872 """ 

873 

874 normalized = None 

875 legacy_normalized = None 

876 

877 def __init__(self, name: Optional[str]): 

878 self.name = name 

879 if name is None: 

880 return 

881 self.normalized = self.normalize(name) 

882 self.legacy_normalized = self.legacy_normalize(name) 

883 

884 @staticmethod 

885 def normalize(name): 

886 """ 

887 PEP 503 normalization plus dashes as underscores. 

888 """ 

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

890 

891 @staticmethod 

892 def legacy_normalize(name): 

893 """ 

894 Normalize the package name as found in the convention in 

895 older packaging tools versions and specs. 

896 """ 

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

898 

899 def __bool__(self): 

900 return bool(self.name) 

901 

902 

903@install 

904class MetadataPathFinder(NullFinder, DistributionFinder): 

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

906 

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

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

909 """ 

910 

911 @classmethod 

912 def find_distributions( 

913 cls, context=DistributionFinder.Context() 

914 ) -> Iterable[PathDistribution]: 

915 """ 

916 Find distributions. 

917 

918 Return an iterable of all Distribution instances capable of 

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

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

921 of directories ``context.path``. 

922 """ 

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

924 return map(PathDistribution, found) 

925 

926 @classmethod 

927 def _search_paths(cls, name, paths): 

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

929 prepared = Prepared(name) 

930 return itertools.chain.from_iterable( 

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

932 ) 

933 

934 @classmethod 

935 def invalidate_caches(cls) -> None: 

936 FastPath.__new__.cache_clear() 

937 

938 

939class PathDistribution(Distribution): 

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

941 """Construct a distribution. 

942 

943 :param path: SimplePath indicating the metadata directory. 

944 """ 

945 self._path = path 

946 

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

948 with suppress( 

949 FileNotFoundError, 

950 IsADirectoryError, 

951 KeyError, 

952 NotADirectoryError, 

953 PermissionError, 

954 ): 

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

956 

957 return None 

958 

959 read_text.__doc__ = Distribution.read_text.__doc__ 

960 

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

962 return self._path.parent / path 

963 

964 @property 

965 def _normalized_name(self): 

966 """ 

967 Performance optimization: where possible, resolve the 

968 normalized name from the file system path. 

969 """ 

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

971 return ( 

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

973 or super()._normalized_name 

974 ) 

975 

976 @staticmethod 

977 def _name_from_stem(stem): 

978 """ 

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

980 'foo' 

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

982 'CherryPy' 

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

984 'face' 

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

986 """ 

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

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

989 return 

990 name, sep, rest = filename.partition('-') 

991 return name 

992 

993 

994def distribution(distribution_name: str) -> Distribution: 

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

996 

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

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

999 """ 

1000 return Distribution.from_name(distribution_name) 

1001 

1002 

1003def distributions(**kwargs) -> Iterable[Distribution]: 

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

1005 

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

1007 """ 

1008 return Distribution.discover(**kwargs) 

1009 

1010 

1011def metadata(distribution_name: str) -> _meta.PackageMetadata: 

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

1013 

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

1015 :return: A PackageMetadata containing the parsed metadata. 

1016 """ 

1017 return Distribution.from_name(distribution_name).metadata 

1018 

1019 

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

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

1022 

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

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

1025 "Version" metadata key. 

1026 """ 

1027 return distribution(distribution_name).version 

1028 

1029 

1030_unique = functools.partial( 

1031 unique_everseen, 

1032 key=py39.normalized_name, 

1033) 

1034""" 

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

1036""" 

1037 

1038 

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

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

1041 

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

1043 result to entry points matching those properties (see 

1044 EntryPoints.select()). 

1045 

1046 :return: EntryPoints for all installed packages. 

1047 """ 

1048 eps = itertools.chain.from_iterable( 

1049 dist.entry_points for dist in _unique(distributions()) 

1050 ) 

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

1052 

1053 

1054def files(distribution_name: str) -> Optional[List[PackagePath]]: 

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

1056 

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

1058 :return: List of files composing the distribution. 

1059 """ 

1060 return distribution(distribution_name).files 

1061 

1062 

1063def requires(distribution_name: str) -> Optional[List[str]]: 

1064 """ 

1065 Return a list of requirements for the named package. 

1066 

1067 :return: An iterable of requirements, suitable for 

1068 packaging.requirement.Requirement. 

1069 """ 

1070 return distribution(distribution_name).requires 

1071 

1072 

1073def packages_distributions() -> Mapping[str, List[str]]: 

1074 """ 

1075 Return a mapping of top-level packages to their 

1076 distributions. 

1077 

1078 >>> import collections.abc 

1079 >>> pkgs = packages_distributions() 

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

1081 True 

1082 """ 

1083 pkg_to_dist = collections.defaultdict(list) 

1084 for dist in distributions(): 

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

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

1087 return dict(pkg_to_dist) 

1088 

1089 

1090def _top_level_declared(dist): 

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

1092 

1093 

1094def _topmost(name: PackagePath) -> Optional[str]: 

1095 """ 

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

1097 """ 

1098 top, *rest = name.parts 

1099 return top if rest else None 

1100 

1101 

1102def _get_toplevel_name(name: PackagePath) -> str: 

1103 """ 

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

1105 sys.path. 

1106 

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

1108 'foo' 

1109 >>> _get_toplevel_name(PackagePath('foo')) 

1110 'foo' 

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

1112 'foo' 

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

1114 'foo' 

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

1116 'foo.pth' 

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

1118 'foo.dist-info' 

1119 """ 

1120 # Defer import of inspect for performance (python/cpython#118761) 

1121 import inspect 

1122 

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

1124 

1125 

1126def _top_level_inferred(dist): 

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

1128 

1129 def importable_name(name): 

1130 return '.' not in name 

1131 

1132 return filter(importable_name, opt_names)