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

383 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-04-22 06:15 +0000

1import os 

2import re 

3import abc 

4import csv 

5import sys 

6import zipp 

7import email 

8import pathlib 

9import operator 

10import textwrap 

11import warnings 

12import functools 

13import itertools 

14import posixpath 

15import contextlib 

16import collections 

17import inspect 

18 

19from . import _adapters, _meta, _py39compat 

20from ._collections import FreezableDefaultDict, Pair 

21from ._compat import ( 

22 NullFinder, 

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 List, Mapping, Optional, cast 

35 

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): 

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

58 

59 @property 

60 def name(self): 

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): 

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, value, group): 

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): 

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

217 return match.group('module') 

218 

219 @property 

220 def attr(self): 

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

222 return match.group('attr') 

223 

224 @property 

225 def extras(self): 

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

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

228 

229 def _for(self, dist): 

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

231 return self 

232 

233 def matches(self, **params): 

234 """ 

235 EntryPoint matches the given parameters. 

236 

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

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

239 True 

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

241 True 

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

243 False 

244 >>> ep.matches() 

245 True 

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

247 True 

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

249 True 

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

251 True 

252 """ 

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

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

255 

256 def _key(self): 

257 return self.name, self.value, self.group 

258 

259 def __lt__(self, other): 

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

261 

262 def __eq__(self, other): 

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

264 

265 def __setattr__(self, name, value): 

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

267 

268 def __repr__(self): 

269 return ( 

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

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

272 ) 

273 

274 def __hash__(self): 

275 return hash(self._key()) 

276 

277 

278class EntryPoints(tuple): 

279 """ 

280 An immutable collection of selectable EntryPoint objects. 

281 """ 

282 

283 __slots__ = () 

284 

285 def __getitem__(self, name): # -> EntryPoint: 

286 """ 

287 Get the EntryPoint in self matching name. 

288 """ 

289 try: 

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

291 except StopIteration: 

292 raise KeyError(name) 

293 

294 def select(self, **params): 

295 """ 

296 Select entry points from self that match the 

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

298 """ 

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

300 

301 @property 

302 def names(self): 

303 """ 

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

305 """ 

306 return {ep.name for ep in self} 

307 

308 @property 

309 def groups(self): 

310 """ 

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

312 """ 

313 return {ep.group for ep in self} 

314 

315 @classmethod 

316 def _from_text_for(cls, text, dist): 

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

318 

319 @staticmethod 

320 def _from_text(text): 

321 return ( 

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

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

324 ) 

325 

326 

327class PackagePath(pathlib.PurePosixPath): 

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

329 

330 def read_text(self, encoding='utf-8'): 

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

332 return stream.read() 

333 

334 def read_binary(self): 

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

336 return stream.read() 

337 

338 def locate(self): 

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

340 return self.dist.locate_file(self) 

341 

342 

343class FileHash: 

344 def __init__(self, spec): 

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

346 

347 def __repr__(self): 

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

349 

350 

351class DeprecatedNonAbstract: 

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

353 all_names = { 

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

355 } 

356 abstract = { 

357 name 

358 for name in all_names 

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

360 } 

361 if abstract: 

362 warnings.warn( 

363 f"Unimplemented abstract methods {abstract}", 

364 DeprecationWarning, 

365 stacklevel=2, 

366 ) 

367 return super().__new__(cls) 

368 

369 

370class Distribution(DeprecatedNonAbstract): 

371 """A Python distribution package.""" 

372 

373 @abc.abstractmethod 

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

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

376 

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

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

379 """ 

380 

381 @abc.abstractmethod 

382 def locate_file(self, path): 

383 """ 

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

385 to it. 

386 """ 

387 

388 @classmethod 

389 def from_name(cls, name: str): 

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

391 

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

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

394 package, if found. 

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

396 metadata cannot be found. 

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

398 """ 

399 if not name: 

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

401 try: 

402 return next(cls.discover(name=name)) 

