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

390 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-03 06:10 +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 contextlib 

17import collections 

18 

19from . import _adapters, _meta, _py39compat 

20from ._collections import FreezableDefaultDict, Pair 

21from ._compat import ( 

22 NullFinder, 

23 StrPath, 

24 install, 

25 pypy_partial, 

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 Iterable, List, Mapping, 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 DeprecatedTuple: 

131 """ 

132 Provide subscript item access for backward compatibility. 

133 

134 >>> recwarn = getfixture('recwarn') 

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

136 >>> ep[:] 

137 ('name', 'value', 'group') 

138 >>> ep[0] 

139 'name' 

140 >>> len(recwarn) 

141 1 

142 """ 

143 

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

145 _warn = functools.partial( 

146 warnings.warn, 

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

148 DeprecationWarning, 

149 stacklevel=pypy_partial(2), 

150 ) 

151 

152 def __getitem__(self, item): 

153 self._warn() 

154 return self._key()[item] 

155 

156 

157class EntryPoint(DeprecatedTuple): 

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

159 

160 See `the packaging docs on entry points 

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

162 for more information. 

163 

164 >>> ep = EntryPoint( 

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

166 >>> ep.module 

167 'package.module' 

168 >>> ep.attr 

169 'attr' 

170 >>> ep.extras 

171 ['extra1', 'extra2'] 

172 """ 

173 

174 pattern = re.compile( 

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

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

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

178 ) 

179 """ 

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

181 which might look like: 

182 

183 - module 

184 - package.module 

185 - package.module:attribute 

186 - package.module:object.attribute 

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

188 

189 Other combinations are possible as well. 

190 

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

192 following the attr, and following any extras. 

193 """ 

194 

195 name: str 

196 value: str 

197 group: str 

198 

199 dist: Optional['Distribution'] = None 

200 

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

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

203 

204 def load(self): 

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

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

207 return the named object. 

208 """ 

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

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

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

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

213 

214 @property 

215 def module(self) -> str: 

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

217 assert match is not None 

218 return match.group('module') 

219 

220 @property 

221 def attr(self) -> str: 

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

223 assert match is not None 

224 return match.group('attr') 

225 

226 @property 

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

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

229 assert match is not None 

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

231 

232 def _for(self, dist): 

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

234 return self 

235 

236 def matches(self, **params): 

237 """ 

238 EntryPoint matches the given parameters. 

239 

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

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

242 True 

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

244 True 

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

246 False 

247 >>> ep.matches() 

248 True 

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

250 True 

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

252 True 

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

254 True 

255 """ 

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

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

258 

259 def _key(self): 

260 return self.name, self.value, self.group 

261 

262 def __lt__(self, other): 

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

264 

265 def __eq__(self, other): 

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

267 

268 def __setattr__(self, name, value): 

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

270 

271 def __repr__(self): 

272 return ( 

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

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

275 ) 

276 

277 def __hash__(self) -> int: 

278 return hash(self._key()) 

279 

280 

281class EntryPoints(tuple): 

282 """ 

283 An immutable collection of selectable EntryPoint objects. 

284 """ 

285 

286 __slots__ = () 

287 

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

289 """ 

290 Get the EntryPoint in self matching name. 

291 """ 

292 try: 

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

294 except StopIteration: 

295 raise KeyError(name) 

296 

297 def select(self, **params): 

298 """ 

299 Select entry points from self that match the 

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

301 """ 

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

303 

304 @property 

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

306 """ 

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

308 """ 

309 return {ep.name for ep in self} 

310 

311 @property 

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

313 """ 

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

315 """ 

316 return {ep.group for ep in self} 

317 

318 @classmethod 

319 def _from_text_for(cls, text, dist): 

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

321 

322 @staticmethod 

323 def _from_text(text): 

324 return ( 

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

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

327 ) 

328 

329 

330class PackagePath(pathlib.PurePosixPath): 

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

332 

333 hash: Optional["FileHash"] 

334 size: int 

335 dist: "Distribution" 

336 

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

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

339 return stream.read() 

340 

341 def read_binary(self) -> bytes: 

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

343 return stream.read() 

344 

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

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

347 return self.dist.locate_file(self) 

348 

349 

350class FileHash: 

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

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

353 

354 def __repr__(self) -> str: 

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

356 

357 

358class DeprecatedNonAbstract: 

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

360 all_names = { 

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

362 } 

363 abstract = { 

364 name 

365 for name in all_names 

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

367 } 

368 if abstract: 

369 warnings.warn( 

370 f"Unimplemented abstract methods {abstract}", 

371 DeprecationWarning, 

372 stacklevel=2, 

373 ) 

374 return super().__new__(cls) 

375 

376 

377class Distribution(DeprecatedNonAbstract): 

378 """A Python distribution package.""" 

379 

380 @abc.abstractmethod 

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

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

383 

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

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

386 """ 

387 

388 @abc.abstractmethod 

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

390 """ 

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

392 to it. 

393 """ 

394 

395 @classmethod 

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

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

398 

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

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

401 package, if found. 

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

403 metadata cannot be found. 

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

405 """ 

406 if not name: 

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

408 try: 

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

410 except StopIteration: 

411 raise PackageNotFoundError(name) 

412 

413 @classmethod 

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

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

416 

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

418 a context. 

419 

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

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

422 """ 

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

424 if context and kwargs: 

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

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

427 return itertools.chain.from_iterable( 

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

429 ) 

430 

431 @staticmethod 

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

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

434 

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

436 :return: a concrete Distribution instance for the path 

437 """ 

438 return PathDistribution(pathlib.Path(path)) 

439 

440 @staticmethod 

441 def _discover_resolvers(): 

442 """Search the meta_path for resolvers.""" 

443 declared = ( 

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

445 ) 

446 return filter(None, declared) 

447 

448 @property 

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

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

451 

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

453 metadata. See PEP 566 for details. 

454 """ 

455 opt_text = ( 

456 self.read_text('METADATA') 

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

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

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

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

461 or self.read_text('') 

462 ) 

463 text = cast(str, opt_text) 

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

465 

466 @property 

467 def name(self) -> str: 

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

469 return self.metadata['Name'] 

470 

471 @property 

472 def _normalized_name(self): 

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

474 return Prepared.normalize(self.name) 

475 

476 @property 

477 def version(self) -> str: 

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

479 return self.metadata['Version'] 

480 

481 @property 

482 def entry_points(self) -> EntryPoints: 

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

484 

485 @property 

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

487 """Files in this distribution. 

488 

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

490 

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

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

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

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

495 """ 

496 

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

498 result = PackagePath(name) 

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

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

501 result.dist = self 

502 return result 

503 

504 @pass_none 

505 def make_files(lines): 

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

507 

508 @pass_none 

509 def skip_missing_files(package_paths): 

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

511 

512 return skip_missing_files( 

513 make_files( 

514 self._read_files_distinfo() 

515 or self._read_files_egginfo_installed() 

516 or self._read_files_egginfo_sources() 

517 ) 

518 ) 

519 

520 def _read_files_distinfo(self): 

521 """ 

522 Read the lines of RECORD 

523 """ 

524 text = self.read_text('RECORD') 

525 return text and text.splitlines() 

526 

527 def _read_files_egginfo_installed(self): 

528 """ 

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

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

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

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

533 

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

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

536 Assume the file is accurate if it exists. 

537 """ 

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

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

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

541 # self._path. 

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

543 if not text or not subdir: 

544 return 

545 

546 paths = ( 

547 (subdir / name) 

548 .resolve() 

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

550 .as_posix() 

551 for name in text.splitlines() 

552 ) 

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

554 

555 def _read_files_egginfo_sources(self): 

556 """ 

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

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

559 might contain literal commas). 

560 

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

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

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

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

565 that are present after the package has been installed. 

566 """ 

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

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

569 

570 @property 

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

572 """Generated requirements specified for this Distribution""" 

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

574 return reqs and list(reqs) 

575 

576 def _read_dist_info_reqs(self): 

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

578 

579 def _read_egg_info_reqs(self): 

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

581 return pass_none(self._deps_from_requires_text)(source) 

582 

583 @classmethod 

584 def _deps_from_requires_text(cls, source): 

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

586 

587 @staticmethod 

588 def _convert_egg_info_reqs_to_simple_reqs(sections): 

589 """ 

590 Historically, setuptools would solicit and store 'extra' 

591 requirements, including those with environment markers, 

592 in separate sections. More modern tools expect each 

593 dependency to be defined separately, with any relevant 

594 extras and environment markers attached directly to that 

595 requirement. This method converts the former to the 

596 latter. See _test_deps_from_requires_text for an example. 

597 """ 

598 

599 def make_condition(name): 

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

601 

602 def quoted_marker(section): 

603 section = section or '' 

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

605 if extra and markers: 

606 markers = f'({markers})' 

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

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

609 

610 def url_req_space(req): 

611 """ 

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

613 Ref python/importlib_metadata#357. 

614 """ 

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

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

617 

618 for section in sections: 

619 space = url_req_space(section.value) 

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

621 

622 

623class DistributionFinder(MetaPathFinder): 

624 """ 

625 A MetaPathFinder capable of discovering installed distributions. 

626 """ 

627 

628 class Context: 

629 """ 

630 Keyword arguments presented by the caller to 

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

632 to narrow the scope of a search for distributions 

633 in all DistributionFinders. 

634 

635 Each DistributionFinder may expect any parameters 

636 and should attempt to honor the canonical 

637 parameters defined below when appropriate. 

638 """ 

639 

640 name = None 

641 """ 

642 Specific name for which a distribution finder should match. 

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

644 """ 

645 

646 def __init__(self, **kwargs): 

647 vars(self).update(kwargs) 

648 

649 @property 

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

651 """ 

652 The sequence of directory path that a distribution finder 

653 should search. 

654 

655 Typically refers to Python installed package paths such as 

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

657 """ 

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

659 

660 @abc.abstractmethod 

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

662 """ 

663 Find distributions. 

664 

665 Return an iterable of all Distribution instances capable of 

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

667 a DistributionFinder.Context instance. 

668 """ 

669 

670 

671class FastPath: 

672 """ 

673 Micro-optimized class for searching a path for 

674 children. 

675 

676 >>> FastPath('').children() 

677 ['...'] 

678 """ 

679 

680 @functools.lru_cache() # type: ignore 

681 def __new__(cls, root): 

682 return super().__new__(cls) 

683 

684 def __init__(self, root): 

685 self.root = root 

686 

687 def joinpath(self, child): 

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

689 

690 def children(self): 

691 with suppress(Exception): 

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

693 with suppress(Exception): 

694 return self.zip_children() 

695 return [] 

696 

697 def zip_children(self): 

698 zip_path = zipp.Path(self.root) 

699 names = zip_path.root.namelist() 

700 self.joinpath = zip_path.joinpath 

701 

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

703 

704 def search(self, name): 

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

706 

707 @property 

708 def mtime(self): 

709 with suppress(OSError): 

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

711 self.lookup.cache_clear() 

712 

713 @method_cache 

714 def lookup(self, mtime): 

715 return Lookup(self) 

716 

717 

718class Lookup: 

719 def __init__(self, path: FastPath): 

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

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

722 self.infos = FreezableDefaultDict(list) 

723 self.eggs = FreezableDefaultDict(list) 

724 

725 for child in path.children(): 

726 low = child.lower() 

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

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

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

730 normalized = Prepared.normalize(name) 

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

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

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

734 legacy_normalized = Prepared.legacy_normalize(name) 

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

736 

737 self.infos.freeze() 

738 self.eggs.freeze() 

739 

740 def search(self, prepared): 

741 infos = ( 

742 self.infos[prepared.normalized] 

743 if prepared 

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

745 ) 

746 eggs = ( 

747 self.eggs[prepared.legacy_normalized] 

748 if prepared 

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

750 ) 

751 return itertools.chain(infos, eggs) 

752 

753 

754class Prepared: 

755 """ 

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

757 """ 

758 

759 normalized = None 

760 legacy_normalized = None 

761 

762 def __init__(self, name): 

763 self.name = name 

764 if name is None: 

765 return 

766 self.normalized = self.normalize(name) 

767 self.legacy_normalized = self.legacy_normalize(name) 

768 

769 @staticmethod 

770 def normalize(name): 

771 """ 

772 PEP 503 normalization plus dashes as underscores. 

773 """ 

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

775 

776 @staticmethod 

777 def legacy_normalize(name): 

778 """ 

779 Normalize the package name as found in the convention in 

780 older packaging tools versions and specs. 

781 """ 

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

783 

784 def __bool__(self): 

785 return bool(self.name) 

786 

787 

788@install 

789class MetadataPathFinder(NullFinder, DistributionFinder): 

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

791 

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

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

794 """ 

795 

796 def find_distributions( 

797 self, context=DistributionFinder.Context() 

798 ) -> Iterable["PathDistribution"]: 

799 """ 

800 Find distributions. 

801 

802 Return an iterable of all Distribution instances capable of 

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

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

805 of directories ``context.path``. 

806 """ 

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

808 return map(PathDistribution, found) 

809 

810 @classmethod 

811 def _search_paths(cls, name, paths): 

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

813 prepared = Prepared(name) 

814 return itertools.chain.from_iterable( 

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

816 ) 

817 

818 def invalidate_caches(cls) -> None: 

819 FastPath.__new__.cache_clear() 

820 

821 

822class PathDistribution(Distribution): 

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

824 """Construct a distribution. 

825 

826 :param path: SimplePath indicating the metadata directory. 

827 """ 

828 self._path = path 

829 

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

831 with suppress( 

832 FileNotFoundError, 

833 IsADirectoryError, 

834 KeyError, 

835 NotADirectoryError, 

836 PermissionError, 

837 ): 

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

839 

840 return None 

841 

842 read_text.__doc__ = Distribution.read_text.__doc__ 

843 

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

845 return self._path.parent / path 

846 

847 @property 

848 def _normalized_name(self): 

849 """ 

850 Performance optimization: where possible, resolve the 

851 normalized name from the file system path. 

852 """ 

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

854 return ( 

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

856 or super()._normalized_name 

857 ) 

858 

859 @staticmethod 

860 def _name_from_stem(stem): 

861 """ 

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

863 'foo' 

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

865 'CherryPy' 

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

867 'face' 

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

869 """ 

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

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

872 return 

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

874 return name 

875 

876 

877def distribution(distribution_name) -> Distribution: 

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

879 

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

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

882 """ 

883 return Distribution.from_name(distribution_name) 

884 

885 

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

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

888 

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

890 """ 

891 return Distribution.discover(**kwargs) 

892 

893 

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

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

896 

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

898 :return: A PackageMetadata containing the parsed metadata. 

899 """ 

900 return Distribution.from_name(distribution_name).metadata 

901 

902 

903def version(distribution_name) -> str: 

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

905 

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

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

908 "Version" metadata key. 

909 """ 

910 return distribution(distribution_name).version 

911 

912 

913_unique = functools.partial( 

914 unique_everseen, 

915 key=_py39compat.normalized_name, 

916) 

917""" 

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

919""" 

920 

921 

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

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

924 

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

926 result to entry points matching those properties (see 

927 EntryPoints.select()). 

928 

929 :return: EntryPoints for all installed packages. 

930 """ 

931 eps = itertools.chain.from_iterable( 

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

933 ) 

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

935 

936 

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

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

939 

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

941 :return: List of files composing the distribution. 

942 """ 

943 return distribution(distribution_name).files 

944 

945 

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

947 """ 

948 Return a list of requirements for the named package. 

949 

950 :return: An iterable of requirements, suitable for 

951 packaging.requirement.Requirement. 

952 """ 

953 return distribution(distribution_name).requires 

954 

955 

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

957 """ 

958 Return a mapping of top-level packages to their 

959 distributions. 

960 

961 >>> import collections.abc 

962 >>> pkgs = packages_distributions() 

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

964 True 

965 """ 

966 pkg_to_dist = collections.defaultdict(list) 

967 for dist in distributions(): 

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

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

970 return dict(pkg_to_dist) 

971 

972 

973def _top_level_declared(dist): 

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

975 

976 

977def _top_level_inferred(dist): 

978 opt_names = { 

979 f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f) 

980 for f in always_iterable(dist.files) 

981 } 

982 

983 @pass_none 

984 def importable_name(name): 

985 return '.' not in name 

986 

987 return filter(importable_name, opt_names)