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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

395 statements  

1from __future__ import annotations 

2 

3import os 

4import re 

5import abc 

6import sys 

7import json 

8import zipp 

9import email 

10import types 

11import inspect 

12import pathlib 

13import operator 

14import textwrap 

15import functools 

16import itertools 

17import posixpath 

18import collections 

19 

20from . import _meta 

21from .compat import py39, py311 

22from ._collections import FreezableDefaultDict, Pair 

23from ._compat import ( 

24 NullFinder, 

25 install, 

26) 

27from ._functools import method_cache, pass_none 

28from ._itertools import always_iterable, bucket, unique_everseen 

29from ._meta import PackageMetadata, SimplePath 

30 

31from contextlib import suppress 

32from importlib import import_module 

33from importlib.abc import MetaPathFinder 

34from itertools import starmap 

35from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast 

36 

37__all__ = [ 

38 'Distribution', 

39 'DistributionFinder', 

40 'PackageMetadata', 

41 'PackageNotFoundError', 

42 'SimplePath', 

43 'distribution', 

44 'distributions', 

45 'entry_points', 

46 'files', 

47 'metadata', 

48 'packages_distributions', 

49 'requires', 

50 'version', 

51] 

52 

53 

54class PackageNotFoundError(ModuleNotFoundError): 

55 """The package was not found.""" 

56 

57 def __str__(self) -> str: 

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

59 

60 @property 

61 def name(self) -> str: # type: ignore[override] 

62 (name,) = self.args 

63 return name 

64 

65 

66class Sectioned: 

67 """ 

68 A simple entry point config parser for performance 

69 

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

71 ... print(item) 

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

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

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

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

76 

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

78 >>> item = next(res) 

79 >>> item.name 

80 'sec1' 

81 >>> item.value 

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

83 >>> item = next(res) 

84 >>> item.value 

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

86 >>> item = next(res) 

87 >>> item.name 

88 'sec2' 

89 >>> item.value 

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

91 >>> list(res) 

92 [] 

93 """ 

94 

95 _sample = textwrap.dedent( 

96 """ 

97 [sec1] 

98 # comments ignored 

99 a = 1 

100 b = 2 

101 

102 [sec2] 

103 a = 2 

104 """ 

105 ).lstrip() 

106 

107 @classmethod 

108 def section_pairs(cls, text): 

109 return ( 

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

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

112 if section.name is not None 

113 ) 

114 

115 @staticmethod 

116 def read(text, filter_=None): 

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

118 name = None 

119 for value in lines: 

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

121 if section_match: 

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

123 continue 

124 yield Pair(name, value) 

125 

126 @staticmethod 

127 def valid(line: str): 

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

129 

130 

131class EntryPoint: 

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

133 