403 except StopIteration: 

404 raise PackageNotFoundError(name) 

405 

406 @classmethod 

407 def discover(cls, **kwargs): 

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

409 

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

411 a context. 

412 

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

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

415 """ 

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

417 if context and kwargs: 

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

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

420 return itertools.chain.from_iterable( 

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

422 ) 

423 

424 @staticmethod 

425 def at(path): 

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

427 

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

429 :return: a concrete Distribution instance for the path 

430 """ 

431 return PathDistribution(pathlib.Path(path)) 

432 

433 @staticmethod 

434 def _discover_resolvers(): 

435 """Search the meta_path for resolvers.""" 

436 declared = ( 

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

438 ) 

439 return filter(None, declared) 

440 

441 @property 

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

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

444 

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

446 metadata. See PEP 566 for details. 

447 """ 

448 opt_text = ( 

449 self.read_text('METADATA') 

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

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

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

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

454 or self.read_text('') 

455 ) 

456 text = cast(str, opt_text) 

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

458 

459 @property 

460 def name(self): 

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

462 return self.metadata['Name'] 

463 

464 @property 

465 def _normalized_name(self): 

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

467 return Prepared.normalize(self.name) 

468 

469 @property 

470 def version(self): 

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

472 return self.metadata['Version'] 

473 

474 @property 

475 def entry_points(self): 

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

477 

478 @property 

479 def files(self): 

480 """Files in this distribution. 

481 

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

483 

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

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

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

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

488 """ 

489 

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

491 result = PackagePath(name) 

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

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

494 result.dist = self 

495 return result 

496 

497 @pass_none 

498 def make_files(lines): 

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

500 

501 @pass_none 

502 def skip_missing_files(package_paths): 

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

504 

505 return skip_missing_files( 

506 make_files( 

507 self._read_files_distinfo() 

508 or self._read_files_egginfo_installed() 

509 or self._read_files_egginfo_sources() 

510 ) 

511 ) 

512 

513 def _read_files_distinfo(self): 

514 """ 

515 Read the lines of RECORD 

516 """ 

517 text = self.read_text('RECORD') 

518 return text and text.splitlines() 

519 

520 def _read_files_egginfo_installed(self): 

521 """ 

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

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

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

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

526 

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

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

529 Assume the file is accurate if it exists. 

530 """ 

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

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

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

534 # self._path. 

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

536 if not text or not subdir: 

537 return 

538 

539 paths = ( 

540 (subdir / name) 

541 .resolve() 

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

543 .as_posix() 

544 for name in text.splitlines() 

545 ) 

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

547 

548 def _read_files_egginfo_sources(self): 

549 """ 

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

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

552 might contain literal commas). 

553 

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

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

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

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

558 that are present after the package has been installed. 

559 """ 

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

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

562 

563 @property 

564 def requires(self): 

565 """Generated requirements specified for this Distribution""" 

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

567 return reqs and list(reqs) 

568 

569 def _read_dist_info_reqs(self): 

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

571 

572 def _read_egg_info_reqs(self): 

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

574 return pass_none(self._deps_from_requires_text)(source) 

575 

576 @classmethod 

577 def _deps_from_requires_text(cls, source): 

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

579 

580 @staticmethod 

581 def _convert_egg_info_reqs_to_simple_reqs(sections): 

582 """ 

583 Historically, setuptools would solicit and store 'extra' 

584 requirements, including those with environment markers, 

585 in separate sections. More modern tools expect each 

586 dependency to be defined separately, with any relevant 

587 extras and environment markers attached directly to that 

588 requirement. This method converts the former to the 

589 latter. See _test_deps_from_requires_text for an example. 

590 """ 

591 

592 def make_condition(name): 

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

594 

595 def quoted_marker(section): 

596 section = section or '' 

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

598 if extra and markers: 

599 markers = f'({markers})' 

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

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

602 

603 def url_req_space(req): 

604 """ 

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

