Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 collections 

16 

17from ._collections import FreezableDefaultDict, Pair 

18from ._compat import ( 

19 NullFinder, 

20 Protocol, 

21 PyPy_repr, 

22 install, 

23) 

24from ._functools import method_cache 

25from ._itertools import unique_everseen 

26 

27from contextlib import suppress 

28from importlib import import_module 

29from importlib.abc import MetaPathFinder 

30from itertools import starmap 

31from typing import Any, List, Mapping, Optional, TypeVar, Union 

32 

33 

34__all__ = [ 

35 'Distribution', 

36 'DistributionFinder', 

37 'PackageNotFoundError', 

38 'distribution', 

39 'distributions', 

40 'entry_points', 

41 'files', 

42 'metadata', 

43 'packages_distributions', 

44 'requires', 

45 'version', 

46] 

47 

48 

49class PackageNotFoundError(ModuleNotFoundError): 

50 """The package was not found.""" 

51 

52 def __str__(self): 

53 tmpl = "No package metadata was found for {self.name}" 

54 return tmpl.format(**locals()) 

55 

56 @property 

57 def name(self): 

58 (name,) = self.args 

59 return name 

60 

61 

62class Sectioned: 

63 """ 

64 A simple entry point config parser for performance 

65 

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

67 ... print(item) 

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

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

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

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

72 

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

74 >>> item = next(res) 

75 >>> item.name 

76 'sec1' 

77 >>> item.value 

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

79 >>> item = next(res) 

80 >>> item.value 

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

82 >>> item = next(res) 

83 >>> item.name 

84 'sec2' 

85 >>> item.value 

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

87 >>> list(res) 

88 [] 

89 """ 

90 

91 _sample = textwrap.dedent( 

92 """ 

93 [sec1] 

94 # comments ignored 

95 a = 1 

96 b = 2 

97 

98 [sec2] 

99 a = 2 

100 """ 

101 ).lstrip() 

102 

103 @classmethod 

104 def section_pairs(cls, text): 

105 return ( 

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

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

108 if section.name is not None 

109 ) 

110 

111 @staticmethod 

112 def read(text, filter_=None): 

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

114 name = None 

115 for value in lines: 

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

117 if section_match: 

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

119 continue 

120 yield Pair(name, value) 

121 

122 @staticmethod 

123 def valid(line): 

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

125 

126 

127class EntryPoint( 

128 PyPy_repr, collections.namedtuple('EntryPointBase', 'name value group') 

129): 

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

131 

