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

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

397 statements  

1import os 

2import re 

3import abc 

4import csv 

5import sys 

6import json 

7import zipp 

8import email 

9import types 

10import inspect 

11import pathlib 

12import operator 

13import textwrap 

14import warnings 

15import functools 

16import itertools 

17import posixpath 

18import collections 

19 

20from . import _adapters, _meta, _py39compat 

21from ._collections import FreezableDefaultDict, Pair 

22from ._compat import ( 

23 NullFinder, 

24 StrPath, 

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

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

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 _py39compat.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 with self.locate().open(encoding=encoding) as stream: 

319 return stream.read() 

320 

321 def read_binary(self) -> bytes: 

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

323 return stream.read() 

324 

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

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

327 return self.dist.locate_file(self) 

328 

329 

330class FileHash: 

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

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

333 

334 def __repr__(self) -> str: 

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

336 

337 

338class DeprecatedNonAbstract: 

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

340 all_names = { 

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

342 } 

343 abstract = { 

344 name 

345 for name in all_names 

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

347 } 

348 if abstract: 

349 warnings.warn( 

350 f"Unimplemented abstract methods {abstract}", 

351 DeprecationWarning, 

352 stacklevel=2, 

353 ) 

354 return super().__new__(cls) 

355 

356 

357class Distribution(DeprecatedNonAbstract): 

358 """A Python distribution package.""" 

359 

360 @abc.abstractmethod 

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

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

363 

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

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

366 """ 

367 

368 @abc.abstractmethod 

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

370 """ 

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

372 to it. 

373 """ 

374 

375 @classmethod 

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

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

378 

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

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

381 package, if found. 

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

383 metadata cannot be found. 

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

385 """ 

386 if not name: 

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

388 try: 

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

390 except StopIteration: 

391 raise PackageNotFoundError(name) 

392 

393 @classmethod 

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

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

396 

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

398 a context. 

399 

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

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

402 """ 

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

404 if context and kwargs: 

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

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

407 return itertools.chain.from_iterable( 

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

409 ) 

410 

411 @staticmethod 

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

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

414 

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

416 :return: a concrete Distribution instance for the path 

417 """ 

418 return PathDistribution(pathlib.Path(path)) 

419 

420 @staticmethod 

421 def _discover_resolvers(): 

422 """Search the meta_path for resolvers.""" 

423 declared = ( 

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

425 ) 

426 return filter(None, declared) 

427 

428 @property 

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

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

431 

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

433 metadata. See PEP 566 for details. 

434 """ 

435 opt_text = ( 

436 self.read_text('METADATA') 

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

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

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

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

441 or self.read_text('') 

442 ) 

443 text = cast(str, opt_text) 

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

445 

446 @property 

447 def name(self) -> str: 

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

449 return self.metadata['Name'] 

450 

451 @property 

452 def _normalized_name(self): 

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

454 return Prepared.normalize(self.name) 

455 

456 @property 

457 def version(self) -> str: 

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

459 return self.metadata['Version'] 

460 

461 @property 

462 def entry_points(self) -> EntryPoints: 

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

464 

465 @property 

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

467 """Files in this distribution. 

468 

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

470 

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

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

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

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

475 """ 

476 

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

478 result = PackagePath(name) 

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

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

481 result.dist = self 

482 return result 

483 

484 @pass_none 

485 def make_files(lines): 

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

487 

488 @pass_none 

489 def skip_missing_files(package_paths): 

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

491 

492 return skip_missing_files( 

493 make_files( 

494 self._read_files_distinfo() 

495 or self._read_files_egginfo_installed() 

496 or self._read_files_egginfo_sources() 

497 ) 

498 ) 

499 

500 def _read_files_distinfo(self): 

501 """ 

502 Read the lines of RECORD 

503 """ 

504 text = self.read_text('RECORD') 

505 return text and text.splitlines() 

506 

507 def _read_files_egginfo_installed(self): 

508 """ 

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

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

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

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

513 

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

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

516 Assume the file is accurate if it exists. 

517 """ 

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

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

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

521 # self._path. 

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

523 if not text or not subdir: 

524 return 

525 

526 paths = ( 

527 (subdir / name) 

528 .resolve() 

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

530 .as_posix() 

531 for name in text.splitlines() 

532 ) 

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

534 

535 def _read_files_egginfo_sources(self): 

536 """ 

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

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

539 might contain literal commas). 

540 

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

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

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

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

545 that are present after the package has been installed. 

