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

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

400 statements  

1from __future__ import annotations 

2 

3import os 

4import re 

5import abc 

6import sys 

7import json 

8import zipp 

9import email 

10import types 

11import pathlib 

12import operator 

13import textwrap 

14import functools 

15import itertools 

16import posixpath 

17import collections 

18 

19from . import _meta 

20from .compat import py39, py311 

21from ._collections import FreezableDefaultDict, Pair 

22from ._compat import ( 

23 NullFinder, 

24 install, 

25) 

26from ._functools import method_cache, pass_none 

27from ._itertools import always_iterable, bucket, unique_everseen 

28from ._meta import PackageMetadata, SimplePath 

29 

30from contextlib import suppress 

31from importlib import import_module 

32from importlib.abc import MetaPathFinder 

33from itertools import starmap 

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

35 

36__all__ = [ 

37 'Distribution', 

38 'DistributionFinder', 

39 'PackageMetadata', 

40 'PackageNotFoundError', 

41 'SimplePath', 

42 'distribution', 

43 'distributions', 

44 'entry_points', 

45 'files', 

46 'metadata', 

47 'packages_distributions', 

48 'requires', 

49 'version', 

50] 

51 

52 

53class PackageNotFoundError(ModuleNotFoundError): 

54 """The package was not found.""" 

55 

56 def __str__(self) -> str: 

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

58 

59 @property 

60 def name(self) -> str: # type: ignore[override] 

61 (name,) = self.args 

62 return name 

63 

64 

65class Sectioned: 

66 """ 

67 A simple entry point config parser for performance 

68 

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

70 ... print(item) 

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

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

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

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

75 

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

77 >>> item = next(res) 

78 >>> item.name 

79 'sec1' 

80 >>> item.value 

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

82 >>> item = next(res) 

83 >>> item.value 

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

85 >>> item = next(res) 

86 >>> item.name 

87 'sec2' 

88 >>> item.value 

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

90 >>> list(res) 

91 [] 

92 """ 

93 

94 _sample = textwrap.dedent( 

95 """ 

96 [sec1] 

97 # comments ignored 

98 a = 1 

99 b = 2 

100 

101 [sec2] 

102 a = 2 

103 """ 

104 ).lstrip() 

105 

106 @classmethod 

107 def section_pairs(cls, text): 

108 return ( 

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

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

111 if section.name is not None 

112 ) 

113 

114 @staticmethod 

115 def read(text, filter_=None): 

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

117 name = None 

118 for value in lines: 

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

120 if section_match: 

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

122 continue 

123 yield Pair(name, value) 

124 

125 @staticmethod 

126 def valid(line: str): 

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

128 

129 

130class EntryPoint: 

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

132 

