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

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

394 statements  

1from __future__ import annotations 

2 

3import os 

4import re 

5import abc 

6import sys 

7import json 

8import zipp 

9import email 

10import types 

11import inspect 

12import pathlib 

13import operator 

14import textwrap 

15import functools 

16import itertools 

17import posixpath 

18import collections 

19 

20from . import _meta 

21from .compat import py39, py311 

22from ._collections import FreezableDefaultDict, Pair 

23from ._compat import ( 

24 NullFinder, 

25 install, 

26) 

27from ._functools import method_cache, pass_none 

28from ._itertools import always_iterable, unique_everseen 

29from ._meta import PackageMetadata, SimplePath 

30 

31from contextlib import suppress 

32from importlib import import_module 

33from importlib.abc import MetaPathFinder 

34from itertools import starmap 

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

36 

37__all__ = [ 

38 'Distribution', 

39 'DistributionFinder', 

40 'PackageMetadata', 

41 'PackageNotFoundError', 

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 attrs = (getattr(self, param) for param in params) 

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

231 

232 def _key(self): 

233 return self.name, self.value, self.group 

234 

235 def __lt__(self, other): 

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

237 

238 def __eq__(self, other): 

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

240 

241 def __setattr__(self, name, value): 

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

243 

244 def __repr__(self): 

245 return ( 

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

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

248 ) 

249 

250 def __hash__(self) -> int: 

251 return hash(self._key()) 

252 

253 

254class EntryPoints(tuple): 

255 """ 

256 An immutable collection of selectable EntryPoint objects. 

257 """ 

258 

259 __slots__ = () 

260 

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

262 """ 

263 Get the EntryPoint in self matching name. 

264 """ 

265 try: 

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

267 except StopIteration: 

268 raise KeyError(name) 

269 

270 def __repr__(self): 

271 """ 

272 Repr with classname and tuple constructor to 

273 signal that we deviate from regular tuple behavior. 

274 """ 

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

276 

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

278 """ 

279 Select entry points from self that match the 

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

281 """ 

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

283 

284 @property 

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

286 """ 

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

288 """ 

289 return {ep.name for ep in self} 

290 

291 @property 

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

293 """ 

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

295 """ 

296 return {ep.group for ep in self} 

297 

298 @classmethod 

299 def _from_text_for(cls, text, dist): 

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

301 

302 @staticmethod 

303 def _from_text(text): 

304 return ( 

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

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

307 ) 

308 

309 

310class PackagePath(pathlib.PurePosixPath): 

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

312 

313 hash: Optional[FileHash] 

314 size: int 

315 dist: Distribution 

316 

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

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

319 

320 def read_binary(self) -> bytes: 

321 return self.locate().read_bytes() 

322 

323 def locate(self) -> SimplePath: 

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

325 return self.dist.locate_file(self) 

326 

327 

328class FileHash: 

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

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

331 

332 def __repr__(self) -> str: 

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

334 

335 

336class Distribution(metaclass=abc.ABCMeta): 

337 """ 

338 An abstract Python distribution package. 

339 

340 Custom providers may derive from this class and define 

341 the abstract methods to provide a concrete implementation 

342 for their environment. Some providers may opt to override 

343 the default implementation of some properties to bypass 

344 the file-reading mechanism. 

345 """ 

346 

347 @abc.abstractmethod 

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

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

350 

351 Python distribution metadata is organized by blobs of text 

352 typically represented as "files" in the metadata directory 

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

354 like: 

355 

356 - METADATA: The distribution metadata including fields 

357 like Name and Version and Description. 

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

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

360 - RECORD: A record of files according to 

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

362 

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

364 not listed here or none at all. 

365 

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

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

368 """ 

369 

370 @abc.abstractmethod 

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

372 """ 

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

374 to it. 

375 """ 

376 

377 @classmethod 

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

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

380 

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

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

383 package, if found. 

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

385 metadata cannot be found. 

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

387 """ 

388 if not name: 

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

390 try: 

391 return next(iter(cls.discover(name=name))) 

392 except StopIteration: 

393 raise PackageNotFoundError(name) 

394 

395 @classmethod 

396 def discover( 

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

398 ) -> Iterable[Distribution]: 

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

400 

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

402 a context. 

403 

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

405 :return: Iterable of Distribution objects for packages matching 

406 the context. 

407 """ 

408 if context and kwargs: 

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

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

411 return itertools.chain.from_iterable( 

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

413 ) 

414 

415 @staticmethod 

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

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

418 

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

420 :return: a concrete Distribution instance for the path 

421 """ 

422 return PathDistribution(pathlib.Path(path)) 

423 

424 @staticmethod 

425 def _discover_resolvers(): 

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

427 declared = ( 

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

429 ) 

430 return filter(None, declared) 

431 

432 @property 

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

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

435 

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

437 metadata per the 

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

439 

440 Custom providers may provide the METADATA file or override this 

441 property. 

442 """ 

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

444 from . import _adapters 

445 

446 opt_text = ( 

447 self.read_text('METADATA') 

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

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

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

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

452 or self.read_text('') 

453 ) 

454 text = cast(str, opt_text) 

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

456 

457 @property 

458 def name(self) -> str: 

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

460 return self.metadata['Name'] 

461 

462 @property 

463 def _normalized_name(self): 

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

465 return Prepared.normalize(self.name) 

466 

467 @property 

468 def version(self) -> str: 

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

470 return self.metadata['Version'] 

471 

472 @property 

473 def entry_points(self) -> EntryPoints: 

474 """ 

475 Return EntryPoints for this distribution. 

476 

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

478 or override this property. 

479 """ 

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

481 

482 @property 

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

484 """Files in this distribution. 

485 

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

487 

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

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

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

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

492 

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

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

495 able to resolve filenames provided by the package. 

496 """ 

497 

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

499 result = PackagePath(name) 

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

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

502 result.dist = self 

503 return result 

504 

505 @pass_none 

506 def make_files(lines): 

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

508 # as other parts of importlib.metadata 

509 import csv 

510 

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

512 

513 @pass_none 

514 def skip_missing_files(package_paths): 

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

516 

517 return skip_missing_files( 

518 make_files( 

519 self._read_files_distinfo() 

520 or self._read_files_egginfo_installed() 

521 or self._read_files_egginfo_sources() 

522 ) 

523 ) 

524 

525 def _read_files_distinfo(self): 

526 """ 

527 Read the lines of RECORD. 

528 """ 

529 text = self.read_text('RECORD') 

530 return text and text.splitlines() 

531 

532 def _read_files_egginfo_installed(self): 

533 """ 

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

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

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

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

538 

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

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

541 Assume the file is accurate if it exists. 

542 """ 

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

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

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

546 # self._path. 

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

548 if not text or not subdir: 

549 return 

550 

551 paths = ( 

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

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

554 .as_posix() 

555 for name in text.splitlines() 

556 ) 

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

558 

559 def _read_files_egginfo_sources(self): 

560 """ 

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

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

563 might contain literal commas). 