134 See `the packaging docs on entry points 

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

136 for more information. 

137 

138 >>> ep = EntryPoint( 

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

140 >>> ep.module 

141 'package.module' 

142 >>> ep.attr 

143 'attr' 

144 >>> ep.extras 

145 ['extra1', 'extra2'] 

146 """ 

147 

148 pattern = re.compile( 

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

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

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

152 ) 

153 """ 

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

155 which might look like: 

156 

157 - module 

158 - package.module 

159 - package.module:attribute 

160 - package.module:object.attribute 

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

162 

163 Other combinations are possible as well. 

164 

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

166 following the attr, and following any extras. 

167 """ 

168 

169 name: str 

170 value: str 

171 group: str 

172 

173 dist: Optional[Distribution] = None 

174 

175 def __init__(self, name: str, value: str, group: str) -> None: 

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

177 

178 def load(self) -> Any: 

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

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

181 return the named object. 

182 """ 

183 match = cast(Match, self.pattern.match(self.value)) 

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

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

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

187 

188 @property 

189 def module(self) -> str: 

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

191 assert match is not None 

192 return match.group('module') 

193 

194 @property 

195 def attr(self) -> str: 

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

197 assert match is not None 

198 return match.group('attr') 

199 

200 @property 

201 def extras(self) -> List[str]: 

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

203 assert match is not None 

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

205 

206 def _for(self, dist): 

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

208 return self 

209 

210 def matches(self, **params): 

211 """ 

212 EntryPoint matches the given parameters. 

213 

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

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

216 True 

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

218 True 

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

220 False 

221 >>> ep.matches() 

222 True 

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

224 True 

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

226 True 

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

228 True 

229 """ 

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

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

232 

233 def _key(self): 

234 return self.name, self.value, self.group 

235 

236 def __lt__(self, other): 

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

238 

239 def __eq__(self, other): 

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

241 

242 def __setattr__(self, name, value): 

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

244 

245 def __repr__(self): 

246 return ( 

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

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

249 ) 

250 

251 def __hash__(self) -> int: 

252 return hash(self._key()) 

253 

254 

255class EntryPoints(tuple): 

256 """ 

257 An immutable collection of selectable EntryPoint objects. 

258 """ 

259 

260 __slots__ = () 

261 

262 def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] 

263 """ 

264 Get the EntryPoint in self matching name. 

265 """ 

266 try: 

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

268 except StopIteration: 

269 raise KeyError(name) 

270 

271 def __repr__(self): 

272 """ 

273 Repr with classname and tuple constructor to 

274 signal that we deviate from regular tuple behavior. 

275 """ 

276 return '%s(%r)' % (self.__class__.__name__, tuple(self)) 

277 

278 def select(self, **params) -> EntryPoints: 

279 """ 

280 Select entry points from self that match the 

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

282 """ 

283 return EntryPoints(ep for ep in self if py39.ep_matches(ep, **params)) 

284 

285 @property 

286 def names(self) -> Set[str]: 

287 """ 

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

289 """ 

290 return {ep.name for ep in self} 

291 

292 @property 

293 def groups(self) -> Set[str]: 

294 """ 

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

296 """ 

297 return {ep.group for ep in self} 

298 

299 @classmethod 

300 def _from_text_for(cls, text, dist): 

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

302 

303 @staticmethod 

304 def _from_text(text): 

305 return ( 

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

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

308 ) 

309 

310 

311class PackagePath(pathlib.PurePosixPath): 

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

313 

314 hash: Optional[FileHash] 

315 size: int 

316 dist: Distribution 

317 

318 def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override] 

319 return self.locate().read_text(encoding=encoding) 

320 

321 def read_binary(self) -> bytes: 

322 return self.locate().read_bytes() 

323 

324 def locate(self) -> SimplePath: 

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

326 return self.dist.locate_file(self) 

327 

328 

329class FileHash: 

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

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

332 

333 def __repr__(self) -> str: 

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

335 

336 

337class Distribution(metaclass=abc.ABCMeta): 

338 """ 

339 An abstract Python distribution package. 

340 

341 Custom providers may derive from this class and define 

342 the abstract methods to provide a concrete implementation 

343 for their environment. Some providers may opt to override 

344 the default implementation of some properties to bypass 

345 the file-reading mechanism. 

346 """ 

347 

348 @abc.abstractmethod 

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

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

351 

352 Python distribution metadata is organized by blobs of text 

353 typically represented as "files" in the metadata directory 

354 (e.g. package-1.0.dist-info). These files include things 

355 like: 

356 

357 - METADATA: The distribution metadata including fields 

358 like Name and Version and Description. 

359 - entry_points.txt: A series of entry points as defined in 

360 `the entry points spec <https://packaging.python.org/en/latest/specifications/entry-points/#file-format>`_. 

361 - RECORD: A record of files according to 

362 `this recording spec <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#the-record-file>`_. 

363 

364 A package may provide any set of files, including those 

365 not listed here or none at all. 

366 

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

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

369 """ 

370 

371 @abc.abstractmethod 

372 def locate_file(self, path: str | os.PathLike[str]) -> SimplePath: 

373 """ 

374 Given a path to a file in this distribution, return a SimplePath 

375 to it. 