546 """ 

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

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

549 

550 @property 

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

552 """Generated requirements specified for this Distribution""" 

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

554 return reqs and list(reqs) 

555 

556 def _read_dist_info_reqs(self): 

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

558 

559 def _read_egg_info_reqs(self): 

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

561 return pass_none(self._deps_from_requires_text)(source) 

562 

563 @classmethod 

564 def _deps_from_requires_text(cls, source): 

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

566 

567 @staticmethod 

568 def _convert_egg_info_reqs_to_simple_reqs(sections): 

569 """ 

570 Historically, setuptools would solicit and store 'extra' 

571 requirements, including those with environment markers, 

572 in separate sections. More modern tools expect each 

573 dependency to be defined separately, with any relevant 

574 extras and environment markers attached directly to that 

575 requirement. This method converts the former to the 

576 latter. See _test_deps_from_requires_text for an example. 

577 """ 

578 

579 def make_condition(name): 

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

581 

582 def quoted_marker(section): 

583 section = section or '' 

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

585 if extra and markers: 

586 markers = f'({markers})' 

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

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

589 

590 def url_req_space(req): 

591 """ 

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

593 Ref python/importlib_metadata#357. 

594 """ 

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

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

597 

598 for section in sections: 

599 space = url_req_space(section.value) 

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

601 

602 @property 

603 def origin(self): 

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

605 

606 def _load_json(self, filename): 

607 return pass_none(json.loads)( 

608 self.read_text(filename), 

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

610 ) 

611 

612 

613class DistributionFinder(MetaPathFinder): 

614 """ 

615 A MetaPathFinder capable of discovering installed distributions. 

616 """ 

617 

618 class Context: 

619 """ 

620 Keyword arguments presented by the caller to 

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

622 to narrow the scope of a search for distributions 

623 in all DistributionFinders. 

624 

625 Each DistributionFinder may expect any parameters 

626 and should attempt to honor the canonical 

627 parameters defined below when appropriate. 

628 """ 

629 

630 name = None 

631 """ 

632 Specific name for which a distribution finder should match. 

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

634 """ 

635 

636 def __init__(self, **kwargs): 

637 vars(self).update(kwargs) 

638 

639 @property 

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

641 """ 

642 The sequence of directory path that a distribution finder 

643 should search. 

644 

645 Typically refers to Python installed package paths such as 

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

647 """ 

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

649 

650 @abc.abstractmethod 

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

652 """ 

653 Find distributions. 

654 

655 Return an iterable of all Distribution instances capable of 

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

657 a DistributionFinder.Context instance. 

658 """ 

659 

660 

661class FastPath: 

662 """ 

663 Micro-optimized class for searching a path for 

664 children. 

665 

666 >>> FastPath('').children() 

667 ['...'] 

668 """ 

669 

670 @functools.lru_cache() # type: ignore 

671 def __new__(cls, root): 

672 return super().__new__(cls) 

673 

674 def __init__(self, root): 

675 self.root = root 

676 

677 def joinpath(self, child): 

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

679 

680 def children(self): 

681 with suppress(Exception): 

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

683 with suppress(Exception): 

684 return self.zip_children() 

685 return [] 

686 

687 def zip_children(self): 

688 zip_path = zipp.Path(self.root) 

689 names = zip_path.root.namelist() 

690 self.joinpath = zip_path.joinpath 

691 

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

693 

694 def search(self, name): 

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

696 

697 @property 

698 def mtime(self): 

699 with suppress(OSError): 

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

701 self.lookup.cache_clear() 

702 

703 @method_cache 

704 def lookup(self, mtime): 

705 return Lookup(self) 

706 

707 

708class Lookup: 

709 def __init__(self, path: FastPath): 

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

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

712 self.infos = FreezableDefaultDict(list) 

713 self.eggs = FreezableDefaultDict(list) 

714 

715 for child in path.children(): 

716 low = child.lower() 

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

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

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

720 normalized = Prepared.normalize(name) 

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

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

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

724 legacy_normalized = Prepared.legacy_normalize(name) 

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

726 

727 self.infos.freeze() 

728 self.eggs.freeze() 

729 

730 def search(self, prepared): 

731 infos = ( 

732 self.infos[prepared.normalized] 

733 if prepared 

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

735 ) 

736 eggs = ( 

737 self.eggs[prepared.legacy_normalized] 

738 if prepared 

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

740 ) 

741 return itertools.chain(infos, eggs) 

742 

743 

744class Prepared: 

745 """ 

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

747 """ 

748 

749 normalized = None 

750 legacy_normalized = None 

751 

752 def __init__(self, name): 

753 self.name = name 

754 if name is None: 

755 return 

756 self.normalized = self.normalize(name) 

757 self.legacy_normalized = self.legacy_normalize(name) 

758 

759 @staticmethod 

760 def normalize(name): 

761 """ 