564 

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

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

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

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

569 that are present after the package has been installed. 

570 """ 

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

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

573 

574 @property 

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

576 """Generated requirements specified for this Distribution""" 

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

578 return reqs and list(reqs) 

579 

580 def _read_dist_info_reqs(self): 

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

582 

583 def _read_egg_info_reqs(self): 

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

585 return pass_none(self._deps_from_requires_text)(source) 

586 

587 @classmethod 

588 def _deps_from_requires_text(cls, source): 

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

590 

591 @staticmethod 

592 def _convert_egg_info_reqs_to_simple_reqs(sections): 

593 """ 

594 Historically, setuptools would solicit and store 'extra' 

595 requirements, including those with environment markers, 

596 in separate sections. More modern tools expect each 

597 dependency to be defined separately, with any relevant 

598 extras and environment markers attached directly to that 

599 requirement. This method converts the former to the 

600 latter. See _test_deps_from_requires_text for an example. 

601 """ 

602 

603 def make_condition(name): 

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

605 

606 def quoted_marker(section): 

607 section = section or '' 

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

609 if extra and markers: 

610 markers = f'({markers})' 

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

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

613 

614 def url_req_space(req): 

615 """ 

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

617 Ref python/importlib_metadata#357. 

618 """ 

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

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

621 

622 for section in sections: 

623 space = url_req_space(section.value) 

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

625 

626 @property 

627 def origin(self): 

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

629 

630 def _load_json(self, filename): 

631 return pass_none(json.loads)( 

632 self.read_text(filename), 

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

634 ) 

635 

636 

637class DistributionFinder(MetaPathFinder): 

638 """ 

639 A MetaPathFinder capable of discovering installed distributions. 

640 

641 Custom providers should implement this interface in order to 

642 supply metadata. 

643 """ 

644 

645 class Context: 

646 """ 

647 Keyword arguments presented by the caller to 

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

649 to narrow the scope of a search for distributions 

