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

427 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:35 +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, Union 

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 _warn = functools.partial( 

143 warnings.warn, 

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

145 DeprecationWarning, 

146 stacklevel=pypy_partial(2), 

147 ) 

148 

149 def __getitem__(self, item): 

150 self._warn() 

151 return self._key()[item] 

152 

153 

154class EntryPoint(DeprecatedTuple): 

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

156 

157 See `the packaging docs on entry points 

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

159 for more information. 

160 

161 >>> ep = EntryPoint( 

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

163 >>> ep.module 

164 'package.module' 

165 >>> ep.attr 

166 'attr' 

167 >>> ep.extras 

168 ['extra1', 'extra2'] 

169 """ 

170 

171 pattern = re.compile( 

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

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

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

175 ) 

176 """ 

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

178 which might look like: 

179 

180 - module 

181 - package.module 

182 - package.module:attribute 

183 - package.module:object.attribute 

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

185 

186 Other combinations are possible as well. 

187 

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

189 following the attr, and following any extras. 

190 """ 

191 

192 name: str 

193 value: str 

194 group: str 

195 

196 dist: Optional['Distribution'] = None 

197 

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

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

200 

201 def load(self): 

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

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

204 return the named object. 

205 """ 

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

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

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

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

210 

211 @property 

212 def module(self): 

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

214 return match.group('module') 

215 

216 @property 

217 def attr(self): 

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

219 return match.group('attr') 

220 

221 @property 

222 def extras(self): 

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

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

225 

226 def _for(self, dist): 

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

228 return self 

229 

230 def __iter__(self): 

231 """ 

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

233 """ 

234 msg = ( 

235 "Construction of dict of EntryPoints is deprecated in " 

236 "favor of EntryPoints." 

237 ) 

238 warnings.warn(msg, DeprecationWarning) 

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

240 

241 def matches(self, **params): 

242 """ 

243 EntryPoint matches the given parameters. 

244 

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

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

247 True 

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

249 True 

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

251 False 

252 >>> ep.matches() 

253 True 

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

255 True 

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

257 True 

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

259 True 

260 """ 

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

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

263 

264 def _key(self): 

265 return self.name, self.value, self.group 

266 

267 def __lt__(self, other): 

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

269 

270 def __eq__(self, other): 

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

272 

273 def __setattr__(self, name, value): 

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

275 

276 def __repr__(self): 

277 return ( 

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

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

280 ) 

281 

282 def __hash__(self): 

283 return hash(self._key()) 

284 

285 

286class DeprecatedList(list): 

287 """ 

288 Allow an otherwise immutable object to implement mutability 

289 for compatibility. 

290 

291 >>> recwarn = getfixture('recwarn') 

292 >>> dl = DeprecatedList(range(3)) 

293 >>> dl[0] = 1 

294 >>> dl.append(3) 

295 >>> del dl[3] 

296 >>> dl.reverse() 

297 >>> dl.sort() 

298 >>> dl.extend([4]) 

299 >>> dl.pop(-1) 

300 4 

301 >>> dl.remove(1) 

302 >>> dl += [5] 

303 >>> dl + [6] 

304 [1, 2, 5, 6] 

305 >>> dl + (6,) 

306 [1, 2, 5, 6] 

307 >>> dl.insert(0, 0) 

308 >>> dl 

309 [0, 1, 2, 5] 

310 >>> dl == [0, 1, 2, 5] 

311 True 

312 >>> dl == (0, 1, 2, 5) 

313 True 

314 >>> len(recwarn) 

315 1 

316 """ 

317 

318 __slots__ = () 

319 

320 _warn = functools.partial( 

321 warnings.warn, 

322 "EntryPoints list interface is deprecated. Cast to list if needed.", 

323 DeprecationWarning, 

324 stacklevel=pypy_partial(2), 

325 ) 

326 

327 def _wrap_deprecated_method(method_name: str): # type: ignore 

328 def wrapped(self, *args, **kwargs): 

329 self._warn() 

330 return getattr(super(), method_name)(*args, **kwargs) 

331 

332 return method_name, wrapped 

333 

334 locals().update( 

335 map( 

336 _wrap_deprecated_method, 

337 '__setitem__ __delitem__ append reverse extend pop remove ' 

338 '__iadd__ insert sort'.split(), 

339 ) 

340 ) 

341 

342 def __add__(self, other): 

343 if not isinstance(other, tuple): 

344 self._warn() 

345 other = tuple(other) 

346 return self.__class__(tuple(self) + other) 

347 

348 def __eq__(self, other): 

349 if not isinstance(other, tuple): 

350 self._warn() 

351 other = tuple(other) 

352 

353 return tuple(self).__eq__(other) 

354 

355 

356class EntryPoints(DeprecatedList): 

357 """ 

358 An immutable collection of selectable EntryPoint objects. 

359 """ 

360 

361 __slots__ = () 

362 

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

364 """ 

365 Get the EntryPoint in self matching name. 

366 """ 

367 if isinstance(name, int): 

368 warnings.warn( 

369 "Accessing entry points by index is deprecated. " 

370 "Cast to tuple if needed.", 

371 DeprecationWarning, 

372 stacklevel=2, 

373 ) 

374 return super().__getitem__(name) 

375 try: 

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

377 except StopIteration: 

378 raise KeyError(name) 

379 

380 def select(self, **params): 

381 """ 

382 Select entry points from self that match the 

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

384 """ 

385 candidates = (_py39compat.ep_matches(ep, **params) for ep in self) 

386 return EntryPoints(ep for ep, predicate in candidates if predicate) 

387 

388 @property 

389 def names(self): 

390 """ 

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

392 """ 

393 return {ep.name for ep in self} 

394 

395 @property 

396 def groups(self): 

397 """ 

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

399 

400 For coverage while SelectableGroups is present. 

401 >>> EntryPoints().groups 

402 set() 

403 """ 

404 return {ep.group for ep in self} 

405 

406 @classmethod 

407 def _from_text_for(cls, text, dist): 

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

409 

410 @staticmethod 

411 def _from_text(text): 

412 return ( 

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

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

415 ) 

416 

417 

418class Deprecated: 

419 """ 

420 Compatibility add-in for mapping to indicate that 

421 mapping behavior is deprecated. 

422 

423 >>> recwarn = getfixture('recwarn') 

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

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

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

427 >>> dd['foo'] 

428 'bar' 

429 >>> list(dd) 

430 ['foo'] 

431 >>> list(dd.keys()) 

432 ['foo'] 

433 >>> 'foo' in dd 

434 True 

435 >>> list(dd.values()) 

436 ['bar'] 

437 >>> len(recwarn) 

438 1 

439 """ 

440 

441 _warn = functools.partial( 

442 warnings.warn, 

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

444 DeprecationWarning, 

445 stacklevel=pypy_partial(2), 

446 ) 

447 

448 def __getitem__(self, name): 

449 self._warn() 

450 return super().__getitem__(name) 

451 

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

453 self._warn() 

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

455 

456 def __iter__(self): 

457 self._warn() 

458 return super().__iter__() 

459 

460 def __contains__(self, *args): 

461 self._warn() 

462 return super().__contains__(*args) 

463 

464 def keys(self): 

465 self._warn() 

466 return super().keys() 

467 

468 def values(self): 

469 self._warn() 

470 return super().values() 

471 

472 

473class SelectableGroups(Deprecated, dict): 

474 """ 

475 A backward- and forward-compatible result from 

476 entry_points that fully implements the dict interface. 

477 """ 

478 

479 @classmethod 

480 def load(cls, eps): 

481 by_group = operator.attrgetter('group') 

482 ordered = sorted(eps, key=by_group) 

483 grouped = itertools.groupby(ordered, by_group) 

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

485 

486 @property 

487 def _all(self): 

488 """ 

489 Reconstruct a list of all entrypoints from the groups. 

490 """ 

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

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

493 

494 @property 

495 def groups(self): 

496 return self._all.groups 

497 

498 @property 

499 def names(self): 

500 """ 

501 for coverage: 

502 >>> SelectableGroups().names 

503 set() 

504 """ 

505 return self._all.names 

506 

507 def select(self, **params): 

508 if not params: 

509 return self 

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

511 

512 

513class PackagePath(pathlib.PurePosixPath): 

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

515 

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

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

518 return stream.read() 

519 

520 def read_binary(self): 

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

522 return stream.read() 

523 

524 def locate(self): 

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

526 return self.dist.locate_file(self) 

527 

528 

529class FileHash: 

530 def __init__(self, spec): 

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

532 

533 def __repr__(self): 

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

535 

536 

537class Distribution: 

538 """A Python distribution package.""" 

539 

540 @abc.abstractmethod 

541 def read_text(self, filename): 

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

543 

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

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

546 """ 

547 

548 @abc.abstractmethod 

549 def locate_file(self, path): 

550 """ 

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

552 to it. 

553 """ 

554 

555 @classmethod 

556 def from_name(cls, name: str): 

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

558 

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

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

561 package, if found. 

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

563 metadata cannot be found. 

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

565 """ 

566 if not name: 

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

568 try: 

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

570 except StopIteration: 

571 raise PackageNotFoundError(name) 

572 

573 @classmethod 

574 def discover(cls, **kwargs): 

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

576 

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

578 a context. 

579 

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

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

582 """ 

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

584 if context and kwargs: 

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

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

587 return itertools.chain.from_iterable( 

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

589 ) 

590 

591 @staticmethod 

592 def at(path): 

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

594 

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

596 :return: a concrete Distribution instance for the path 

597 """ 

598 return PathDistribution(pathlib.Path(path)) 

599 

600 @staticmethod 

601 def _discover_resolvers(): 

602 """Search the meta_path for resolvers.""" 

603 declared = ( 

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

605 ) 

606 return filter(None, declared) 

607 

608 @property 

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

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

611 

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

613 metadata. See PEP 566 for details. 

614 """ 

615 text = ( 

616 self.read_text('METADATA') 

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

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

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

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

621 or self.read_text('') 

622 ) 

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

624 

625 @property 

626 def name(self): 

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

628 return self.metadata['Name'] 

629 

630 @property 

631 def _normalized_name(self): 

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

633 return Prepared.normalize(self.name) 

634 

635 @property 

636 def version(self): 

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

638 return self.metadata['Version'] 

639 

640 @property 

641 def entry_points(self): 

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

643 

644 @property 

645 def files(self): 

646 """Files in this distribution. 

647 

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

649 

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

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

652 missing. 

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

654 """ 

655 

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

657 result = PackagePath(name) 

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

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

660 result.dist = self 

661 return result 

662 

663 @pass_none 

664 def make_files(lines): 

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

666 

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

668 

669 def _read_files_distinfo(self): 

670 """ 

671 Read the lines of RECORD 

672 """ 

673 text = self.read_text('RECORD') 

674 return text and text.splitlines() 

675 

676 def _read_files_egginfo(self): 

677 """ 

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

679 in quotes. 

680 """ 

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

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

683 

684 @property 

685 def requires(self): 

686 """Generated requirements specified for this Distribution""" 

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

688 return reqs and list(reqs) 

689 

690 def _read_dist_info_reqs(self): 

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

692 

693 def _read_egg_info_reqs(self): 

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

695 return pass_none(self._deps_from_requires_text)(source) 

696 

697 @classmethod 

698 def _deps_from_requires_text(cls, source): 

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

700 

701 @staticmethod 

702 def _convert_egg_info_reqs_to_simple_reqs(sections): 

703 """ 

704 Historically, setuptools would solicit and store 'extra' 

705 requirements, including those with environment markers, 

706 in separate sections. More modern tools expect each 

707 dependency to be defined separately, with any relevant 

708 extras and environment markers attached directly to that 

709 requirement. This method converts the former to the 

710 latter. See _test_deps_from_requires_text for an example. 

711 """ 

712 

713 def make_condition(name): 

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

715 

716 def quoted_marker(section): 

717 section = section or '' 

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

719 if extra and markers: 

720 markers = f'({markers})' 

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

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

723 

724 def url_req_space(req): 

725 """ 

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

727 Ref python/importlib_metadata#357. 

728 """ 

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

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

731 

732 for section in sections: 

733 space = url_req_space(section.value) 

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

735 

736 

737class DistributionFinder(MetaPathFinder): 

738 """ 

739 A MetaPathFinder capable of discovering installed distributions. 

740 """ 

741 

742 class Context: 

743 """ 

744 Keyword arguments presented by the caller to 

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

746 to narrow the scope of a search for distributions 

747 in all DistributionFinders. 

748 

749 Each DistributionFinder may expect any parameters 

750 and should attempt to honor the canonical 

751 parameters defined below when appropriate. 

752 """ 

753 

754 name = None 

755 """ 

756 Specific name for which a distribution finder should match. 

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

758 """ 

759 

760 def __init__(self, **kwargs): 

761 vars(self).update(kwargs) 

762 

763 @property 

764 def path(self): 

765 """ 

766 The sequence of directory path that a distribution finder 

767 should search. 

768 

769 Typically refers to Python installed package paths such as 

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

771 """ 

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

773 

774 @abc.abstractmethod 

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

776 """ 

777 Find distributions. 

778 

779 Return an iterable of all Distribution instances capable of 

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

781 a DistributionFinder.Context instance. 

782 """ 

783 

784 

785class FastPath: 

786 """ 

787 Micro-optimized class for searching a path for 

788 children. 

789 

790 >>> FastPath('').children() 

791 ['...'] 

792 """ 

793 

794 @functools.lru_cache() # type: ignore 

795 def __new__(cls, root): 

796 return super().__new__(cls) 

797 

798 def __init__(self, root): 

799 self.root = root 

800 

801 def joinpath(self, child): 

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

803 

804 def children(self): 

805 with suppress(Exception): 

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

807 with suppress(Exception): 

808 return self.zip_children() 

809 return [] 

810 

811 def zip_children(self): 

812 zip_path = zipp.Path(self.root) 

813 names = zip_path.root.namelist() 

814 self.joinpath = zip_path.joinpath 

815 

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

817 

818 def search(self, name): 

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

820 

821 @property 

822 def mtime(self): 

823 with suppress(OSError): 

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

825 self.lookup.cache_clear() 

826 

827 @method_cache 

828 def lookup(self, mtime): 

829 return Lookup(self) 

830 

831 

832class Lookup: 

833 def __init__(self, path: FastPath): 

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

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

836 self.infos = FreezableDefaultDict(list) 

837 self.eggs = FreezableDefaultDict(list) 

838 

839 for child in path.children(): 

840 low = child.lower() 

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

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

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

844 normalized = Prepared.normalize(name) 

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

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

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

848 legacy_normalized = Prepared.legacy_normalize(name) 

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

850 

851 self.infos.freeze() 

852 self.eggs.freeze() 

853 

854 def search(self, prepared): 

855 infos = ( 

856 self.infos[prepared.normalized] 

857 if prepared 

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

859 ) 

860 eggs = ( 

861 self.eggs[prepared.legacy_normalized] 

862 if prepared 

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

864 ) 

865 return itertools.chain(infos, eggs) 

866 

867 

868class Prepared: 

869 """ 

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

871 """ 

872 

873 normalized = None 

874 legacy_normalized = None 

875 

876 def __init__(self, name): 

877 self.name = name 

878 if name is None: 

879 return 

880 self.normalized = self.normalize(name) 

881 self.legacy_normalized = self.legacy_normalize(name) 

882 

883 @staticmethod 

884 def normalize(name): 

885 """ 

886 PEP 503 normalization plus dashes as underscores. 

887 """ 

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

889 

890 @staticmethod 

891 def legacy_normalize(name): 

892 """ 

893 Normalize the package name as found in the convention in 

894 older packaging tools versions and specs. 

895 """ 

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

897 

898 def __bool__(self): 

899 return bool(self.name) 

900 

901 

902@install 

903class MetadataPathFinder(NullFinder, DistributionFinder): 

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

905 

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

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

908 """ 

909 

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

911 """ 

912 Find distributions. 

913 

914 Return an iterable of all Distribution instances capable of 

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

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

917 of directories ``context.path``. 

918 """ 

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

920 return map(PathDistribution, found) 

921 

922 @classmethod 

923 def _search_paths(cls, name, paths): 

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

925 prepared = Prepared(name) 

926 return itertools.chain.from_iterable( 

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

928 ) 

929 

930 def invalidate_caches(cls): 

931 FastPath.__new__.cache_clear() 

932 

933 

934class PathDistribution(Distribution): 

935 def __init__(self, path: SimplePath): 

936 """Construct a distribution. 

937 

938 :param path: SimplePath indicating the metadata directory. 

939 """ 

940 self._path = path 

941 

942 def read_text(self, filename): 

943 with suppress( 

944 FileNotFoundError, 

945 IsADirectoryError, 

946 KeyError, 

947 NotADirectoryError, 

948 PermissionError, 

949 ): 

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

951 

952 read_text.__doc__ = Distribution.read_text.__doc__ 

953 

954 def locate_file(self, path): 

955 return self._path.parent / path 

956 

957 @property 

958 def _normalized_name(self): 

959 """ 

960 Performance optimization: where possible, resolve the 

961 normalized name from the file system path. 

962 """ 

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

964 return ( 

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

966 or super()._normalized_name 

967 ) 

968 

969 @staticmethod 

970 def _name_from_stem(stem): 

971 """ 

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

973 'foo' 

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

975 'CherryPy' 

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

977 'face' 

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

979 """ 

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

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

982 return 

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

984 return name 

985 

986 

987def distribution(distribution_name): 

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

989 

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

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

992 """ 

993 return Distribution.from_name(distribution_name) 

994 

995 

996def distributions(**kwargs): 

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

998 

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

1000 """ 

1001 return Distribution.discover(**kwargs) 

1002 

1003 

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

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

1006 

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

1008 :return: A PackageMetadata containing the parsed metadata. 

1009 """ 

1010 return Distribution.from_name(distribution_name).metadata 

1011 

1012 

1013def version(distribution_name): 

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

1015 

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

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

1018 "Version" metadata key. 

1019 """ 

1020 return distribution(distribution_name).version 

1021 

1022 

1023_unique = functools.partial( 

1024 unique_everseen, 

1025 key=_py39compat.normalized_name, 

1026) 

1027""" 

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

1029""" 

1030 

1031 

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

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

1034 

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

1036 result to entry points matching those properties (see 

1037 EntryPoints.select()). 

1038 

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

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

1041 will return ``EntryPoints`` instead of ``SelectableGroups`` 

1042 even when no selection parameters are supplied. 

1043 

1044 For maximum future compatibility, pass selection parameters 

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

1046 

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

1048 """ 

1049 eps = itertools.chain.from_iterable( 

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

1051 ) 

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

1053 

1054 

1055def files(distribution_name): 

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

1057 

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

1059 :return: List of files composing the distribution. 

1060 """ 

1061 return distribution(distribution_name).files 

1062 

1063 

1064def requires(distribution_name): 

1065 """ 

1066 Return a list of requirements for the named package. 

1067 

1068 :return: An iterator of requirements, suitable for 

1069 packaging.requirement.Requirement. 

1070 """ 

1071 return distribution(distribution_name).requires 

1072 

1073 

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

1075 """ 

1076 Return a mapping of top-level packages to their 

1077 distributions. 

1078 

1079 >>> import collections.abc 

1080 >>> pkgs = packages_distributions() 

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

1082 True 

1083 """ 

1084 pkg_to_dist = collections.defaultdict(list) 

1085 for dist in distributions(): 

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

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

1088 return dict(pkg_to_dist) 

1089 

1090 

1091def _top_level_declared(dist): 

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

1093 

1094 

1095def _top_level_inferred(dist): 

1096 return { 

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

1098 for f in always_iterable(dist.files) 

1099 if f.suffix == ".py" 

1100 }