762 PEP 503 normalization plus dashes as underscores. 

763 """ 

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

765 

766 @staticmethod 

767 def legacy_normalize(name): 

768 """ 

769 Normalize the package name as found in the convention in 

770 older packaging tools versions and specs. 

771 """ 

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

773 

774 def __bool__(self): 

775 return bool(self.name) 

776 

777 

778@install 

779class MetadataPathFinder(NullFinder, DistributionFinder): 

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

781 

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

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

784 """ 

785 

786 def find_distributions( 

787 self, context=DistributionFinder.Context() 

788 ) -> Iterable["PathDistribution"]: 

789 """ 

790 Find distributions. 

791 

792 Return an iterable of all Distribution instances capable of 

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

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

795 of directories ``context.path``. 

796 """ 

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

798 return map(PathDistribution, found) 

799 

800 @classmethod 

801 def _search_paths(cls, name, paths): 

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

803 prepared = Prepared(name) 

804 return itertools.chain.from_iterable( 

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

806 ) 

807 

808 def invalidate_caches(cls) -> None: 

809 FastPath.__new__.cache_clear() 

810 

811 

812class PathDistribution(Distribution): 

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

814 """Construct a distribution. 

815 

816 :param path: SimplePath indicating the metadata directory. 

817 """ 

818 self._path = path 

819 

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

821 with suppress( 

822 FileNotFoundError, 

823 IsADirectoryError, 

824 KeyError, 

825 NotADirectoryError, 

826 PermissionError, 

827 ): 

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

829 

830 return None 

831 

832 read_text.__doc__ = Distribution.read_text.__doc__ 

833 

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

835 return self._path.parent / path 

836 

837 @property 

838 def _normalized_name(self): 

839 """ 

840 Performance optimization: where possible, resolve the 

841 normalized name from the file system path. 

842 """ 

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

844 return ( 

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

846 or super()._normalized_name 

847 ) 

848 

849 @staticmethod 

850 def _name_from_stem(stem): 

851 """ 

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

853 'foo' 

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

855 'CherryPy' 

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

857 'face' 

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

859 """ 

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

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

862 return 

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

864 return name 

865 

866 

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

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

869 

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

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

872 """ 

873 return Distribution.from_name(distribution_name) 

874 

875 

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

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

878 

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

880 """ 

881 return Distribution.discover(**kwargs) 

882 

883 

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

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

886 

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

888 :return: A PackageMetadata containing the parsed metadata. 

889 """ 

890 return Distribution.from_name(distribution_name).metadata 

891 

892 

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

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

895 

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

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

898 "Version" metadata key. 

899 """ 

900 return distribution(distribution_name).version 

901 

902 

903_unique = functools.partial( 

904 unique_everseen, 

905 key=_py39compat.normalized_name, 

906) 

907""" 

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

909""" 

910 

911 

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

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

914 

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

916 result to entry points matching those properties (see 

917 EntryPoints.select()). 

918 

919 :return: EntryPoints for all installed packages. 

920 """ 

921 eps = itertools.chain.from_iterable( 

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

923 ) 

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

925 

926 

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

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

929 

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

931 :return: List of files composing the distribution. 

932 """ 

933 return distribution(distribution_name).files 

934 

935 

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

937 """ 

938 Return a list of requirements for the named package. 

939 

940 :return: An iterable of requirements, suitable for 

941 packaging.requirement.Requirement. 

942 """ 

943 return distribution(distribution_name).requires 

944 

945 

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

947 """ 

948 Return a mapping of top-level packages to their 

949 distributions. 

950 

951 >>> import collections.abc 

952 >>> pkgs = packages_distributions() 

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

954 True 

955 """ 

956 pkg_to_dist = collections.defaultdict(list) 

957 for dist in distributions(): 

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

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

960 return dict(pkg_to_dist) 

961 

962 

963def _top_level_declared(dist): 

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

965 

966 

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

968 """ 

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

970 """ 

971 top, *rest = name.parts 

972 return top if rest else None 

973 

974 

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

976 """ 

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

978 sys.path. 

979 

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

981 'foo' 

982 >>> _get_toplevel_name(PackagePath('foo')) 

983 'foo' 

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

985 'foo' 

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

987 'foo' 

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

989 'foo.pth' 

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

991 'foo.dist-info' 

992 """ 

993 return _topmost(name) or ( 

994 # python/typeshed#10328 

995 inspect.getmodulename(name) # type: ignore 

996 or str(name) 

997 ) 

998 

999 

1000def _top_level_inferred(dist): 

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

1002 

1003 def importable_name(name): 

1004 return '.' not in name 

1005 

1006 return filter(importable_name, opt_names)