650 in all DistributionFinders. 

651 

652 Each DistributionFinder may expect any parameters 

653 and should attempt to honor the canonical 

654 parameters defined below when appropriate. 

655 

656 This mechanism gives a custom provider a means to 

657 solicit additional details from the caller beyond 

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

659 For example, imagine a provider that exposes suites 

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

661 A caller may wish to query only for distributions in 

662 a particular realm and could call 

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

664 custom provider to only include distributions from that 

665 realm. 

666 """ 

667 

668 name = None 

669 """ 

670 Specific name for which a distribution finder should match. 

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

672 """ 

673 

674 def __init__(self, **kwargs): 

675 vars(self).update(kwargs) 

676 

677 @property 

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

679 """ 

680 The sequence of directory path that a distribution finder 

681 should search. 

682 

683 Typically refers to Python installed package paths such as 

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

685 """ 

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

687 

688 @abc.abstractmethod 

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

690 """ 

691 Find distributions. 

692 

693 Return an iterable of all Distribution instances capable of 

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

695 a DistributionFinder.Context instance. 

696 """ 

697 

698 

699class FastPath: 

700 """ 

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

702 

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

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

705 

706 >>> FastPath('').children() 

707 ['...'] 

708 

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

710 

711 >>> FastPath('foobar') is FastPath('foobar') 

712 True 

713 """ 

714 

715 @functools.lru_cache() # type: ignore 

716 def __new__(cls, root): 

717 return super().__new__(cls) 

718 

719 def __init__(self, root): 

720 self.root = root 

721 

722 def joinpath(self, child): 

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

724 

725 def children(self): 

726 with suppress(Exception): 

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

728 with suppress(Exception): 

729 return self.zip_children() 

730 return [] 

731 

732 def zip_children(self): 

733 zip_path = zipp.Path(self.root) 

734 names = zip_path.root.namelist() 

735 self.joinpath = zip_path.joinpath 

736 

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

738 

739 def search(self, name): 

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

741 

742 @property 

743 def mtime(self): 

744 with suppress(OSError): 

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

746 self.lookup.cache_clear() 

747 

748 @method_cache 

749 def lookup(self, mtime): 

750 return Lookup(self) 

751 

752 

753class Lookup: 

754 """ 

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

756 """ 

757 

758 def __init__(self, path: FastPath): 

759 """ 

760 Calculate all of the children representing metadata. 

761 

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

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

764 metadata (eggs). 

765 """ 

766 

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

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

769 self.infos = FreezableDefaultDict(list) 

770 self.eggs = FreezableDefaultDict(list) 

771 

772 for child in path.children(): 

773 low = child.lower() 

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

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

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

777 normalized = Prepared.normalize(name) 

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

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

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

781 legacy_normalized = Prepared.legacy_normalize(name) 

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

783 

784 self.infos.freeze() 

785 self.eggs.freeze() 

786 

787 def search(self, prepared: Prepared): 

788 """ 

789 Yield all infos and eggs matching the Prepared query. 

790 """ 

791 infos = ( 

792 self.infos[prepared.normalized] 

793 if prepared 

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

795 ) 

796 eggs = ( 

797 self.eggs[prepared.legacy_normalized] 

798 if prepared 

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

800 ) 

801 return itertools.chain(infos, eggs) 

802 

803 

804class Prepared: 

805 """ 

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

807 

808 Pre-calculates the normalization to prevent repeated operations. 

809 

810 >>> none = Prepared(None) 

811 >>> none.normalized 

812 >>> none.legacy_normalized 

813 >>> bool(none) 

814 False 

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

816 >>> sample.normalized 

817 'sample_pkg_name_foo' 

818 >>> sample.legacy_normalized 

819 'sample__pkg_name.foo' 

820 >>> bool(sample) 

821 True 

822 """ 

823 

824 normalized = None 

825 legacy_normalized = None 

826 

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

828 self.name = name 

829 if name is None: 

830 return 

831 self.normalized = self.normalize(name) 

832 self.legacy_normalized = self.legacy_normalize(name) 

833 

834 @staticmethod 

835 def normalize(name): 

836 """ 

837 PEP 503 normalization plus dashes as underscores. 

838 """ 

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

840 

841 @staticmethod 

842 def legacy_normalize(name): 

843 """ 

844 Normalize the package name as found in the convention in 

845 older packaging tools versions and specs. 