376 """ 

377 

378 @classmethod 

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

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

381 

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

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

384 package, if found. 

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

386 metadata cannot be found. 

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

388 """ 

389 if not name: 

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

391 try: 

392 return next(iter(cls._prefer_valid(cls.discover(name=name)))) 

393 except StopIteration: 

394 raise PackageNotFoundError(name) 

395 

396 @classmethod 

397 def discover( 

398 cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs 

399 ) -> Iterable[Distribution]: 

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

401 

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

403 a context. 

404 

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

406 :return: Iterable of Distribution objects for packages matching 

407 the context. 

408 """ 

409 if context and kwargs: 

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

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

412 return itertools.chain.from_iterable( 

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

414 ) 

415 

416 @staticmethod 

417 def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]: 

418 """ 

419 Prefer (move to the front) distributions that have metadata. 

420 

421 Ref python/importlib_resources#489. 

422 """ 

423 buckets = bucket(dists, lambda dist: bool(dist.metadata)) 

424 return itertools.chain(buckets[True], buckets[False]) 

425 

426 @staticmethod 

427 def at(path: str | os.PathLike[str]) -> Distribution: 

428 """Return a Distribution for the indicated metadata path. 

429 

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

431 :return: a concrete Distribution instance for the path 

432 """ 

433 return PathDistribution(pathlib.Path(path)) 

434 

435 @staticmethod 

436 def _discover_resolvers(): 

437 """Search the meta_path for resolvers (MetadataPathFinders).""" 

438 declared = ( 

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

440 ) 

441 return filter(None, declared) 

442 

443 @property 

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

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

446 

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

448 metadata per the 

449 `Core metadata specifications <https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata>`_. 

450 

451 Custom providers may provide the METADATA file or override this 

452 property. 

453 """ 

454 # deferred for performance (python/cpython#109829) 

455 from . import _adapters 

456 

457 opt_text = ( 

458 self.read_text('METADATA') 

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

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

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

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

463 or self.read_text('') 

464 ) 

465 text = cast(str, opt_text) 

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

467 

468 @property 

469 def name(self) -> str: 

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

471 return self.metadata['Name'] 

472 

473 @property 

474 def _normalized_name(self): 

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

476 return Prepared.normalize(self.name) 

477 

478 @property 

479 def version(self) -> str: 

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

481 return self.metadata['Version'] 

482 

483 @property 

484 def entry_points(self) -> EntryPoints: 

485 """ 

486 Return EntryPoints for this distribution. 

487 

488 Custom providers may provide the ``entry_points.txt`` file 

489 or override this property. 

490 """ 

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

492 

493 @property 

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

495 """Files in this distribution. 

496 

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

498 

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

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

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

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

503 

504 Custom providers are recommended to provide a "RECORD" file (in 

505 ``read_text``) or override this property to allow for callers to be 

506 able to resolve filenames provided by the package. 

507 """ 

508 

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

510 result = PackagePath(name) 

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

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

513 result.dist = self 

514 return result 

515 

516 @pass_none 

517 def make_files(lines): 

518 # Delay csv import, since Distribution.files is not as widely used 

519 # as other parts of importlib.metadata 

520 import csv 

521 

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

523 

524 @pass_none 

525 def skip_missing_files(package_paths): 

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

527 

528 return skip_missing_files( 

529 make_files( 

530 self._read_files_distinfo() 

531 or self._read_files_egginfo_installed() 

532 or self._read_files_egginfo_sources() 

533 ) 

534 ) 

535 

536 def _read_files_distinfo(self): 

537 """ 

538 Read the lines of RECORD. 

539 """ 

540 text = self.read_text('RECORD') 

541 return text and text.splitlines() 

542 

543 def _read_files_egginfo_installed(self): 

544 """ 

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

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

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

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

549 

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

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

552 Assume the file is accurate if it exists. 

553 """ 

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

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

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

557 # self._path. 

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

559 if not text or not subdir: 

560 return 

561 

