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

376 statements  

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

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 Distribution(metaclass=abc.ABCMeta): 

352 """A Python distribution package.""" 

353 

354 @abc.abstractmethod 

355 def read_text(self, filename): 

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

357 

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

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

360 """ 

361 

362 @abc.abstractmethod 

363 def locate_file(self, path): 

364 """ 

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

366 to it. 

367 """ 

368 

369 @classmethod 

370 def from_name(cls, name: str): 

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

372 

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

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

375 package, if found. 

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

377 metadata cannot be found. 

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

379 """ 

380 if not name: 

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

382 try: 

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

384 except StopIteration: 

385 raise PackageNotFoundError(name) 

386 

387 @classmethod 

388 def discover(cls, **kwargs): 

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

390 

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

392 a context. 

393 

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

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

396 """ 

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

398 if context and kwargs: 

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

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

401 return itertools.chain.from_iterable( 

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

403 ) 

404 

405 @staticmethod 

406 def at(path): 

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

408 

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

410 :return: a concrete Distribution instance for the path 

411 """ 

412 return PathDistribution(pathlib.Path(path)) 

413 

414 @staticmethod 

415 def _discover_resolvers(): 

416 """Search the meta_path for resolvers.""" 

417 declared = ( 

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

419 ) 

420 return filter(None, declared) 

421 

422 @property 

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

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

425 

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

427 metadata. See PEP 566 for details. 

428 """ 

429 text = ( 

430 self.read_text('METADATA') 

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

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

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

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

435 or self.read_text('') 

436 ) 

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

438 

439 @property 

440 def name(self): 

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

442 return self.metadata['Name'] 

443 

444 @property 

445 def _normalized_name(self): 

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

447 return Prepared.normalize(self.name) 

448 

449 @property 

450 def version(self): 

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

452 return self.metadata['Version'] 

453 

454 @property 

455 def entry_points(self): 

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

457 

458 @property 

459 def files(self): 

460 """Files in this distribution. 

461 

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

463 

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

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

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

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

468 """ 

469 

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

471 result = PackagePath(name) 

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

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

474 result.dist = self 

475 return result 

476 

477 @pass_none 

478 def make_files(lines): 

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

480 

481 @pass_none 

482 def skip_missing_files(package_paths): 

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

484 

485 return skip_missing_files( 

486 make_files( 

487 self._read_files_distinfo() 

488 or self._read_files_egginfo_installed() 

489 or self._read_files_egginfo_sources() 

490 ) 

491 ) 

492 

493 def _read_files_distinfo(self): 

494 """ 

495 Read the lines of RECORD 

496 """ 

497 text = self.read_text('RECORD') 

498 return text and text.splitlines() 

499 

500 def _read_files_egginfo_installed(self): 

501 """ 

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

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

504 relative to the site-packages directory, and must also be 

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

506 

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

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

509 Hence, even if we can assume that this file is accurate 

510 when it exists, we cannot assume that it always exists. 

511 """ 

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

513 # We need to prepend the .egg-info/ subdir to the lines in this file. 

514 # But this subdir is only available in the PathDistribution's self._path 

515 # which is not easily accessible from this base class... 

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

517 if not text or not subdir: 

518 return 

519 with contextlib.suppress(Exception): 

520 ret = [ 

521 str((subdir / line).resolve().relative_to(self.locate_file(''))) 

522 for line in text.splitlines() 

523 ] 

524 return map('"{}"'.format, ret) 

525 

526 def _read_files_egginfo_sources(self): 

527 """ 

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

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

530 might contain literal commas). 

531 

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

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

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

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

536 that are present after the package has been installed. 

537 """ 

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

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

540 

541 @property 

542 def requires(self): 

543 """Generated requirements specified for this Distribution""" 

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

545 return reqs and list(reqs) 

546 

547 def _read_dist_info_reqs(self): 

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

549 

550 def _read_egg_info_reqs(self): 

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

552 return pass_none(self._deps_from_requires_text)(source) 

553 

554 @classmethod 

555 def _deps_from_requires_text(cls, source): 

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

557 

558 @staticmethod 

559 def _convert_egg_info_reqs_to_simple_reqs(sections): 

560 """ 

