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

393 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-01 06:54 +0000

1import os 

2import re 

3import abc 

4import csv 

5import sys 

6import zipp 

7import email 

8import inspect 

9import pathlib 

10import operator 

11import textwrap 

12import warnings 

13import functools 

14import itertools 

15import posixpath 

16import collections 

17 

18from . import _adapters, _meta, _py39compat 

19from ._collections import FreezableDefaultDict, Pair 

20from ._compat import ( 

21 NullFinder, 

22 StrPath, 

23 install, 

24 pypy_partial, 

25) 

26from ._functools import method_cache, pass_none 

27from ._itertools import always_iterable, 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 Iterable, List, Mapping, Optional, Set, cast 

35 

36__all__ = [ 

37 'Distribution', 

38 'DistributionFinder', 

39 'PackageMetadata', 

40 'PackageNotFoundError', 

41 'distribution', 

42 'distributions', 

43 'entry_points', 

44 'files', 

45 'metadata', 

46 'packages_distributions', 

47 'requires', 

48 'version', 

49] 

50 

51 

52class PackageNotFoundError(ModuleNotFoundError): 

53 """The package was not found.""" 

54 

55 def __str__(self) -> str: 

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

57 

58 @property 

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

60 (name,) = self.args 

61 return name 

62 

63 

64class Sectioned: 

65 """ 

66 A simple entry point config parser for performance 

67 

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

69 ... print(item) 

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

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

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

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

74 

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

76 >>> item = next(res) 

77 >>> item.name 

78 'sec1' 

79 >>> item.value 

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

81 >>> item = next(res) 

82 >>> item.value 

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

84 >>> item = next(res) 

85 >>> item.name 

86 'sec2' 

87 >>> item.value 

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

89 >>> list(res) 

90 [] 

91 """ 

92 

93 _sample = textwrap.dedent( 

94 """ 

95 [sec1] 

96 # comments ignored 

97 a = 1 

98 b = 2 

99 

100 [sec2] 

101 a = 2 

102 """ 

103 ).lstrip() 

104 

105 @classmethod 

106 def section_pairs(cls, text): 

107 return ( 

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

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

110 if section.name is not None 

111 ) 

112 

113 @staticmethod 

114 def read(text, filter_=None): 

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

116 name = None 

117 for value in lines: 

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

119 if section_match: 

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

121 continue 

122 yield Pair(name, value) 

123 

124 @staticmethod 

125 def valid(line: str): 

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

127 

128 

129class DeprecatedTuple: 

130 """ 

131 Provide subscript item access for backward compatibility. 

132 

133 >>> recwarn = getfixture('recwarn') 

134 >>> ep = EntryPoint(name='name', value='value', group='group') 

135 >>> ep[:] 

136 ('name', 'value', 'group') 

137 >>> ep[0] 

138 'name' 

139 >>> len(recwarn) 

140 1 

141 """ 

142 

143 # Do not remove prior to 2023-05-01 or Python 3.13 

144 _warn = functools.partial( 

145 warnings.warn, 

146 "EntryPoint tuple interface is deprecated. Access members by name.", 

147 DeprecationWarning, 

148 stacklevel=pypy_partial(2), 

149 ) 

150 

151 def __getitem__(self, item): 

152 self._warn() 

153 return self._key()[item] 

154 

155 

156class EntryPoint(DeprecatedTuple): 

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

158 