846 """ 

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

848 

849 def __bool__(self): 

850 return bool(self.name) 

851 

852 

853@install 

854class MetadataPathFinder(NullFinder, DistributionFinder): 

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

856 

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

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

859 """ 

860 

861 @classmethod 

862 def find_distributions( 

863 cls, context=DistributionFinder.Context() 

864 ) -> Iterable[PathDistribution]: 

865 """ 

866 Find distributions. 

867 

868 Return an iterable of all Distribution instances capable of 

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

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

871 of directories ``context.path``. 

872 """ 

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

874 return map(PathDistribution, found) 

875 

876 @classmethod 

877 def _search_paths(cls, name, paths): 

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

879 prepared = Prepared(name) 

880 return itertools.chain.from_iterable( 

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

882 ) 

883 

884 @classmethod 

885 def invalidate_caches(cls) -> None: 

886 FastPath.__new__.cache_clear() 

887 

888 

889class PathDistribution(Distribution): 

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

891 """Construct a distribution. 

892 

893 :param path: SimplePath indicating the metadata directory. 

894 """ 

895 self._path = path 

896 

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

898 with suppress( 

899 FileNotFoundError, 

900 IsADirectoryError, 

901 KeyError, 

902 NotADirectoryError, 

903 PermissionError, 

904 ): 

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

906 

907 return None 

908 

909 read_text.__doc__ = Distribution.read_text.__doc__ 

910 

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

912 return self._path.parent / path 

913 

914 @property 

915 def _normalized_name(self): 

916 """ 

917 Performance optimization: where possible, resolve the 

918 normalized name from the file system path. 

919 """ 

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

921 return ( 

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

923 or super()._normalized_name 

924 ) 

925 

926 @staticmethod 

927 def _name_from_stem(stem): 

928 """ 

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

930 'foo' 

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

932 'CherryPy' 

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

934 'face' 

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

936 """ 

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

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

939 return 

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

941 return name 

942 

943 

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

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

946 

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

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

949 """ 

950 return Distribution.from_name(distribution_name) 

951 

952 

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

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

955 

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

957 """ 

958 return Distribution.discover(**kwargs) 

959 

960 

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

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

963 

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

965 :return: A PackageMetadata containing the parsed metadata. 

966 """ 

967 return Distribution.from_name(distribution_name).metadata 

968 

969 

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

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

972 

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

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

975 "Version" metadata key. 

976 """ 

977 return distribution(distribution_name).version 

978 

979 

980_unique = functools.partial( 

981 unique_everseen, 

982 key=py39.normalized_name, 

983) 

984""" 

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

986""" 

987 

988 

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

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

991 

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

993 result to entry points matching those properties (see 

994 EntryPoints.select()). 

995 

996 :return: EntryPoints for all installed packages. 

997 """ 

998 eps = itertools.chain.from_iterable( 

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

1000 ) 

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

1002 

1003 

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

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

1006 

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

1008 :return: List of files composing the distribution. 

1009 """ 

1010 return distribution(distribution_name).files 

1011 

1012 

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

1014 """ 

1015 Return a list of requirements for the named package. 

1016 

1017 :return: An iterable of requirements, suitable for 

1018 packaging.requirement.Requirement. 

1019 """ 

1020 return distribution(distribution_name).requires 

1021 

1022 

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

1024 """ 

1025 Return a mapping of top-level packages to their 

1026 distributions. 

1027 

1028 >>> import collections.abc 

1029 >>> pkgs = packages_distributions() 

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

1031 True 

1032 """ 

1033 pkg_to_dist = collections.defaultdict(list) 

1034 for dist in distributions(): 

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

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

1037 return dict(pkg_to_dist) 

1038 

1039 

1040def _top_level_declared(dist): 

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

1042 

1043 

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

1045 """ 

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

1047 """ 

1048 top, *rest = name.parts 

1049 return top if rest else None 

1050 

1051 

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

1053 """ 

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

1055 sys.path. 

1056 

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

1058 'foo' 

1059 >>> _get_toplevel_name(PackagePath('foo')) 

1060 'foo' 

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

1062 'foo' 

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

1064 'foo' 

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

1066 'foo.pth' 

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

1068 'foo.dist-info' 

1069 """ 

1070 return _topmost(name) or ( 

1071 # python/typeshed#10328 

1072 inspect.getmodulename(name) # type: ignore 

1073 or str(name) 

1074 ) 

1075 

1076 

1077def _top_level_inferred(dist): 

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

1079 

1080 def importable_name(name): 

1081 return '.' not in name 

1082 

1083 return filter(importable_name, opt_names)