561 Historically, setuptools would solicit and store 'extra' 

562 requirements, including those with environment markers, 

563 in separate sections. More modern tools expect each 

564 dependency to be defined separately, with any relevant 

565 extras and environment markers attached directly to that 

566 requirement. This method converts the former to the 

567 latter. See _test_deps_from_requires_text for an example. 

568 """ 

569 

570 def make_condition(name): 

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

572 

573 def quoted_marker(section): 

574 section = section or '' 

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

576 if extra and markers: 

577 markers = f'({markers})' 

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

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

580 

581 def url_req_space(req): 

582 """ 

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

584 Ref python/importlib_metadata#357. 

585 """ 

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

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

588 

589 for section in sections: 

590 space = url_req_space(section.value) 

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

592 

593 

594class DistributionFinder(MetaPathFinder): 

595 """ 

596 A MetaPathFinder capable of discovering installed distributions. 

597 """ 

598 

599 class Context: 

600 """ 

601 Keyword arguments presented by the caller to 

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

603 to narrow the scope of a search for distributions 

604 in all DistributionFinders. 

605 

606 Each DistributionFinder may expect any parameters 

607 and should attempt to honor the canonical 

608 parameters defined below when appropriate. 

609 """ 

610 

611 name = None 

612 """ 

613 Specific name for which a distribution finder should match. 

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

615 """ 

616 

617 def __init__(self, **kwargs): 

618 vars(self).update(kwargs) 

619 

620 @property 

621 def path(self): 

622 """ 

623 The sequence of directory path that a distribution finder 

624 should search. 

625 

626 Typically refers to Python installed package paths such as 

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

628 """ 

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

630 

631 @abc.abstractmethod 

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

633 """ 

634 Find distributions. 

635 

636 Return an iterable of all Distribution instances capable of 

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

638 a DistributionFinder.Context instance. 

639 """ 

640 

641 

642class FastPath: 

643 """ 

644 Micro-optimized class for searching a path for 

645 children. 

646 

647 >>> FastPath('').children() 

648 ['...'] 

649 """ 

650 

651 @functools.lru_cache() # type: ignore 

652 def __new__(cls, root): 

653 return super().__new__(cls) 

654 

655 def __init__(self, root): 

656 self.root = root 

657 

658 def joinpath(self, child): 

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

660 

661 def children(self): 

662 with suppress(Exception): 

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

664 with suppress(Exception): 

665 return self.zip_children() 

666 return [] 

667 

668 def zip_children(self): 

669 zip_path = zipp.Path(self.root) 

670 names = zip_path.root.namelist() 

671 self.joinpath = zip_path.joinpath 

672 

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

674 

675 def search(self, name): 

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

677 

678 @property 

679 def mtime(self): 

680 with suppress(OSError): 

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

682 self.lookup.cache_clear() 

683 

684 @method_cache 

685 def lookup(self, mtime): 

686 return Lookup(self) 

687 

688 

689class Lookup: 

690 def __init__(self, path: FastPath): 

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

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

693 self.infos = FreezableDefaultDict(list) 

694 self.eggs = FreezableDefaultDict(list) 

695 

696 for child in path.children(): 

697 low = child.lower() 

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

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

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

701 normalized = Prepared.normalize(name) 

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

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

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

705 legacy_normalized = Prepared.legacy_normalize(name) 

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

707 

708 self.infos.freeze() 

709 self.eggs.freeze() 

710 

711 def search(self, prepared): 

712 infos = ( 

713 self.infos[prepared.normalized] 

714 if prepared 

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

716 ) 

717 eggs = ( 

718 self.eggs[prepared.legacy_normalized] 

719 if prepared 

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

721 ) 

722 return itertools.chain(infos, eggs) 

723 

724 

725class Prepared: 

726 """ 

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

728 """ 

729 

730 normalized = None 

731 legacy_normalized = None 

732 

733 def __init__(self, name): 

734 self.name = name 

735 if name is None: 

736 return 

737 self.normalized = self.normalize(name) 

738 self.legacy_normalized = self.legacy_normalize(name) 

739 

740 @staticmethod 

741 def normalize(name): 

742 """ 

743 PEP 503 normalization plus dashes as underscores. 