159 See `the packaging docs on entry points 

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

161 for more information. 

162 

163 >>> ep = EntryPoint( 

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

165 >>> ep.module 

166 'package.module' 

167 >>> ep.attr 

168 'attr' 

169 >>> ep.extras 

170 ['extra1', 'extra2'] 

171 """ 

172 

173 pattern = re.compile( 

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

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

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

177 ) 

178 """ 

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

180 which might look like: 

181 

182 - module 

183 - package.module 

184 - package.module:attribute 

185 - package.module:object.attribute 

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

187 

188 Other combinations are possible as well. 

189 

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

191 following the attr, and following any extras. 

192 """ 

193 

194 name: str 

195 value: str 

196 group: str 

197 

198 dist: Optional['Distribution'] = None 

199 

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

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

202 

203 def load(self): 

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

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

206 return the named object. 

207 """ 

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

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

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

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

212 

213 @property 

214 def module(self) -> str: 

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

216 assert match is not None 

217 return match.group('module') 

218 

219 @property 

220 def attr(self) -> str: 

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

222 assert match is not None 

223 return match.group('attr') 

224 

225 @property 

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

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

228 assert match is not None 

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

230 

231 def _for(self, dist): 

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

233 return self 

234 

235 def matches(self, **params): 

236 """ 

237 EntryPoint matches the given parameters. 

238 

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

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

241 True 

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

243 True 

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

245 False 

246 >>> ep.matches() 

247 True 

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

249 True 

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

251 True 

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

253 True 

254 """ 

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

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

257 

258 def _key(self): 

259 return self.name, self.value, self.group 

260 

261 def __lt__(self, other): 

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

263 

264 def __eq__(self, other): 

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

266 

267 def __setattr__(self, name, value): 

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

269 

270 def __repr__(self): 

271 return ( 

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

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

274 ) 

275 

276 def __hash__(self) -> int: 

277 return hash(self._key()) 

278 

279 

280class EntryPoints(tuple): 

281 """ 

282 An immutable collection of selectable EntryPoint objects. 

283 """ 

284 

285 __slots__ = () 

286 

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

288 """ 

289 Get the EntryPoint in self matching name. 

290 """ 

291 try: 

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

293 except StopIteration: 

294 raise KeyError(name) 

295 

296 def select(self, **params): 

297 """ 

298 Select entry points from self that match the 

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

300 """ 

301 return EntryPoints(ep for ep in self if _py39compat.ep_matches(ep, **params)) 

302 

303 @property 

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

305 """ 

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

307 """ 

308 return {ep.name for ep in self} 

309 

310 @property 

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

312 """ 

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

314 """ 

315 return {ep.group for ep in self} 

316 

317 @classmethod 

318 def _from_text_for(cls, text, dist): 

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

320 

321 @staticmethod 

322 def _from_text(text): 

323 return ( 

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

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

326 ) 

327 

328 

329class PackagePath(pathlib.PurePosixPath): 

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

331 

332 hash: Optional["FileHash"] 

333 size: int 

334 dist: "Distribution" 

335 

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

337 with self.locate().open(encoding=encoding) as stream: 

338 return stream.read() 

339 

340 def read_binary(self) -> bytes: 

341 with self.locate().open('rb') as stream: 

342 return stream.read() 

343 

344 def locate(self) -> pathlib.Path: 

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

346 return self.dist.locate_file(self) 

347 

348 

349class FileHash: 

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

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

352 

353 def __repr__(self) -> str: 

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

355 

356 

357class DeprecatedNonAbstract: 

358 def __new__(cls, *args, **kwargs): 

359 all_names = { 

360 name for subclass in inspect.getmro(cls) for name in vars(subclass) 

361 } 

362 abstract = { 

363 name 

364 for name in all_names 

365 if getattr(getattr(cls, name), '__isabstractmethod__', False) 

366 } 

367 if abstract: 

368 warnings.warn( 

369 f"Unimplemented abstract methods {abstract}", 

370 DeprecationWarning, 

371 stacklevel=2, 

372 ) 

373 return super().__new__(cls) 

374 

375 

376class Distribution(DeprecatedNonAbstract): 

377 """A Python distribution package.""" 

378 

379 @abc.abstractmethod 

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

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

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: StrPath) -> pathlib.Path: 

389 """ 

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

391 to it. 

392 """ 

393 

394 @classmethod 

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

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

397 

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

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

400 package, if found. 

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

402 metadata cannot be found. 

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

404 """ 

405 if not name: 

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

407 try: 

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

409 except StopIteration: 

410 raise PackageNotFoundError(name) 

411 

412 @classmethod 

413 def discover(cls, **kwargs) -> Iterable["Distribution"]: 

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

415 

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

417 a context. 

418 

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

420 :return: Iterable of Distribution objects for all packages. 

421 """ 

422 context = kwargs.pop('context', None) 

423 if context and kwargs: 

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

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

426 return itertools.chain.from_iterable( 

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

428 ) 

429 

430 @staticmethod 

431 def at(path: StrPath) -> "Distribution": 

432 """Return a Distribution for the indicated metadata path 

433 

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

435 :return: a concrete Distribution instance for the path 

436 """ 

437 return PathDistribution(pathlib.Path(path)) 

438 

439 @staticmethod 

440 def _discover_resolvers(): 

441 """Search the meta_path for resolvers.""" 

442 declared = ( 

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

444 ) 

445 return filter(None, declared) 

446 

447 @property 

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

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

450 

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

452 metadata. See PEP 566 for details. 

453 """ 

454 opt_text = ( 

455 self.read_text('METADATA') 

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

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

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

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

460 or self.read_text('') 

461 ) 

462 text = cast(str, opt_text) 

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

464 

465 @property 

466 def name(self) -> str: 

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

468 return self.metadata['Name'] 

469 

470 @property 

471 def _normalized_name(self): 

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

473 return Prepared.normalize(self.name) 

474 

475 @property 

476 def version(self) -> str: 

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

478 return self.metadata['Version'] 

479 

480 @property 

481 def entry_points(self) -> EntryPoints: 

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

483 

484 @property 

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

486 """Files in this distribution. 

487 

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

489 

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

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

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

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

494 """ 

495 

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

497 result = PackagePath(name) 

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

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

500 result.dist = self 

501 return result 

502 

503 @pass_none 

504 def make_files(lines): 

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

506 

507 @pass_none 

508 def skip_missing_files(package_paths): 

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

510 

511 return skip_missing_files( 

512 make_files( 

513 self._read_files_distinfo() 

514 or self._read_files_egginfo_installed() 

515 or self._read_files_egginfo_sources() 

516 ) 

517 ) 

518 

519 def _read_files_distinfo(self): 

520 """ 

521 Read the lines of RECORD 

522 """ 

523 text = self.read_text('RECORD') 

524 return text and text.splitlines() 

525 

526 def _read_files_egginfo_installed(self): 

527 """ 

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

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

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

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

532 

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

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

535 Assume the file is accurate if it exists. 

536 """ 

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

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

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

540 # self._path. 

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

542 if not text or not subdir: 

543 return 

544 

545 paths = ( 

546 (subdir / name) 

547 .resolve() 

548 .relative_to(self.locate_file('').resolve()) 

549 .as_posix() 

550 for name in text.splitlines() 

551 ) 

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

553 

554 def _read_files_egginfo_sources(self): 

555 """ 

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

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

558 might contain literal commas). 

559 

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

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

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

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

564 that are present after the package has been installed. 

565 """ 

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

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

568 

569 @property 

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

571 """Generated requirements specified for this Distribution""" 

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

573 return reqs and list(reqs) 

574 

575 def _read_dist_info_reqs(self): 

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

577 

578 def _read_egg_info_reqs(self): 

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

580 return pass_none(self._deps_from_requires_text)(source) 

581 

582 @classmethod 

583 def _deps_from_requires_text(cls, source): 

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

585 

586 @staticmethod 

587 def _convert_egg_info_reqs_to_simple_reqs(sections): 

588 """ 

589 Historically, setuptools would solicit and store 'extra' 

590 requirements, including those with environment markers, 

591 in separate sections. More modern tools expect each 

592 dependency to be defined separately, with any relevant 

593 extras and environment markers attached directly to that 

594 requirement. This method converts the former to the 

595 latter. See _test_deps_from_requires_text for an example. 

596 """ 

597 

598 def make_condition(name): 

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

600 

601 def quoted_marker(section): 

602 section = section or '' 

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

604 if extra and markers: 

605 markers = f'({markers})' 

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

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

608 

609 def url_req_space(req): 

610 """ 

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

612 Ref python/importlib_metadata#357. 

613 """ 

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

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

616 

617 for section in sections: 

618 space = url_req_space(section.value) 

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

620 

621 

622class DistributionFinder(MetaPathFinder): 

623 """ 

624 A MetaPathFinder capable of discovering installed distributions. 

625 """ 

626 

627 class Context: 

628 """ 

629 Keyword arguments presented by the caller to 

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

631 to narrow the scope of a search for distributions 

632 in all DistributionFinders. 

633 

634 Each DistributionFinder may expect any parameters 

635 and should attempt to honor the canonical 

636 parameters defined below when appropriate. 

637 """ 

638 

639 name = None 

640 """ 

641 Specific name for which a distribution finder should match. 

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

643 """ 

644 

645 def __init__(self, **kwargs): 

646 vars(self).update(kwargs) 

647 

648 @property 

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

650 """ 

651 The sequence of directory path that a distribution finder 

652 should search. 

653 

654 Typically refers to Python installed package paths such as 

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

656 """ 

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

658 

659 @abc.abstractmethod 

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

661 """ 

662 Find distributions. 

663 

664 Return an iterable of all Distribution instances capable of 

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

666 a DistributionFinder.Context instance. 

667 """ 

668 

669 

670class FastPath: 

671 """ 

672 Micro-optimized class for searching a path for 

673 children. 

674 

675 >>> FastPath('').children() 

676 ['...'] 

677 """ 

678 

679 @functools.lru_cache() # type: ignore 

680 def __new__(cls, root): 

681 return super().__new__(cls) 

682 

683 def __init__(self, root): 

684 self.root = root 

685 

686 def joinpath(self, child): 

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

688 

689 def children(self): 

690 with suppress(Exception): 

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

692 with suppress(Exception): 

693 return self.zip_children() 

694 return [] 

695 

696 def zip_children(self): 

697 zip_path = zipp.Path(self.root) 

698 names = zip_path.root.namelist() 

699 self.joinpath = zip_path.joinpath 

700 

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

702 

703 def search(self, name): 

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

705 

706 @property 

707 def mtime(self): 

708 with suppress(OSError): 

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

710 self.lookup.cache_clear() 

711 

712 @method_cache 

713 def lookup(self, mtime): 

714 return Lookup(self) 

715 

716 

717class Lookup: 

718 def __init__(self, path: FastPath): 

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

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

721 self.infos = FreezableDefaultDict(list) 

722 self.eggs = FreezableDefaultDict(list) 

723 

724 for child in path.children(): 

725 low = child.lower() 

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

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

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

729 normalized = Prepared.normalize(name) 

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

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

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

733 legacy_normalized = Prepared.legacy_normalize(name) 

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

735 

736 self.infos.freeze() 

737 self.eggs.freeze() 

738 

739 def search(self, prepared): 

740 infos = ( 

741 self.infos[prepared.normalized] 

742 if prepared 

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

744 ) 

745 eggs = ( 

746 self.eggs[prepared.legacy_normalized] 

747 if prepared 

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

749 ) 

750 return itertools.chain(infos, eggs) 

751 

752 

753class Prepared: 

754 """ 

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

756 """ 

757 

758 normalized = None 

759 legacy_normalized = None 

760 

761 def __init__(self, name): 

762 self.name = name 

763 if name is None: 

764 return 

765 self.normalized = self.normalize(name) 

766 self.legacy_normalized = self.legacy_normalize(name) 

767 

768 @staticmethod 

769 def normalize(name): 

770 """ 

771 PEP 503 normalization plus dashes as underscores. 

772 """ 

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

774 

775 @staticmethod 

776 def legacy_normalize(name): 

777 """ 

778 Normalize the package name as found in the convention in 

779 older packaging tools versions and specs. 

780 """ 

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

782 

783 def __bool__(self): 

784 return bool(self.name) 

785 

786 

787@install 

788class MetadataPathFinder(NullFinder, DistributionFinder): 

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

790 

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

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

793 """ 

794 

795 def find_distributions( 

796 self, context=DistributionFinder.Context() 

797 ) -> Iterable["PathDistribution"]: 

798 """ 

799 Find distributions. 

800 

801 Return an iterable of all Distribution instances capable of 

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

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

804 of directories ``context.path``. 

805 """ 

806 found = self._search_paths(context.name, context.path) 

807 return map(PathDistribution, found) 

808 

809 @classmethod 

810 def _search_paths(cls, name, paths): 

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

812 prepared = Prepared(name) 

813 return itertools.chain.from_iterable( 

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

815 ) 

816 

817 def invalidate_caches(cls) -> None: 

818 FastPath.__new__.cache_clear() 

819 

820 

821class PathDistribution(Distribution): 

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

823 """Construct a distribution. 

824 

825 :param path: SimplePath indicating the metadata directory. 

826 """ 

827 self._path = path 

828 

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

830 with suppress( 

831 FileNotFoundError, 

832 IsADirectoryError, 

833 KeyError, 

834 NotADirectoryError, 

835 PermissionError, 

836 ): 

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

838 

839 return None 

840 

841 read_text.__doc__ = Distribution.read_text.__doc__ 

842 

843 def locate_file(self, path: StrPath) -> pathlib.Path: 

844 return self._path.parent / path 

845 

846 @property 

847 def _normalized_name(self): 

848 """ 

849 Performance optimization: where possible, resolve the 

850 normalized name from the file system path. 

851 """ 

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

853 return ( 

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

855 or super()._normalized_name 

856 ) 

857 

858 @staticmethod 

859 def _name_from_stem(stem): 

860 """ 

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

862 'foo' 

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

864 'CherryPy' 

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

866 'face' 

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

868 """ 

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

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

871 return 

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

873 return name 

874 

875 

876def distribution(distribution_name) -> Distribution: 

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

878 

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

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

881 """ 

882 return Distribution.from_name(distribution_name) 

883 

884 

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

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

887 

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

889 """ 

890 return Distribution.discover(**kwargs) 

891 

892 

893def metadata(distribution_name) -> _meta.PackageMetadata: 

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

895 

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

897 :return: A PackageMetadata containing the parsed metadata. 

898 """ 

899 return Distribution.from_name(distribution_name).metadata 

900 

901 

902def version(distribution_name) -> str: 

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

904 

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

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

907 "Version" metadata key. 

908 """ 

909 return distribution(distribution_name).version 

910 

911 

912_unique = functools.partial( 

913 unique_everseen, 

914 key=_py39compat.normalized_name, 

915) 

916""" 

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

918""" 

919 

920 

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

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

923 

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

925 result to entry points matching those properties (see 

926 EntryPoints.select()). 

927 

928 :return: EntryPoints for all installed packages. 

929 """ 

930 eps = itertools.chain.from_iterable( 

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

932 ) 

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

934 

935 

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

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

938 

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

940 :return: List of files composing the distribution. 

941 """ 

942 return distribution(distribution_name).files 

943 

944 

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

946 """ 

947 Return a list of requirements for the named package. 

948 

949 :return: An iterable of requirements, suitable for 

950 packaging.requirement.Requirement. 

951 """ 

952 return distribution(distribution_name).requires 

953 

954 

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

956 """ 

957 Return a mapping of top-level packages to their 

958 distributions. 

959 

960 >>> import collections.abc 

961 >>> pkgs = packages_distributions() 

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

963 True 

964 """ 

965 pkg_to_dist = collections.defaultdict(list) 

966 for dist in distributions(): 

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

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

969 return dict(pkg_to_dist) 

970 

971 

972def _top_level_declared(dist): 

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

974 

975 

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

977 """ 

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

979 """ 

980 top, *rest = name.parts 

981 return top if rest else None 

982 

983 

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

985 """ 

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

987 sys.path. 

988 

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

990 'foo' 

991 >>> _get_toplevel_name(PackagePath('foo')) 

992 'foo' 

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

994 'foo' 

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

996 'foo' 

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

998 'foo.pth' 

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

1000 'foo.dist-info' 

1001 """ 

1002 return _topmost(name) or ( 

1003 # python/typeshed#10328 

1004 inspect.getmodulename(name) # type: ignore 

1005 or str(name) 

1006 ) 

1007 

1008 

1009def _top_level_inferred(dist): 

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

1011 

1012 def importable_name(name): 

1013 return '.' not in name 

1014 

1015 return filter(importable_name, opt_names)