606 Ref python/importlib_metadata#357. 

607 """ 

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

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

610 

611 for section in sections: 

612 space = url_req_space(section.value) 

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

614 

615 

616class DistributionFinder(MetaPathFinder): 

617 """ 

618 A MetaPathFinder capable of discovering installed distributions. 

619 """ 

620 

621 class Context: 

622 """ 

623 Keyword arguments presented by the caller to 

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

625 to narrow the scope of a search for distributions 

626 in all DistributionFinders. 

627 

628 Each DistributionFinder may expect any parameters 

629 and should attempt to honor the canonical 

630 parameters defined below when appropriate. 

631 """ 

632 

633 name = None 

634 """ 

635 Specific name for which a distribution finder should match. 

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

637 """ 

638 

639 def __init__(self, **kwargs): 

640 vars(self).update(kwargs) 

641 

642 @property 

643 def path(self): 

644 """ 

645 The sequence of directory path that a distribution finder 

646 should search. 

647 

648 Typically refers to Python installed package paths such as 

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

650 """ 

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

652 

653 @abc.abstractmethod 

654 def find_distributions(self, context=Context()): 

655 """ 

656 Find distributions. 

657 

658 Return an iterable of all Distribution instances capable of 

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

660 a DistributionFinder.Context instance. 

661 """ 

662 

663 

664class FastPath: 

665 """ 

666 Micro-optimized class for searching a path for 

667 children. 

668 

669 >>> FastPath('').children() 

670 ['...'] 

671 """ 

672 

673 @functools.lru_cache() # type: ignore 

674 def __new__(cls, root): 

675 return super().__new__(cls) 

676 

677 def __init__(self, root): 

678 self.root = root 

679 

680 def joinpath(self, child): 

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

682 

683 def children(self): 

684 with suppress(Exception): 

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

686 with suppress(Exception): 

687 return self.zip_children() 

688 return [] 

689 

690 def zip_children(self): 

691 zip_path = zipp.Path(self.root) 

692 names = zip_path.root.namelist() 

693 self.joinpath = zip_path.joinpath 

694 

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

696 

697 def search(self, name): 

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

699 

700 @property 

701 def mtime(self): 

702 with suppress(OSError): 

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

704 self.lookup.cache_clear() 

705 

706 @method_cache 

707 def lookup(self, mtime): 

708 return Lookup(self) 

709 

710 

711class Lookup: 

712 def __init__(self, path: FastPath): 

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

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

715 self.infos = FreezableDefaultDict(list) 

716 self.eggs = FreezableDefaultDict(list) 

717 

718 for child in path.children(): 

719 low = child.lower() 

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

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

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

723 normalized = Prepared.normalize(name) 

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

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

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

727 legacy_normalized = Prepared.legacy_normalize(name) 

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

729 

730 self.infos.freeze() 

731 self.eggs.freeze() 

732 

733 def search(self, prepared): 

734 infos = ( 

735 self.infos[prepared.normalized] 

736 if prepared 

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

738 ) 

739 eggs = ( 

740 self.eggs[prepared.legacy_normalized] 

741 if prepared 

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

743 ) 

744 return itertools.chain(infos, eggs) 

745 

746 

747class Prepared: 

748 """ 

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

750 """ 

751 

752 normalized = None 

753 legacy_normalized = None 

754 

755 def __init__(self, name): 

756 self.name = name 

757 if name is None: 

758 return 

759 self.normalized = self.normalize(name) 

760 self.legacy_normalized = self.legacy_normalize(name) 

761 

762 @staticmethod 

763 def normalize(name): 

764 """ 

765 PEP 503 normalization plus dashes as underscores. 

766 """ 

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

768 

769 @staticmethod 

770 def legacy_normalize(name): 

771 """ 

772 Normalize the package name as found in the convention in 

773 older packaging tools versions and specs. 

774 """ 

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

776 

777 def __bool__(self): 

778 return bool(self.name) 

779 

780 

781@install 

782class MetadataPathFinder(NullFinder, DistributionFinder): 

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

784 

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

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

787 """ 