133 See `the packaging docs on entry points 

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

135 for more information. 

136 

137 >>> ep = EntryPoint( 

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

139 >>> ep.module 

140 'package.module' 

141 >>> ep.attr 

142 'attr' 

143 >>> ep.extras 

144 ['extra1', 'extra2'] 

145 """ 

146 

147 pattern = re.compile( 

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

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

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

151 ) 

152 """ 

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

154 which might look like: 

155 

156 - module 

157 - package.module 

158 - package.module:attribute 

159 - package.module:object.attribute 

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

161 

162 Other combinations are possible as well. 

163 

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

165 following the attr, and following any extras. 

166 """ 

167 

168 name: str 

169 value: str 

170 group: str 

171 

172 dist: Optional[Distribution] = None 

173 

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

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

176 

177 def load(self) -> Any: 

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

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

180 return the named object. 

181 """ 

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

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

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

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

186 

187 @property 

188 def module(self) -> str: 

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

190 assert match is not None 

191 return match.group('module') 

192 

193 @property 

194 def attr(self) -> str: 

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

196 assert match is not None 

197 return match.group('attr') 

198 

199 @property 

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

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

202 assert match is not None 

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

204 

205 def _for(self, dist): 

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

207 return self 

208 

209 def matches(self, **params): 

210 """ 

211 EntryPoint matches the given parameters. 

212 

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

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

215 True 

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

217 True 

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

219 False 

220 >>> ep.matches() 

221 True 

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

223 True 

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

225 True 

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

227 True 

228 """ 

229 self._disallow_dist(params) 

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

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

232 

233 @staticmethod 

234 def _disallow_dist(params): 

235 """ 

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

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

238 Traceback (most recent call last): 

239 ... 

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

241 """ 

242 if "dist" in params: 

243 raise ValueError( 

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

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

246 "located distribution." 

247 ) 

248 

249 def _key(self): 

250 return self.name, self.value, self.group 

251 

252 def __lt__(self, other): 

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

254 

255 def __eq__(self, other): 

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

257 

258 def __setattr__(self, name, value): 

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

260 

261 def __repr__(self): 

262 return ( 

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

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

265 ) 

266 

267 def __hash__(self) -> int: 

268 return hash(self._key()) 

269 

270 

271class EntryPoints(tuple): 

272 """ 

273 An immutable collection of selectable EntryPoint objects. 

274 """ 

275 

276 __slots__ = () 

277 

278 def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] 

279 """ 

280 Get the EntryPoint in self matching name. 

281 """ 

282 try: 

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

284 except StopIteration: 

285 raise KeyError(name) 

286 

287 def __repr__(self): 

288 """ 

289 Repr with classname and tuple constructor to 

290 signal that we deviate from regular tuple behavior. 

291 """ 

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

293 

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

295 """ 

296 Select entry points from self that match the 

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

298 """ 

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

300 

301 @property 

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

303 """ 

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

305 """ 

306 return {ep.name for ep in self} 

307 

308 @property 

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

310 """ 

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

312 """ 

313 return {ep.group for ep in self} 

314 

315 @classmethod 

316 def _from_text_for(cls, text, dist): 

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

318 

319 @staticmethod 

320 def _from_text(text): 

321 return ( 

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

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

324 ) 

325 

326 

327class PackagePath(pathlib.PurePosixPath): 

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

329 

330 hash: Optional[FileHash] 

331 size: int 

332 dist: Distribution 

333 

334 def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override] 

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

336 

337 def read_binary(self) -> bytes: 

338 return self.locate().read_bytes() 

339 

340 def locate(self) -> SimplePath: 

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

342 return self.dist.locate_file(self) 

343 

344 

345class FileHash: 

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

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

348 

349 def __repr__(self) -> str: 

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

351 

352 

353class Distribution(metaclass=abc.ABCMeta): 

354 """ 

355 An abstract Python distribution package. 

356 

357 Custom providers may derive from this class and define 

358 the abstract methods to provide a concrete implementation 

359 for their environment. Some providers may opt to override 

360 the default implementation of some properties to bypass 

361 the file-reading mechanism. 

362 """ 

363 

364 @abc.abstractmethod 

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

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

367 

368 Python distribution metadata is organized by blobs of text 

369 typically represented as "files" in the metadata directory 

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

371 like: 

372 

373 - METADATA: The distribution metadata including fields 

374 like Name and Version and Description. 

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

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

377 - RECORD: A record of files according to 

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

379 

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

381 not listed here or none at all. 

382 

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

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

385 """ 

386 

387 @abc.abstractmethod 

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

389 """ 

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

391 to it. 

392 

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

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

395 Distribution to represent files in the distribution as 

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

397 to resolve such objects. 

398 

399 Some Distribution providers may elect not to resolve SimplePath 

400 objects within the distribution by raising a 

401 NotImplementedError, but consumers of such a Distribution would 

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

403 """ 

404 

405 @classmethod 

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

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

408 

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

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

411 package, if found. 

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

413 metadata cannot be found. 

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

415 """ 

416 if not name: 

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

418 try: 

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

420 except StopIteration: 

421 raise PackageNotFoundError(name) 

422 

423 @classmethod 

424 def discover( 

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

426 ) -> Iterable[Distribution]: 

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

428 

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

430 a context. 

431 

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

433 :return: Iterable of Distribution objects for packages matching 

434 the context. 

435 """ 

436 if context and kwargs: 

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

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

439 return itertools.chain.from_iterable( 

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

441 ) 

442 

443 @staticmethod 

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

445 """ 

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

447 

448 Ref python/importlib_resources#489. 

449 """ 

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

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

452 

453 @staticmethod 

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

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

456 

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

458 :return: a concrete Distribution instance for the path 

459 """ 

460 return PathDistribution(pathlib.Path(path)) 

461 

462 @staticmethod 

463 def _discover_resolvers(): 

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

465 declared = ( 

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

467 ) 

468 return filter(None, declared) 

469 

470 @property 

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

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

473 

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

475 metadata per the 

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

477 

478 Custom providers may provide the METADATA file or override this 

479 property. 

480 """ 

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

482 from . import _adapters 

483 

484 opt_text = ( 

485 self.read_text('METADATA') 

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

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

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

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

490 or self.read_text('') 

491 ) 

492 text = cast(str, opt_text) 

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

494 

495 @property 

496 def name(self) -> str: 

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

498 return self.metadata['Name'] 

499 

500 @property 

501 def _normalized_name(self): 

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

503 return Prepared.normalize(self.name) 

504 

505 @property 

506 def version(self) -> str: 

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

508 return self.metadata['Version'] 

509 

510 @property 

511 def entry_points(self) -> EntryPoints: 

512 """ 

513 Return EntryPoints for this distribution. 

514 

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

516 or override this property. 

517 """ 

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

519 

520 @property 

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

522 """Files in this distribution. 

523 

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

525 

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

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

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

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

530 

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

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

533 able to resolve filenames provided by the package. 

534 """ 

535 

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

537 result = PackagePath(name) 

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

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

540 result.dist = self 

541 return result 

542 

543 @pass_none 

544 def make_files(lines): 

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

546 # as other parts of importlib.metadata 

547 import csv 

548 

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

550 

551 @pass_none 

552 def skip_missing_files(package_paths): 

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

554 

555 return skip_missing_files( 

556 make_files( 

557 self._read_files_distinfo() 

558 or self._read_files_egginfo_installed() 

559 or self._read_files_egginfo_sources() 

560 ) 

561 ) 

562 

563 def _read_files_distinfo(self): 

564 """ 

565 Read the lines of RECORD. 

566 """ 

567 text = self.read_text('RECORD') 

568 return text and text.splitlines() 

569 

570 def _read_files_egginfo_installed(self): 

571 """ 

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

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

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

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

576 

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

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

579 Assume the file is accurate if it exists. 

580 """ 

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

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

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

584 # self._path. 

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

586 if not text or not subdir: 

587 return 

588 

589 paths = ( 

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

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

592 .as_posix() 

593 for name in text.splitlines() 

594 ) 

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

596 

597 def _read_files_egginfo_sources(self): 

598 """ 

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

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

601 might contain literal commas). 

602 

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

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

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

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

607 that are present after the package has been installed. 

608 """ 

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

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

611 

612 @property 

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

614 """Generated requirements specified for this Distribution""" 

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

616 return reqs and list(reqs) 

617 

618 def _read_dist_info_reqs(self): 

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

620 

621 def _read_egg_info_reqs(self): 

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

623 return pass_none(self._deps_from_requires_text)(source) 

624 

625 @classmethod 

626 def _deps_from_requires_text(cls, source): 

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

628 

629 @staticmethod 

630 def _convert_egg_info_reqs_to_simple_reqs(sections): 

631 """ 

632 Historically, setuptools would solicit and store 'extra' 

633 requirements, including those with environment markers, 

634 in separate sections. More modern tools expect each 

635 dependency to be defined separately, with any relevant 

636 extras and environment markers attached directly to that 

637 requirement. This method converts the former to the 

638 latter. See _test_deps_from_requires_text for an example. 

639 """ 

640 

641 def make_condition(name): 

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

643 

644 def quoted_marker(section): 

645 section = section or '' 

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

647 if extra and markers: 

648 markers = f'({markers})' 

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

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

651 

652 def url_req_space(req): 

653 """ 

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

655 Ref python/importlib_metadata#357. 

656 """ 

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

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

659 

660 for section in sections: 

661 space = url_req_space(section.value) 

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

663 

664 @property 

665 def origin(self): 

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

667 

668 def _load_json(self, filename): 

669 return pass_none(json.loads)( 

670 self.read_text(filename), 

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

672 ) 

673 

674 

675class DistributionFinder(MetaPathFinder): 

676 """ 

677 A MetaPathFinder capable of discovering installed distributions. 

678 

679 Custom providers should implement this interface in order to 

680 supply metadata. 

681 """ 

682 

683 class Context: 

684 """ 

685 Keyword arguments presented by the caller to 

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

687 to narrow the scope of a search for distributions 

688 in all DistributionFinders. 

689 

690 Each DistributionFinder may expect any parameters 

691 and should attempt to honor the canonical 

692 parameters defined below when appropriate. 

693 

694 This mechanism gives a custom provider a means to 

695 solicit additional details from the caller beyond 

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

697 For example, imagine a provider that exposes suites 

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

699 A caller may wish to query only for distributions in 

700 a particular realm and could call 

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

702 custom provider to only include distributions from that 

703 realm. 

704 """ 

705 

706 name = None 

707 """ 

708 Specific name for which a distribution finder should match. 

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

710 """ 

711 

712 def __init__(self, **kwargs): 

713 vars(self).update(kwargs) 

714 

715 @property 

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

717 """ 

718 The sequence of directory path that a distribution finder 

719 should search. 

720 

721 Typically refers to Python installed package paths such as 

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

723 """ 

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

725 

726 @abc.abstractmethod 

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

728 """ 

729 Find distributions. 

730 

731 Return an iterable of all Distribution instances capable of 

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

733 a DistributionFinder.Context instance. 

734 """ 

735 

736 

737class FastPath: 

738 """ 

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

740 

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

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

743 

744 >>> FastPath('').children() 

745 ['...'] 

746 

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

748 

749 >>> FastPath('foobar') is FastPath('foobar') 

750 True 

751 """ 

752 

753 @functools.lru_cache() # type: ignore 

754 def __new__(cls, root): 

755 return super().__new__(cls) 

756 

757 def __init__(self, root): 

758 self.root = root 

759 

760 def joinpath(self, child): 

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

762 

763 def children(self): 

764 with suppress(Exception): 

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

766 with suppress(Exception): 

767 return self.zip_children() 

768 return [] 

769 

770 def zip_children(self): 

771 zip_path = zipp.Path(self.root) 

772 names = zip_path.root.namelist() 

773 self.joinpath = zip_path.joinpath 

774 

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

776 

777 def search(self, name): 

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

779 

780 @property 

781 def mtime(self): 

782 with suppress(OSError): 

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

784 self.lookup.cache_clear() 

785 

786 @method_cache 

787 def lookup(self, mtime): 

788 return Lookup(self) 

789 

790 

791class Lookup: 

792 """ 

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

794 """ 

795 

796 def __init__(self, path: FastPath): 

797 """ 

798 Calculate all of the children representing metadata. 

799 

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

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

802 metadata (eggs). 

803 """ 

804 

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

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

807 self.infos = FreezableDefaultDict(list) 

808 self.eggs = FreezableDefaultDict(list) 

809 

810 for child in path.children(): 

811 low = child.lower() 

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

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

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

815 normalized = Prepared.normalize(name) 

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

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

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

819 legacy_normalized = Prepared.legacy_normalize(name) 

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

821 

822 self.infos.freeze() 

823 self.eggs.freeze() 

824 

825 def search(self, prepared: Prepared): 

826 """ 

827 Yield all infos and eggs matching the Prepared query. 

828 """ 

829 infos = ( 

830 self.infos[prepared.normalized] 

831 if prepared 

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

833 ) 

834 eggs = ( 

835 self.eggs[prepared.legacy_normalized] 

836 if prepared 

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

838 ) 

839 return itertools.chain(infos, eggs) 

840 

841 

842class Prepared: 

843 """ 

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

845 

846 Pre-calculates the normalization to prevent repeated operations. 

847 

848 >>> none = Prepared(None) 

849 >>> none.normalized 

850 >>> none.legacy_normalized 

851 >>> bool(none) 

852 False 

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

854 >>> sample.normalized 

855 'sample_pkg_name_foo' 

856 >>> sample.legacy_normalized 

857 'sample__pkg_name.foo' 

858 >>> bool(sample) 

859 True 

860 """ 

861 

862 normalized = None 

863 legacy_normalized = None 

864 

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

866 self.name = name 

867 if name is None: 

868 return 

869 self.normalized = self.normalize(name) 

870 self.legacy_normalized = self.legacy_normalize(name) 

871 

872 @staticmethod 

873 def normalize(name): 

874 """ 

875 PEP 503 normalization plus dashes as underscores. 

876 """ 

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

878 

879 @staticmethod 

880 def legacy_normalize(name): 

881 """ 

882 Normalize the package name as found in the convention in 

883 older packaging tools versions and specs. 

884 """ 

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

886 

887 def __bool__(self): 

888 return bool(self.name) 

889 

890 

891@install 

892class MetadataPathFinder(NullFinder, DistributionFinder): 

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

894 

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

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

897 """ 

898 

899 @classmethod 

900 def find_distributions( 

901 cls, context=DistributionFinder.Context() 

902 ) -> Iterable[PathDistribution]: 

903 """ 

904 Find distributions. 

905 

906 Return an iterable of all Distribution instances capable of 

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

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

909 of directories ``context.path``. 

910 """ 

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

912 return map(PathDistribution, found) 

913 

914 @classmethod 

915 def _search_paths(cls, name, paths): 

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

917 prepared = Prepared(name) 

918 return itertools.chain.from_iterable( 

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

920 ) 

921 

922 @classmethod 

923 def invalidate_caches(cls) -> None: 

924 FastPath.__new__.cache_clear() 

925 

926 

927class PathDistribution(Distribution): 

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

929 """Construct a distribution. 

930 

931 :param path: SimplePath indicating the metadata directory. 

932 """ 

933 self._path = path 

934 

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

936 with suppress( 

937 FileNotFoundError, 

938 IsADirectoryError, 

939 KeyError, 

940 NotADirectoryError, 

941 PermissionError, 

942 ): 

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

944 

945 return None 

946 

947 read_text.__doc__ = Distribution.read_text.__doc__ 

948 

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

950 return self._path.parent / path 

951 

952 @property 

953 def _normalized_name(self): 

954 """ 

955 Performance optimization: where possible, resolve the 

956 normalized name from the file system path. 

957 """ 

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

959 return ( 

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

961 or super()._normalized_name 

962 ) 

963 

964 @staticmethod 

965 def _name_from_stem(stem): 

966 """ 

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

968 'foo' 

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

970 'CherryPy' 

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

972 'face' 

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

974 """ 

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

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

977 return 

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

979 return name 

980 

981 

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

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

984 

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

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

987 """ 

988 return Distribution.from_name(distribution_name) 

989 

990 

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

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

993 

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

995 """ 

996 return Distribution.discover(**kwargs) 

997 

998 

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

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

1001 

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

1003 :return: A PackageMetadata containing the parsed metadata. 

1004 """ 

1005 return Distribution.from_name(distribution_name).metadata 

1006 

1007 

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

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

1010 

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

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

1013 "Version" metadata key. 

1014 """ 

1015 return distribution(distribution_name).version 

1016 

1017 

1018_unique = functools.partial( 

1019 unique_everseen, 

1020 key=py39.normalized_name, 

1021) 

1022""" 

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

1024""" 

1025 

1026 

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

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

1029 

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

1031 result to entry points matching those properties (see 

1032 EntryPoints.select()). 

1033 

1034 :return: EntryPoints for all installed packages. 

1035 """ 

1036 eps = itertools.chain.from_iterable( 

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

1038 ) 

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

1040 

1041 

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

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

1044 

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

1046 :return: List of files composing the distribution. 

1047 """ 

1048 return distribution(distribution_name).files 

1049 

1050 

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

1052 """ 

1053 Return a list of requirements for the named package. 

1054 

1055 :return: An iterable of requirements, suitable for 

1056 packaging.requirement.Requirement. 

1057 """ 

1058 return distribution(distribution_name).requires 

1059 

1060 

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

1062 """ 

1063 Return a mapping of top-level packages to their 

1064 distributions. 

1065 

1066 >>> import collections.abc 

1067 >>> pkgs = packages_distributions() 

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

1069 True 

1070 """ 

1071 pkg_to_dist = collections.defaultdict(list) 

1072 for dist in distributions(): 

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

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

1075 return dict(pkg_to_dist) 

1076 

1077 

1078def _top_level_declared(dist): 

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

1080 

1081 

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

1083 """ 

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

1085 """ 

1086 top, *rest = name.parts 

1087 return top if rest else None 

1088 

1089 

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

1091 """ 

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

1093 sys.path. 

1094 

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

1096 'foo' 

1097 >>> _get_toplevel_name(PackagePath('foo')) 

1098 'foo' 

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

1100 'foo' 

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

1102 'foo' 

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

1104 'foo.pth' 

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

1106 'foo.dist-info' 

1107 """ 

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

1109 import inspect 

1110 

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

1112 

1113 

1114def _top_level_inferred(dist): 

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

1116 

1117 def importable_name(name): 

1118 return '.' not in name 

1119 

1120 return filter(importable_name, opt_names)