562 paths = ( 

563 py311.relative_fix((subdir / name).resolve()) 

564 .relative_to(self.locate_file('').resolve(), walk_up=True) 

565 .as_posix() 

566 for name in text.splitlines() 

567 ) 

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

569 

570 def _read_files_egginfo_sources(self): 

571 """ 

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

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

574 might contain literal commas). 

575 

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

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

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

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

580 that are present after the package has been installed. 

581 """ 

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

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

584 

585 @property 

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

587 """Generated requirements specified for this Distribution""" 

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

589 return reqs and list(reqs) 

590 

591 def _read_dist_info_reqs(self): 

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

593 

594 def _read_egg_info_reqs(self): 

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

596 return pass_none(self._deps_from_requires_text)(source) 

597 

598 @classmethod 

599 def _deps_from_requires_text(cls, source): 

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

601 

602 @staticmethod 

603 def _convert_egg_info_reqs_to_simple_reqs(sections): 

604 """ 

605 Historically, setuptools would solicit and store 'extra' 

606 requirements, including those with environment markers, 

607 in separate sections. More modern tools expect each 

608 dependency to be defined separately, with any relevant 

609 extras and environment markers attached directly to that 

610 requirement. This method converts the former to the 

611 latter. See _test_deps_from_requires_text for an example. 

612 """ 

613 

614 def make_condition(name): 

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

616 

617 def quoted_marker(section): 

618 section = section or '' 

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

620 if extra and markers: 

621 markers = f'({markers})' 

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

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

624 

625 def url_req_space(req): 

626 """ 

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

628 Ref python/importlib_metadata#357. 

629 """ 

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

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

632 

633 for section in sections: 

634 space = url_req_space(section.value) 

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

636 

637 @property 

638 def origin(self): 

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

640 

641 def _load_json(self, filename): 

642 return pass_none(json.loads)( 

643 self.read_text(filename), 

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

645 ) 

646 

647 

648class DistributionFinder(MetaPathFinder): 

649 """ 

650 A MetaPathFinder capable of discovering installed distributions. 

651 

652 Custom providers should implement this interface in order to 

653 supply metadata. 

654 """ 

655 

656 class Context: 

657 """ 

658 Keyword arguments presented by the caller to 

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

660 to narrow the scope of a search for distributions 

661 in all DistributionFinders. 

662 

663 Each DistributionFinder may expect any parameters 

664 and should attempt to honor the canonical 

665 parameters defined below when appropriate. 

666 

667 This mechanism gives a custom provider a means to 

668 solicit additional details from the caller beyond 

669 "name" and "path" when searching distributions. 

670 For example, imagine a provider that exposes suites 

671 of packages in either a "public" or "private" ``realm``. 

672 A caller may wish to query only for distributions in 

673 a particular realm and could call 

674 ``distributions(realm="private")`` to signal to the 

675 custom provider to only include distributions from that 

676 realm. 

677 """ 

678 

679 name = None 

680 """ 

681 Specific name for which a distribution finder should match. 

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

683 """ 

684 

685 def __init__(self, **kwargs): 

686 vars(self).update(kwargs) 

687 

688 @property 

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

690 """ 

691 The sequence of directory path that a distribution finder 

692 should search. 

693 

694 Typically refers to Python installed package paths such as 

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

696 """ 

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

698 

699 @abc.abstractmethod 

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

701 """ 

702 Find distributions. 

703 

704 Return an iterable of all Distribution instances capable of 

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

706 a DistributionFinder.Context instance. 

707 """ 

708 

709 

710class FastPath: 

711 """ 

712 Micro-optimized class for searching a root for children. 

713 

714 Root is a path on the file system that may contain metadata 

715 directories either as natural directories or within a zip file. 

716 

717 >>> FastPath('').children() 

718 ['...'] 

719 

720 FastPath objects are cached and recycled for any given root. 

721 

722 >>> FastPath('foobar') is FastPath('foobar') 

723 True 

724 """ 

725 

726 @functools.lru_cache() # type: ignore 

727 def __new__(cls, root): 

728 return super().__new__(cls) 