788 

789 def find_distributions(self, context=DistributionFinder.Context()): 

790 """ 

791 Find distributions. 

792 

793 Return an iterable of all Distribution instances capable of 

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

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

796 of directories ``context.path``. 

797 """ 

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

799 return map(PathDistribution, found) 

800 

801 @classmethod 

802 def _search_paths(cls, name, paths): 

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

804 prepared = Prepared(name) 

805 return itertools.chain.from_iterable( 

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

807 ) 

808 

809 def invalidate_caches(cls): 

810 FastPath.__new__.cache_clear() 

811 

812 

813class PathDistribution(Distribution): 

814 def __init__(self, path: SimplePath): 

815 """Construct a distribution. 

816 

817 :param path: SimplePath indicating the metadata directory. 

818 """ 

819 self._path = path 

820 

821 def read_text(self, filename): 

822 with suppress( 

823 FileNotFoundError, 

824 IsADirectoryError, 

825 KeyError, 

826 NotADirectoryError, 

827 PermissionError, 

828 ): 

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

830 

831 read_text.__doc__ = Distribution.read_text.__doc__ 

832 

833 def locate_file(self, path): 

834 return self._path.parent / path 

835 

836 @property 

837 def _normalized_name(self): 

838 """ 

839 Performance optimization: where possible, resolve the 

840 normalized name from the file system path. 

841 """ 

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

843 return ( 

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

845 or super()._normalized_name 

846 ) 

847 

848 @staticmethod 

849 def _name_from_stem(stem): 

850 """ 

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

852 'foo' 

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

854 'CherryPy' 

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

856 'face' 

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

858 """ 

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

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

861 return 

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

863 return name 

864 

865 

866def distribution(distribution_name): 

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

868 

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

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

871 """ 

872 return Distribution.from_name(distribution_name) 

873 

874 

875def distributions(**kwargs): 

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

877 

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

879 """ 

880 return Distribution.discover(**kwargs) 

881 

882 

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

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

885 

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

887 :return: A PackageMetadata containing the parsed metadata. 

888 """ 

889 return Distribution.from_name(distribution_name).metadata 

890 

891 

892def version(distribution_name): 

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

894 

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

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

897 "Version" metadata key. 

898 """ 

899 return distribution(distribution_name).version 

900 

901 

902_unique = functools.partial( 

903 unique_everseen, 

904 key=_py39compat.normalized_name, 

905) 

906""" 

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

908""" 

909 

910 

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

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

913 

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

915 result to entry points matching those properties (see 

916 EntryPoints.select()). 

917 

918 :return: EntryPoints for all installed packages. 

919 """ 

920 eps = itertools.chain.from_iterable( 

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

922 ) 

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

924 

925 

926def files(distribution_name): 

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

928 

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

930 :return: List of files composing the distribution. 

931 """ 

932 return distribution(distribution_name).files 

933 

934 

935def requires(distribution_name): 

936 """ 

937 Return a list of requirements for the named package. 

938 

939 :return: An iterator of requirements, suitable for 

940 packaging.requirement.Requirement. 

941 """ 

942 return distribution(distribution_name).requires 

943 

944 

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

946 """ 

947 Return a mapping of top-level packages to their 

948 distributions. 

949 

950 >>> import collections.abc 

951 >>> pkgs = packages_distributions() 

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

953 True 

954 """ 

955 pkg_to_dist = collections.defaultdict(list) 

956 for dist in distributions(): 

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

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

959 return dict(pkg_to_dist) 

960 

961 

962def _top_level_declared(dist): 

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

964 

965 

966def _top_level_inferred(dist): 

967 opt_names = { 

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

969 for f in always_iterable(dist.files) 

970 } 

971 

972 @pass_none 

973 def importable_name(name): 

974 return '.' not in name 

975 

976 return filter(importable_name, opt_names)