132 See `the packaging docs on entry points 

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

134 for more information. 

135 """ 

136 

137 pattern = re.compile( 

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

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

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

141 ) 

142 """ 

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

144 which might look like: 

145 

146 - module 

147 - package.module 

148 - package.module:attribute 

149 - package.module:object.attribute 

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

151 

152 Other combinations are possible as well. 

153 

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

155 following the attr, and following any extras. 

156 """ 

157 

158 dist: Optional['Distribution'] = None 

159 

160 def load(self): 

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

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

163 return the named object. 

164 """ 

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

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

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

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

169 

170 @property 

171 def module(self): 

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

173 return match.group('module') 

174 

175 @property 

176 def attr(self): 

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

178 return match.group('attr') 

179 

180 @property 

181 def extras(self): 

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

183 return list(re.finditer(r'\w+', match.group('extras') or '')) 

184 

185 def _for(self, dist): 

186 self.dist = dist 

187 return self 

188 

189 def __iter__(self): 

190 """ 

191 Supply iter so one may construct dicts of EntryPoints by name. 

192 """ 

193 msg = ( 

194 "Construction of dict of EntryPoints is deprecated in " 

195 "favor of EntryPoints." 

196 ) 

197 warnings.warn(msg, DeprecationWarning) 

198 return iter((self.name, self)) 

199 

200 def __reduce__(self): 

201 return ( 

202 self.__class__, 

203 (self.name, self.value, self.group), 

204 ) 

205 

206 def matches(self, **params): 

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

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

209 

210 

211class EntryPoints(tuple): 

212 """ 

213 An immutable collection of selectable EntryPoint objects. 

214 """ 

215 

216 __slots__ = () 

217 

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

219 """ 

220 Get the EntryPoint in self matching name. 

221 """ 

222 try: 

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

224 except StopIteration: 

225 raise KeyError(name) 

226 

227 def select(self, **params): 

228 """ 

229 Select entry points from self that match the 

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

231 """ 

232 return EntryPoints(ep for ep in self if ep.matches(**params)) 

233 

234 @property 

235 def names(self): 

236 """ 

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

238 """ 

239 return set(ep.name for ep in self) 

240 

241 @property 

242 def groups(self): 

243 """ 

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

245 

246 For coverage while SelectableGroups is present. 

247 >>> EntryPoints().groups 

248 set() 

249 """ 

250 return set(ep.group for ep in self) 

251 

252 @classmethod 

253 def _from_text_for(cls, text, dist): 

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

255 

256 @classmethod 

257 def _from_text(cls, text): 

258 return itertools.starmap(EntryPoint, cls._parse_groups(text or '')) 

259 

260 @staticmethod 

261 def _parse_groups(text): 

262 return ( 

263 (item.value.name, item.value.value, item.name) 

264 for item in Sectioned.section_pairs(text) 

265 ) 

266 

267 

268def flake8_bypass(func): 

269 # defer inspect import as performance optimization. 

270 import inspect 

271 

272 is_flake8 = any('flake8' in str(frame.filename) for frame in inspect.stack()[:5]) 

273 return func if not is_flake8 else lambda: None 

274 

275 

276class Deprecated: 

277 """ 

278 Compatibility add-in for mapping to indicate that 

279 mapping behavior is deprecated. 

280 

281 >>> recwarn = getfixture('recwarn') 

282 >>> class DeprecatedDict(Deprecated, dict): pass 

283 >>> dd = DeprecatedDict(foo='bar') 

284 >>> dd.get('baz', None) 

285 >>> dd['foo'] 

286 'bar' 

287 >>> list(dd) 

288 ['foo'] 

289 >>> list(dd.keys()) 

290 ['foo'] 

291 >>> 'foo' in dd 

292 True 

293 >>> list(dd.values()) 

294 ['bar'] 

295 >>> len(recwarn) 

296 1 

297 """ 

298 

299 _warn = functools.partial( 

300 warnings.warn, 

301 "SelectableGroups dict interface is deprecated. Use select.", 

302 DeprecationWarning, 

303 stacklevel=2, 

304 ) 

305 

306 def __getitem__(self, name): 

307 self._warn() 

308 return super().__getitem__(name) 

309 

310 def get(self, name, default=None): 

311 flake8_bypass(self._warn)() 

312 return super().get(name, default) 

313 

314 def __iter__(self): 

315 self._warn() 

316 return super().__iter__() 

317 

318 def __contains__(self, *args): 

319 self._warn() 

320 return super().__contains__(*args) 

321 

322 def keys(self): 

323 self._warn() 

324 return super().keys() 

325 

326 def values(self): 

327 self._warn() 

328 return super().values() 

329 

330 

331class SelectableGroups(Deprecated, dict): 

332 """ 

333 A backward- and forward-compatible result from 

334 entry_points that fully implements the dict interface. 

335 """ 

336 

337 @classmethod 

338 def load(cls, eps): 

339 by_group = operator.attrgetter('group') 

340 ordered = sorted(eps, key=by_group) 

341 grouped = itertools.groupby(ordered, by_group) 

342 return cls((group, EntryPoints(eps)) for group, eps in grouped) 

343 

344 @property 

345 def _all(self): 

346 """ 

347 Reconstruct a list of all entrypoints from the groups. 

348 """ 

349 groups = super(Deprecated, self).values() 

350 return EntryPoints(itertools.chain.from_iterable(groups)) 

351 

352 @property 

353 def groups(self): 

354 return self._all.groups 

355 

356 @property 

357 def names(self): 

358 """ 

359 for coverage: 

360 >>> SelectableGroups().names 

361 set() 

362 """ 

363 return self._all.names 

364 

365 def select(self, **params): 

366 if not params: 

367 return self 

368 return self._all.select(**params) 

369 

370 

371class PackagePath(pathlib.PurePosixPath): 

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

373 

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

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

376 return stream.read() 

377 

378 def read_binary(self): 

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

380 return stream.read() 

381 

382 def locate(self): 

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

384 return self.dist.locate_file(self) 

385 

386 

387class FileHash: 

388 def __init__(self, spec): 

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

390 

391 def __repr__(self): 

392 return '<FileHash mode: {} value: {}>'.format(self.mode, self.value) 

393 

394 

395_T = TypeVar("_T") 

396 

397 

398class PackageMetadata(Protocol): 

399 def __len__(self) -> int: 

400 ... # pragma: no cover 

401 

402 def __contains__(self, item: str) -> bool: 

403 ... # pragma: no cover 

404 

405 def __getitem__(self, key: str) -> str: 

406 ... # pragma: no cover 

407 

408 def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]: 

409 """ 

410 Return all values associated with a possibly multi-valued key. 

411 """ 

412 

413 

414class Distribution: 

415 """A Python distribution package.""" 

416 

417 @abc.abstractmethod 

418 def read_text(self, filename): 

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

420 

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

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

423 """ 

424 

425 @abc.abstractmethod 

426 def locate_file(self, path): 

427 """ 

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

429 to it. 

430 """ 

431 

432 @classmethod 

433 def from_name(cls, name): 

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

435 

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

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

438 package, if found. 

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

440 metadata cannot be found. 

441 """ 

442 for resolver in cls._discover_resolvers(): 

443 dists = resolver(DistributionFinder.Context(name=name)) 

444 dist = next(iter(dists), None) 

445 if dist is not None: 

446 return dist 

447 else: 

448 raise PackageNotFoundError(name) 

449 

450 @classmethod 

451 def discover(cls, **kwargs): 

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

453 

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

455 a context. 

456 

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

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

459 """ 

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

461 if context and kwargs: 

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

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

464 return itertools.chain.from_iterable( 

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

466 ) 

467 

468 @staticmethod 

469 def at(path): 

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

471 

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

473 :return: a concrete Distribution instance for the path 

474 """ 

475 return PathDistribution(pathlib.Path(path)) 

476 

477 @staticmethod 

478 def _discover_resolvers(): 

479 """Search the meta_path for resolvers.""" 

480 declared = ( 

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

482 ) 

483 return filter(None, declared) 

484 

485 @classmethod 

486 def _local(cls, root='.'): 

487 from pep517 import build, meta 

488 

489 system = build.compat_system(root) 

490 builder = functools.partial( 

491 meta.build, 

492 source_dir=root, 

493 system=system, 

494 ) 

495 return PathDistribution(zipp.Path(meta.build_as_zip(builder))) 

496 

497 @property 

498 def metadata(self) -> PackageMetadata: 

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

500 

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

502 metadata. See PEP 566 for details. 

503 """ 

504 text = ( 

505 self.read_text('METADATA') 

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

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

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

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

510 or self.read_text('') 

511 ) 

512 return email.message_from_string(text) 

513 

514 @property 

515 def name(self): 

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

517 return self.metadata['Name'] 

518 

519 @property 

520 def version(self): 

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

522 return self.metadata['Version'] 

523 

524 @property 

525 def entry_points(self): 

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

527 

528 @property 

529 def files(self): 

530 """Files in this distribution. 

531 

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

533 

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

535 (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is 

536 missing. 

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

538 """ 

539 file_lines = self._read_files_distinfo() or self._read_files_egginfo() 

540 

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

542 result = PackagePath(name) 

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

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

545 result.dist = self 

546 return result 

547 

548 return file_lines and list(starmap(make_file, csv.reader(file_lines))) 

549 

550 def _read_files_distinfo(self): 

551 """ 

552 Read the lines of RECORD 

553 """ 

554 text = self.read_text('RECORD') 

555 return text and text.splitlines() 

556 

557 def _read_files_egginfo(self): 

558 """ 

559 SOURCES.txt might contain literal commas, so wrap each line 

560 in quotes. 

561 """ 

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

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

564 

565 @property 

566 def requires(self): 

567 """Generated requirements specified for this Distribution""" 

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

569 return reqs and list(reqs) 

570 

571 def _read_dist_info_reqs(self): 

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

573 

574 def _read_egg_info_reqs(self): 

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

576 return source and self._deps_from_requires_text(source) 

577 

578 @classmethod 

579 def _deps_from_requires_text(cls, source): 

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

581 

582 @staticmethod 

583 def _convert_egg_info_reqs_to_simple_reqs(sections): 

584 """ 

585 Historically, setuptools would solicit and store 'extra' 

586 requirements, including those with environment markers, 

587 in separate sections. More modern tools expect each 

588 dependency to be defined separately, with any relevant 

589 extras and environment markers attached directly to that 

590 requirement. This method converts the former to the 

591 latter. See _test_deps_from_requires_text for an example. 

592 """ 

593 

594 def make_condition(name): 

595 return name and 'extra == "{name}"'.format(name=name) 

596 

597 def parse_condition(section): 

598 section = section or '' 

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

600 if extra and markers: 

601 markers = '({markers})'.format(markers=markers) 

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

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

604 

605 for section in sections: 

606 yield section.value + parse_condition(section.name) 

607 

608 

609class DistributionFinder(MetaPathFinder): 

610 """ 

611 A MetaPathFinder capable of discovering installed distributions. 

612 """ 

613 

614 class Context: 

615 """ 

616 Keyword arguments presented by the caller to 

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

618 to narrow the scope of a search for distributions 

619 in all DistributionFinders. 

620 

621 Each DistributionFinder may expect any parameters 

622 and should attempt to honor the canonical 

623 parameters defined below when appropriate. 

624 """ 

625 

626 name = None 

627 """ 

628 Specific name for which a distribution finder should match. 

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

630 """ 

631 

632 def __init__(self, **kwargs): 

633 vars(self).update(kwargs) 

634 

635 @property 

636 def path(self): 

637 """ 

638 The path that a distribution finder should search. 

639 

640 Typically refers to Python package paths and defaults 

641 to ``sys.path``. 

642 """ 

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

644 

645 @abc.abstractmethod 

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

647 """ 

648 Find distributions. 

649 

650 Return an iterable of all Distribution instances capable of 

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

652 a DistributionFinder.Context instance. 

653 """ 

654 

655 

656class FastPath: 

657 """ 

658 Micro-optimized class for searching a path for 

659 children. 

660 """ 

661 

662 @functools.lru_cache() # type: ignore 

663 def __new__(cls, root): 

664 return super().__new__(cls) 

665 

666 def __init__(self, root): 

667 self.root = str(root) 

668 

669 def joinpath(self, child): 

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

671 

672 def children(self): 

673 with suppress(Exception): 

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

675 with suppress(Exception): 

676 return self.zip_children() 

677 return [] 

678 

679 def zip_children(self): 

680 zip_path = zipp.Path(self.root) 

681 names = zip_path.root.namelist() 

682 self.joinpath = zip_path.joinpath 

683 

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

685 

686 def search(self, name): 

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

688 

689 @property 

690 def mtime(self): 

691 with suppress(OSError): 

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

693 self.lookup.cache_clear() 

694 

695 @method_cache 

696 def lookup(self, mtime): 

697 return Lookup(self) 

698 

699 

700class Lookup: 

701 def __init__(self, path: FastPath): 

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

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

704 self.infos = FreezableDefaultDict(list) 

705 self.eggs = FreezableDefaultDict(list) 

706 

707 for child in path.children(): 

708 low = child.lower() 

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

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

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

712 normalized = Prepared.normalize(name) 

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

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

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

716 legacy_normalized = Prepared.legacy_normalize(name) 

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

718 

719 self.infos.freeze() 

720 self.eggs.freeze() 

721 

722 def search(self, prepared): 

723 infos = ( 

724 self.infos[prepared.normalized] 

725 if prepared 

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

727 ) 

728 eggs = ( 

729 self.eggs[prepared.legacy_normalized] 

730 if prepared 

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

732 ) 

733 return itertools.chain(infos, eggs) 

734 

735 

736class Prepared: 

737 """ 

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

739 """ 

740 

741 normalized = None 

742 legacy_normalized = None 

743 

744 def __init__(self, name): 

745 self.name = name 

746 if name is None: 

747 return 

748 self.normalized = self.normalize(name) 

749 self.legacy_normalized = self.legacy_normalize(name) 

750 

751 @staticmethod 

752 def normalize(name): 

753 """ 

754 PEP 503 normalization plus dashes as underscores. 

755 """ 

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

757 

758 @staticmethod 

759 def legacy_normalize(name): 

760 """ 

761 Normalize the package name as found in the convention in 

762 older packaging tools versions and specs. 

763 """ 

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

765 

766 def __bool__(self): 

767 return bool(self.name) 

768 

769 

770@install 

771class MetadataPathFinder(NullFinder, DistributionFinder): 

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

773 

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

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

776 """ 

777 

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

779 """ 

780 Find distributions. 

781 

782 Return an iterable of all Distribution instances capable of 

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

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

785 of directories ``context.path``. 

786 """ 

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

788 return map(PathDistribution, found) 

789 

790 @classmethod 

791 def _search_paths(cls, name, paths): 

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

793 prepared = Prepared(name) 

794 return itertools.chain.from_iterable( 

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

796 ) 

797 

798 def invalidate_caches(cls): 

799 FastPath.__new__.cache_clear() 

800 

801 

802class PathDistribution(Distribution): 

803 def __init__(self, path): 

804 """Construct a distribution from a path to the metadata directory. 

805 

806 :param path: A pathlib.Path or similar object supporting 

807 .joinpath(), __div__, .parent, and .read_text(). 

808 """ 

809 self._path = path 

810 

811 def read_text(self, filename): 

812 with suppress( 

813 FileNotFoundError, 

814 IsADirectoryError, 

815 KeyError, 

816 NotADirectoryError, 

817 PermissionError, 

818 ): 

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

820 

821 read_text.__doc__ = Distribution.read_text.__doc__ 

822 

823 def locate_file(self, path): 

824 return self._path.parent / path 

825 

826 

827def distribution(distribution_name): 

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

829 

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

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

832 """ 

833 return Distribution.from_name(distribution_name) 

834 

835 

836def distributions(**kwargs): 

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

838 

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

840 """ 

841 return Distribution.discover(**kwargs) 

842 

843 

844def metadata(distribution_name) -> PackageMetadata: 

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

846 

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

848 :return: A PackageMetadata containing the parsed metadata. 

849 """ 

850 return Distribution.from_name(distribution_name).metadata 

851 

852 

853def version(distribution_name): 

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

855 

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

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

858 "Version" metadata key. 

859 """ 

860 return distribution(distribution_name).version 

861 

862 

863def entry_points(**params) -> Union[EntryPoints, SelectableGroups]: 

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

865 

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

867 result to entry points matching those properties (see 

868 EntryPoints.select()). 

869 

870 For compatibility, returns ``SelectableGroups`` object unless 

871 selection parameters are supplied. In the future, this function 

872 will return ``EntryPoints`` instead of ``SelectableGroups`` 

873 even when no selection parameters are supplied. 

874 

875 For maximum future compatibility, pass selection parameters 

876 or invoke ``.select`` with parameters on the result. 

877 

878 :return: EntryPoints or SelectableGroups for all installed packages. 

879 """ 

880 unique = functools.partial(unique_everseen, key=operator.attrgetter('name')) 

881 eps = itertools.chain.from_iterable( 

882 dist.entry_points for dist in unique(distributions()) 

883 ) 

884 return SelectableGroups.load(eps).select(**params) 

885 

886 

887def files(distribution_name): 

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

889 

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

891 :return: List of files composing the distribution. 

892 """ 

893 return distribution(distribution_name).files 

894 

895 

896def requires(distribution_name): 

897 """ 

898 Return a list of requirements for the named package. 

899 

900 :return: An iterator of requirements, suitable for 

901 packaging.requirement.Requirement. 

902 """ 

903 return distribution(distribution_name).requires 

904 

905 

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

907 """ 

908 Return a mapping of top-level packages to their 

909 distributions. 

910 

911 >>> import collections.abc 

912 >>> pkgs = packages_distributions() 

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

914 True 

915 """ 

916 pkg_to_dist = collections.defaultdict(list) 

917 for dist in distributions(): 

918 for pkg in (dist.read_text('top_level.txt') or '').split(): 

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

920 return dict(pkg_to_dist)