729 

730 def __init__(self, root): 

731 self.root = root 

732 

733 def joinpath(self, child): 

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

735 

736 def children(self): 

737 with suppress(Exception): 

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

739 with suppress(Exception): 

740 return self.zip_children() 

741 return [] 

742 

743 def zip_children(self): 

744 zip_path = zipp.Path(self.root) 

745 names = zip_path.root.namelist() 

746 self.joinpath = zip_path.joinpath 

747 

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

749 

750 def search(self, name): 

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

752 

753 @property 

754 def mtime(self): 

755 with suppress(OSError): 

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

757 self.lookup.cache_clear() 

758 

759 @method_cache 

760 def lookup(self, mtime): 

761 return Lookup(self) 

762 

763 

764class Lookup: 

765 """ 

766 A micro-optimized class for searching a (fast) path for metadata. 

767 """ 

768 

769 def __init__(self, path: FastPath): 

770 """ 

771 Calculate all of the children representing metadata. 

772 

773 From the children in the path, calculate early all of the 

774 children that appear to represent metadata (infos) or legacy 

775 metadata (eggs). 

776 """ 

777 

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

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

780 self.infos = FreezableDefaultDict(list) 

781 self.eggs = FreezableDefaultDict(list) 

782 

783 for child in path.children(): 

784 low = child.lower() 

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

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

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

788 normalized = Prepared.normalize(name) 

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

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

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

792 legacy_normalized = Prepared.legacy_normalize(name) 

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

794 

795 self.infos.freeze() 

796 self.eggs.freeze() 

797 

798 def search(self, prepared: Prepared): 

799 """ 

800 Yield all infos and eggs matching the Prepared query. 

801 """ 

802 infos = ( 

803 self.infos[prepared.normalized] 

804 if prepared 

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

806 ) 

807 eggs = ( 

808 self.eggs[prepared.legacy_normalized] 

809 if prepared 

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

811 ) 

812 return itertools.chain(infos, eggs) 

813 

814 

815class Prepared: 

816 """ 

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

818 

819 Pre-calculates the normalization to prevent repeated operations. 

820 

821 >>> none = Prepared(None) 

822 >>> none.normalized 

823 >>> none.legacy_normalized 

824 >>> bool(none) 

825 False 

826 >>> sample = Prepared('Sample__Pkg-name.foo') 

827 >>> sample.normalized 

828 'sample_pkg_name_foo' 

829 >>> sample.legacy_normalized 

830 'sample__pkg_name.foo' 

831 >>> bool(sample) 

832 True 

833 """ 

834 

835 normalized = None 

836 legacy_normalized = None 

837 

838 def __init__(self, name: Optional[str]): 

839 self.name = name 

840 if name is None: 

841 return 

842 self.normalized = self.normalize(name) 

843 self.legacy_normalized = self.legacy_normalize(name) 

844 

845 @staticmethod 

846 def normalize(name): 

847 """ 

848 PEP 503 normalization plus dashes as underscores. 

849 """ 

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

851 

852 @staticmethod 

853 def legacy_normalize(name): 

854 """ 

855 Normalize the package name as found in the convention in 

856 older packaging tools versions and specs. 

857 """ 

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

859 

860 def __bool__(self): 

861 return bool(self.name) 

862 

863 

864@install 

865class MetadataPathFinder(NullFinder, DistributionFinder): 

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

867 

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

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

870 """ 

871 

872 @classmethod 

873 def find_distributions( 

874 cls, context=DistributionFinder.Context() 

875 ) -> Iterable[PathDistribution]: 

876 """ 

877 Find distributions. 

878 

879 Return an iterable of all Distribution instances capable of 

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

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

882 of directories ``context.path``. 

883 """ 

884 found = cls._search_paths(context.name, context.path) 

885 return map(PathDistribution, found) 

886 

887 @classmethod 

888 def _search_paths(cls, name, paths): 

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

890 prepared = Prepared(name) 

891 return itertools.chain.from_iterable( 

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

893 ) 

894 

895 @classmethod 

896 def invalidate_caches(cls) -> None: 

897 FastPath.__new__.cache_clear() 

898 

899 

900class PathDistribution(Distribution): 

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

902 """Construct a distribution. 