744 """ 

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

746 

747 @staticmethod 

748 def legacy_normalize(name): 

749 """ 

750 Normalize the package name as found in the convention in 

751 older packaging tools versions and specs. 

752 """ 

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

754 

755 def __bool__(self): 

756 return bool(self.name) 

757 

758 

759@install 

760class MetadataPathFinder(NullFinder, DistributionFinder): 

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

762 

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

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

765 """ 

766 

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

768 """ 

769 Find distributions. 

770 

771 Return an iterable of all Distribution instances capable of 

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

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

774 of directories ``context.path``. 

775 """ 

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

777 return map(PathDistribution, found) 

778 

779 @classmethod 

780 def _search_paths(cls, name, paths): 

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

782 prepared = Prepared(name) 

783 return itertools.chain.from_iterable( 

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

785 ) 

786 

787 def invalidate_caches(cls): 

788 FastPath.__new__.cache_clear() 

789 

790 

791class PathDistribution(Distribution): 

792 def __init__(self, path: SimplePath): 

793 """Construct a distribution. 

794 

795 :param path: SimplePath indicating the metadata directory. 

796 """ 

797 self._path = path 

798 

799 def read_text(self, filename): 

800 with suppress( 

801 FileNotFoundError, 

802 IsADirectoryError, 

803 KeyError, 

804 NotADirectoryError, 

805 PermissionError, 

806 ): 

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

808 

809 read_text.__doc__ = Distribution.read_text.__doc__ 

810 

811 def locate_file(self, path): 

812 return self._path.parent / path 

813 

814 @property 

815 def _normalized_name(self): 

816 """ 

817 Performance optimization: where possible, resolve the 

818 normalized name from the file system path. 

819 """ 

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

821 return ( 

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

823 or super()._normalized_name 

824 ) 

825 

826 @staticmethod 

827 def _name_from_stem(stem): 

828 """ 

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

830 'foo' 

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

832 'CherryPy' 

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

834 'face' 

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

836 """ 

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

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

839 return 

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

841 return name 

842 

843 

844def distribution(distribution_name): 

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

846 

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

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

849 """ 

850 return Distribution.from_name(distribution_name) 

851 

852 

853def distributions(**kwargs): 

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

855 

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

857 """ 

858 return Distribution.discover(**kwargs) 

859 

860 

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

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

863 

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

865 :return: A PackageMetadata containing the parsed metadata. 

866 """ 

867 return Distribution.from_name(distribution_name).metadata 

868 

869 

870def version(distribution_name): 

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

872 

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

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

875 "Version" metadata key. 

876 """ 

877 return distribution(distribution_name).version 

878 

879 

880_unique = functools.partial( 

881 unique_everseen, 

882 key=_py39compat.normalized_name, 

883) 

884""" 

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

886""" 

887 

888 

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

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

891 

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

893 result to entry points matching those properties (see 

894 EntryPoints.select()). 

895 

896 :return: EntryPoints for all installed packages. 

897 """ 

898 eps = itertools.chain.from_iterable( 

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

900 ) 

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

902 

903 

904def files(distribution_name): 

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

906 

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

908 :return: List of files composing the distribution. 

909 """ 

910 return distribution(distribution_name).files 

911 

912 

913def requires(distribution_name): 

914 """ 

915 Return a list of requirements for the named package. 

916 

917 :return: An iterator of requirements, suitable for 

918 packaging.requirement.Requirement. 

919 """ 

920 return distribution(distribution_name).requires 

921 

922 

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

924 """ 

925 Return a mapping of top-level packages to their 

926 distributions. 

927 

928 >>> import collections.abc 

929 >>> pkgs = packages_distributions() 

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

931 True 

932 """ 

933 pkg_to_dist = collections.defaultdict(list) 

934 for dist in distributions(): 

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

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

937 return dict(pkg_to_dist) 

938 

939 

940def _top_level_declared(dist): 

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

942 

943 

944def _top_level_inferred(dist): 

945 opt_names = { 

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

947 for f in always_iterable(dist.files) 

948 } 

949 

950 @pass_none 

951 def importable_name(name): 

952 return '.' not in name 

953 

954 return filter(importable_name, opt_names)