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

359 statements  

« prev     ^ index     » next       coverage.py v7.0.1, created at 2022-12-25 06:11 +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 collections 

16 

17from . import _adapters, _meta, _py39compat 

18from ._collections import FreezableDefaultDict, Pair 

19from ._compat import ( 

20 NullFinder, 

21 install, 

22 pypy_partial, 

23) 

24from ._functools import method_cache, pass_none 

25from ._itertools import always_iterable, unique_everseen 

26from ._meta import PackageMetadata, SimplePath 

27 

28from contextlib import suppress 

29from importlib import import_module 

30from importlib.abc import MetaPathFinder 

31from itertools import starmap 

32from typing import List, Mapping, Optional 

33 

34 

35__all__ = [ 

36 'Distribution', 

37 'DistributionFinder', 

38 'PackageMetadata', 

39 'PackageNotFoundError', 

40 'distribution', 

41 'distributions', 

42 'entry_points', 

43 'files', 

44 'metadata', 

45 'packages_distributions', 

46 'requires', 

47 'version', 

48] 

49 

50 

51class PackageNotFoundError(ModuleNotFoundError): 

52 """The package was not found.""" 

53 

54 def __str__(self): 

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

56 

57 @property 

58 def name(self): 

59 (name,) = self.args 

60 return name 

61 

62 

63class Sectioned: 

64 """ 

65 A simple entry point config parser for performance 

66 

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

68 ... print(item) 

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

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

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

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

73 

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

75 >>> item = next(res) 

76 >>> item.name 

77 'sec1' 

78 >>> item.value 

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

80 >>> item = next(res) 

81 >>> item.value 

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

83 >>> item = next(res) 

84 >>> item.name 

85 'sec2' 

86 >>> item.value 

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

88 >>> list(res) 

89 [] 

90 """ 

91 

92 _sample = textwrap.dedent( 

93 """ 

94 [sec1] 

95 # comments ignored 

96 a = 1 

97 b = 2 

98 

99 [sec2] 

100 a = 2 

101 """ 

102 ).lstrip() 

103 

104 @classmethod 

105 def section_pairs(cls, text): 

106 return ( 

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

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

109 if section.name is not None 

110 ) 

111 

112 @staticmethod 

113 def read(text, filter_=None): 

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

115 name = None 

116 for value in lines: 

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

118 if section_match: 

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

120 continue 

121 yield Pair(name, value) 

122 

123 @staticmethod 

124 def valid(line): 

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

126 

127 

128class DeprecatedTuple: 

129 """ 

130 Provide subscript item access for backward compatibility. 

131 

132 >>> recwarn = getfixture('recwarn') 

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

134 >>> ep[:] 

135 ('name', 'value', 'group') 

136 >>> ep[0] 

137 'name' 

138 >>> len(recwarn) 

139 1 

140 """ 

141 

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

143 _warn = functools.partial( 

144 warnings.warn, 

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

146 DeprecationWarning, 

147 stacklevel=pypy_partial(2), 

148 ) 

149 

150 def __getitem__(self, item): 

151 self._warn() 

152 return self._key()[item] 

153 

154 

155class EntryPoint(DeprecatedTuple): 

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

157 