903 

904 :param path: SimplePath indicating the metadata directory. 

905 """ 

906 self._path = path 

907 

908 def read_text(self, filename: str | os.PathLike[str]) -> Optional[str]: 

909 with suppress( 

910 FileNotFoundError, 

911 IsADirectoryError, 

912 KeyError, 

913 NotADirectoryError, 

914 PermissionError, 

915 ): 

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

917 

918 return None 

919 

920 read_text.__doc__ = Distribution.read_text.__doc__ 

921 

922 def locate_file(self, path: str | os.PathLike[str]) -> SimplePath: 

923 return self._path.parent / path 

924 

925 @property 

926 def _normalized_name(self): 

927 """ 

928 Performance optimization: where possible, resolve the 

929 normalized name from the file system path. 

930 """ 

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

932 return ( 

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

934 or super()._normalized_name 

935 ) 

936 

937 @staticmethod 

938 def _name_from_stem(stem): 

939 """ 

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

941 'foo' 

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

943 'CherryPy' 

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

945 'face' 

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

947 """ 

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

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

950 return 

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

952 return name 

953 

954 

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

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

957 

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

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

960 """ 

961 return Distribution.from_name(distribution_name) 

962 

963 

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

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

966 

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

968 """ 

969 return Distribution.discover(**kwargs) 

970 

971 

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

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

974 

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

976 :return: A PackageMetadata containing the parsed metadata. 

977 """ 

978 return Distribution.from_name(distribution_name).metadata 

979 

980 

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

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

983 

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

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

986 "Version" metadata key. 

987 """ 

988 return distribution(distribution_name).version 

989 

990 

991_unique = functools.partial( 

992 unique_everseen, 

993 key=py39.normalized_name, 

994) 

995""" 

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

997""" 

998 

999 

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

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

1002 

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

1004 result to entry points matching those properties (see 

1005 EntryPoints.select()). 

1006 

1007 :return: EntryPoints for all installed packages. 

1008 """ 

1009 eps = itertools.chain.from_iterable( 

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

1011 ) 

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

1013 

1014 

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

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

1017 

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

1019 :return: List of files composing the distribution. 

1020 """ 

1021 return distribution(distribution_name).files 

1022 

1023 

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

1025 """ 

1026 Return a list of requirements for the named package. 

1027 

1028 :return: An iterable of requirements, suitable for 

1029 packaging.requirement.Requirement. 

1030 """ 

1031 return distribution(distribution_name).requires 

1032 

1033 

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

1035 """ 

1036 Return a mapping of top-level packages to their 

1037 distributions. 

1038 

1039 >>> import collections.abc 

1040 >>> pkgs = packages_distributions() 

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

1042 True 

1043 """ 

1044 pkg_to_dist = collections.defaultdict(list) 

1045 for dist in distributions(): 

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

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

1048 return dict(pkg_to_dist) 

1049 

1050 

1051def _top_level_declared(dist): 

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

1053 

1054 

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

1056 """ 

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

1058 """ 

1059 top, *rest = name.parts 

1060 return top if rest else None 

1061 

1062 

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

1064 """ 

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

1066 sys.path. 

1067 

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

1069 'foo' 

1070 >>> _get_toplevel_name(PackagePath('foo')) 

1071 'foo' 

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

1073 'foo' 

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

1075 'foo' 

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

1077 'foo.pth' 

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

1079 'foo.dist-info' 

1080 """ 

1081 return _topmost(name) or ( 

1082 # python/typeshed#10328 

1083 inspect.getmodulename(name) # type: ignore 

1084 or str(name) 

1085 ) 

1086 

1087 

1088def _top_level_inferred(dist): 

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

1090 

1091 def importable_name(name): 

1092 return '.' not in name 

1093 

1094 return filter(importable_name, opt_names)