158 See `the packaging docs on entry points 

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

160 for more information. 

161 

162 >>> ep = EntryPoint( 

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

164 >>> ep.module 

165 'package.module' 

166 >>> ep.attr 

167 'attr' 

168 >>> ep.extras 

169 ['extra1', 'extra2'] 

170 """ 

171 

172 pattern = re.compile( 

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

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

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

176 ) 

177 """ 

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

179 which might look like: 

180 

181 - module 

182 - package.module 

183 - package.module:attribute 

184 - package.module:object.attribute 

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

186 

187 Other combinations are possible as well. 

188 

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

190 following the attr, and following any extras. 

191 """ 

192 

193 name: str 

194 value: str 

195 group: str 

196 

197 dist: Optional['Distribution'] = None 

198 

199 def __init__(self, name, value, group): 

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

201 

202 def load(self): 

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

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

205 return the named object. 

206 """ 

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

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

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

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

211 

212 @property 

213 def module(self): 

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

215 return match.group('module') 

216 

217 @property 

218 def attr(self): 

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

220 return match.group('attr') 

221 

222 @property 

223 def extras(self): 

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

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

226 

227 def _for(self, dist): 

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

229 return self 

230 

231 def matches(self, **params): 

232 """ 

233 EntryPoint matches the given parameters. 

234 

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

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

237 True 

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

239 True 

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

241 False 

242 >>> ep.matches() 

243 True 

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

245 True 

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

247 True 

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

249 True 

250 """ 

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

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

253 

254 def _key(self): 

255 return self.name, self.value, self.group 

256 

257 def __lt__(self, other): 

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

259 

260 def __eq__(self, other): 

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

262 

263 def __setattr__(self, name, value): 

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

265 

266 def __repr__(self): 

267 return ( 

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

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

270 ) 

271 

272 def __hash__(self): 

273 return hash(self._key()) 

274 

275 

276class EntryPoints(tuple): 

277 """ 

278 An immutable collection of selectable EntryPoint objects. 

279 """ 

280 

281 __slots__ = () 

282 

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

284 """ 

285 Get the EntryPoint in self matching name. 

286 """ 

287 try: 

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

289 except StopIteration: 

290 raise KeyError(name) 

291 

292 def select(self, **params): 

293 """ 

294 Select entry points from self that match the 

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

296 """ 

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

298 

299 @property 

300 def names(self): 

301 """ 

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

303 """ 

304 return {ep.name for ep in self} 

305 

306 @property 

307 def groups(self): 

308 """ 

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

310 """ 

311 return {ep.group for ep in self} 

312 

313 @classmethod 

314 def _from_text_for(cls, text, dist): 

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

316 

317 @staticmethod 

318 def _from_text(text): 

319 return ( 

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

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

322 ) 

323 

324 

325class PackagePath(pathlib.PurePosixPath): 

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

327 

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

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

330 return stream.read() 

331 

332 def read_binary(self): 

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

334 return stream.read() 

335 

336 def locate(self): 

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

338 return self.dist.locate_file(self) 

339 

340 

341class FileHash: 

342 def __init__(self, spec): 

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

344 

345 def __repr__(self): 

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

347 

348 

349class Distribution: 

350 """A Python distribution package.""" 

351 

352 @abc.abstractmethod 

353 def read_text(self, filename): 

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

355 

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

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

358 """ 

359 

360 @abc.abstractmethod 

361 def locate_file(self, path): 

362 """ 

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

364 to it. 

365 """ 

366 

367 @classmethod 

368 def from_name(cls, name: str): 

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

370 

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

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

373 package, if found. 

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

375 metadata cannot be found. 

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

377 """ 

378 if not name: 

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

380 try: 

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

382 except StopIteration: 

383 raise PackageNotFoundError(name) 

384 

385 @classmethod 

386 def discover(cls, **kwargs): 

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

388 

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

390 a context. 

391 

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

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

394 """ 

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

396 if context and kwargs: 

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

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

399 return itertools.chain.from_iterable( 

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

401 ) 

402 

403 @staticmethod 

404 def at(path): 

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

406 

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

408 :return: a concrete Distribution instance for the path 

409 """ 

410 return PathDistribution(pathlib.Path(path)) 

411 

412 @staticmethod 

413 def _discover_resolvers(): 

414 """Search the meta_path for resolvers.""" 

415 declared = ( 

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

417 ) 

418 return filter(None, declared) 

419 

420 @property 

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

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

423 

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

425 metadata. See PEP 566 for details. 

426 """ 

427 text = ( 

428 self.read_text('METADATA') 

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

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

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

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

433 or self.read_text('') 

434 ) 

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

436 

437 @property 

438 def name(self): 

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

440 return self.metadata['Name'] 

441 

442 @property 

443 def _normalized_name(self): 

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

445 return Prepared.normalize(self.name) 

446 

447 @property 

448 def version(self): 

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

450 return self.metadata['Version'] 

451 

452 @property 

453 def entry_points(self): 

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

455 

456 @property 

457 def files(self): 

458 """Files in this distribution. 

459 

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

461 

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

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

464 missing. 

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

466 """ 

467 

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

469 result = PackagePath(name) 

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

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

472 result.dist = self 

473 return result 

474 

475 @pass_none 

476 def make_files(lines): 

477 return list(starmap(make_file, csv.reader(lines))) 

478 

479 return make_files(self._read_files_distinfo() or self._read_files_egginfo()) 

480 

481 def _read_files_distinfo(self): 

482 """ 

483 Read the lines of RECORD 

484 """ 

485 text = self.read_text('RECORD') 

486 return text and text.splitlines() 

487 

488 def _read_files_egginfo(self): 

489 """ 

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

491 in quotes. 

492 """ 

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

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

495 

496 @property 

497 def requires(self): 

498 """Generated requirements specified for this Distribution""" 

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

500 return reqs and list(reqs) 

501 

502 def _read_dist_info_reqs(self): 

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

504 

505 def _read_egg_info_reqs(self): 

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

507 return pass_none(self._deps_from_requires_text)(source) 

508 

509 @classmethod 

510 def _deps_from_requires_text(cls, source): 

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

512 

513 @staticmethod 

514 def _convert_egg_info_reqs_to_simple_reqs(sections): 

515 """ 

516 Historically, setuptools would solicit and store 'extra' 

517 requirements, including those with environment markers, 

518 in separate sections. More modern tools expect each 

519 dependency to be defined separately, with any relevant 

520 extras and environment markers attached directly to that 

521 requirement. This method converts the former to the 

522 latter. See _test_deps_from_requires_text for an example. 

523 """ 

524 

525 def make_condition(name): 

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

527 

528 def quoted_marker(section): 

529 section = section or '' 

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

531 if extra and markers: 

532 markers = f'({markers})' 

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

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

535 

536 def url_req_space(req): 

537 """ 

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

539 Ref python/importlib_metadata#357. 

540 """ 

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

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

543 

544 for section in sections: 

545 space = url_req_space(section.value) 

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

547 

548 

549class DistributionFinder(MetaPathFinder): 

550 """ 

551 A MetaPathFinder capable of discovering installed distributions. 

552 """ 

553 

554 class Context: 

555 """ 

556 Keyword arguments presented by the caller to 

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

558 to narrow the scope of a search for distributions 

559 in all DistributionFinders. 

560 

561 Each DistributionFinder may expect any parameters 

562 and should attempt to honor the canonical 

563 parameters defined below when appropriate. 

564 """ 

565 

566 name = None 

567 """ 

568 Specific name for which a distribution finder should match. 

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

570 """ 

571 

572 def __init__(self, **kwargs): 

573 vars(self).update(kwargs) 

574 

575 @property 

576 def path(self): 

577 """ 

578 The sequence of directory path that a distribution finder 

579 should search. 

580 

581 Typically refers to Python installed package paths such as 

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

583 """ 

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

585 

586 @abc.abstractmethod 

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

588 """ 

589 Find distributions. 

590 

591 Return an iterable of all Distribution instances capable of 

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

593 a DistributionFinder.Context instance. 

594 """ 

595 

596 

597class FastPath: 

598 """ 

599 Micro-optimized class for searching a path for 

600 children. 

601 

602 >>> FastPath('').children() 

603 ['...'] 

604 """ 

605 

606 @functools.lru_cache() # type: ignore 

607 def __new__(cls, root): 

608 return super().__new__(cls) 

609 

610 def __init__(self, root): 

611 self.root = root 

612 

613 def joinpath(self, child): 

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

615 

616 def children(self): 

617 with suppress(Exception): 

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

619 with suppress(Exception): 

620 return self.zip_children() 

621 return [] 

622 

623 def zip_children(self): 

624 zip_path = zipp.Path(self.root) 

625 names = zip_path.root.namelist() 

626 self.joinpath = zip_path.joinpath 

627 

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

629 

630 def search(self, name): 

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

632 

633 @property 

634 def mtime(self): 

635 with suppress(OSError): 

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

637 self.lookup.cache_clear() 

638 

639 @method_cache 

640 def lookup(self, mtime): 

641 return Lookup(self) 

642 

643 

644class Lookup: 

645 def __init__(self, path: FastPath): 

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

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

648 self.infos = FreezableDefaultDict(list) 

649 self.eggs = FreezableDefaultDict(list) 

650 

651 for child in path.children(): 

652 low = child.lower() 

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

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

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

656 normalized = Prepared.normalize(name) 

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

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

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

660 legacy_normalized = Prepared.legacy_normalize(name) 

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

662 

663 self.infos.freeze() 

664 self.eggs.freeze() 

665 

666 def search(self, prepared): 

667 infos = ( 

668 self.infos[prepared.normalized] 

669 if prepared 

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

671 ) 

672 eggs = ( 

673 self.eggs[prepared.legacy_normalized] 

674 if prepared 

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

676 ) 

677 return itertools.chain(infos, eggs) 

678 

679 

680class Prepared: 

681 """ 

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

683 """ 

684 

685 normalized = None 

686 legacy_normalized = None 

687 

688 def __init__(self, name): 

689 self.name = name 

690 if name is None: 

691 return 

692 self.normalized = self.normalize(name) 

693 self.legacy_normalized = self.legacy_normalize(name) 

694 

695 @staticmethod 

696 def normalize(name): 

697 """ 

698 PEP 503 normalization plus dashes as underscores. 

699 """ 

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

701 

702 @staticmethod 

703 def legacy_normalize(name): 

704 """ 

705 Normalize the package name as found in the convention in 

706 older packaging tools versions and specs. 

707 """ 

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

709 

710 def __bool__(self): 

711 return bool(self.name) 

712 

713 

714@install 

715class MetadataPathFinder(NullFinder, DistributionFinder): 

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

717 

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

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

720 """ 

721 

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

723 """ 

724 Find distributions. 

725 

726 Return an iterable of all Distribution instances capable of 

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

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

729 of directories ``context.path``. 

730 """ 

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

732 return map(PathDistribution, found) 

733 

734 @classmethod 

735 def _search_paths(cls, name, paths): 

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

737 prepared = Prepared(name) 

738 return itertools.chain.from_iterable( 

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

740 ) 

741 

742 def invalidate_caches(cls): 

743 FastPath.__new__.cache_clear() 

744 

745 

746class PathDistribution(Distribution): 

747 def __init__(self, path: SimplePath): 

748 """Construct a distribution. 

749 

750 :param path: SimplePath indicating the metadata directory. 

751 """ 

752 self._path = path 

753 

754 def read_text(self, filename): 

755 with suppress( 

756 FileNotFoundError, 

757 IsADirectoryError, 

758 KeyError, 

759 NotADirectoryError, 

760 PermissionError, 

761 ): 

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

763 

764 read_text.__doc__ = Distribution.read_text.__doc__ 

765 

766 def locate_file(self, path): 

767 return self._path.parent / path 

768 

769 @property 

770 def _normalized_name(self): 

771 """ 

772 Performance optimization: where possible, resolve the 

773 normalized name from the file system path. 

774 """ 

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

776 return ( 

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

778 or super()._normalized_name 

779 ) 

780 

781 @staticmethod 

782 def _name_from_stem(stem): 

783 """ 

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

785 'foo' 

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

787 'CherryPy' 

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

789 'face' 

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

791 """ 

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

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

794 return 

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

796 return name 

797 

798 

799def distribution(distribution_name): 

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

801 

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

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

804 """ 

805 return Distribution.from_name(distribution_name) 

806 

807 

808def distributions(**kwargs): 

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

810 

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

812 """ 

813 return Distribution.discover(**kwargs) 

814 

815 

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

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

818 

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

820 :return: A PackageMetadata containing the parsed metadata. 

821 """ 

822 return Distribution.from_name(distribution_name).metadata 

823 

824 

825def version(distribution_name): 

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

827 

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

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

830 "Version" metadata key. 

831 """ 

832 return distribution(distribution_name).version 

833 

834 

835_unique = functools.partial( 

836 unique_everseen, 

837 key=_py39compat.normalized_name, 

838) 

839""" 

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

841""" 

842 

843 

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

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

846 

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

848 result to entry points matching those properties (see 

849 EntryPoints.select()). 

850 

851 :return: EntryPoints for all installed packages. 

852 """ 

853 eps = itertools.chain.from_iterable( 

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

855 ) 

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

857 

858 

859def files(distribution_name): 

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

861 

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

863 :return: List of files composing the distribution. 

864 """ 

865 return distribution(distribution_name).files 

866 

867 

868def requires(distribution_name): 

869 """ 

870 Return a list of requirements for the named package. 

871 

872 :return: An iterator of requirements, suitable for 

873 packaging.requirement.Requirement. 

874 """ 

875 return distribution(distribution_name).requires 

876 

877 

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

879 """ 

880 Return a mapping of top-level packages to their 

881 distributions. 

882 

883 >>> import collections.abc 

884 >>> pkgs = packages_distributions() 

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

886 True 

887 """ 

888 pkg_to_dist = collections.defaultdict(list) 

889 for dist in distributions(): 

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

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

892 return dict(pkg_to_dist) 

893 

894 

895def _top_level_declared(dist): 

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

897 

898 

899def _top_level_inferred(dist): 

900 return { 

901 f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name 

902 for f in always_iterable(dist.files) 

903 if f.suffix == ".py" 

904 }