logo

oasis-root

Compiled tree of Oasis Linux based on own branch at <https://hacktivis.me/git/oasis/> git clone https://anongit.hacktivis.me/git/oasis-root.git

__init__.py (29133B)


  1. import os
  2. import re
  3. import abc
  4. import csv
  5. import sys
  6. import email
  7. import pathlib
  8. import zipfile
  9. import operator
  10. import textwrap
  11. import warnings
  12. import functools
  13. import itertools
  14. import posixpath
  15. import collections
  16. from . import _adapters, _meta
  17. from ._meta import PackageMetadata
  18. from ._collections import FreezableDefaultDict, Pair
  19. from ._functools import method_cache
  20. from ._itertools import unique_everseen
  21. from ._meta import PackageMetadata, SimplePath
  22. from contextlib import suppress
  23. from importlib import import_module
  24. from importlib.abc import MetaPathFinder
  25. from itertools import starmap
  26. from typing import List, Mapping, Optional, Union
  27. __all__ = [
  28. 'Distribution',
  29. 'DistributionFinder',
  30. 'PackageMetadata',
  31. 'PackageNotFoundError',
  32. 'distribution',
  33. 'distributions',
  34. 'entry_points',
  35. 'files',
  36. 'metadata',
  37. 'packages_distributions',
  38. 'requires',
  39. 'version',
  40. ]
  41. class PackageNotFoundError(ModuleNotFoundError):
  42. """The package was not found."""
  43. def __str__(self):
  44. return f"No package metadata was found for {self.name}"
  45. @property
  46. def name(self):
  47. (name,) = self.args
  48. return name
  49. class Sectioned:
  50. """
  51. A simple entry point config parser for performance
  52. >>> for item in Sectioned.read(Sectioned._sample):
  53. ... print(item)
  54. Pair(name='sec1', value='# comments ignored')
  55. Pair(name='sec1', value='a = 1')
  56. Pair(name='sec1', value='b = 2')
  57. Pair(name='sec2', value='a = 2')
  58. >>> res = Sectioned.section_pairs(Sectioned._sample)
  59. >>> item = next(res)
  60. >>> item.name
  61. 'sec1'
  62. >>> item.value
  63. Pair(name='a', value='1')
  64. >>> item = next(res)
  65. >>> item.value
  66. Pair(name='b', value='2')
  67. >>> item = next(res)
  68. >>> item.name
  69. 'sec2'
  70. >>> item.value
  71. Pair(name='a', value='2')
  72. >>> list(res)
  73. []
  74. """
  75. _sample = textwrap.dedent(
  76. """
  77. [sec1]
  78. # comments ignored
  79. a = 1
  80. b = 2
  81. [sec2]
  82. a = 2
  83. """
  84. ).lstrip()
  85. @classmethod
  86. def section_pairs(cls, text):
  87. return (
  88. section._replace(value=Pair.parse(section.value))
  89. for section in cls.read(text, filter_=cls.valid)
  90. if section.name is not None
  91. )
  92. @staticmethod
  93. def read(text, filter_=None):
  94. lines = filter(filter_, map(str.strip, text.splitlines()))
  95. name = None
  96. for value in lines:
  97. section_match = value.startswith('[') and value.endswith(']')
  98. if section_match:
  99. name = value.strip('[]')
  100. continue
  101. yield Pair(name, value)
  102. @staticmethod
  103. def valid(line):
  104. return line and not line.startswith('#')
  105. class EntryPoint(
  106. collections.namedtuple('EntryPointBase', 'name value group')):
  107. """An entry point as defined by Python packaging conventions.
  108. See `the packaging docs on entry points
  109. <https://packaging.python.org/specifications/entry-points/>`_
  110. for more information.
  111. """
  112. pattern = re.compile(
  113. r'(?P<module>[\w.]+)\s*'
  114. r'(:\s*(?P<attr>[\w.]+))?\s*'
  115. r'(?P<extras>\[.*\])?\s*$'
  116. )
  117. """
  118. A regular expression describing the syntax for an entry point,
  119. which might look like:
  120. - module
  121. - package.module
  122. - package.module:attribute
  123. - package.module:object.attribute
  124. - package.module:attr [extra1, extra2]
  125. Other combinations are possible as well.
  126. The expression is lenient about whitespace around the ':',
  127. following the attr, and following any extras.
  128. """
  129. dist: Optional['Distribution'] = None
  130. def load(self):
  131. """Load the entry point from its definition. If only a module
  132. is indicated by the value, return that module. Otherwise,
  133. return the named object.
  134. """
  135. match = self.pattern.match(self.value)
  136. module = import_module(match.group('module'))
  137. attrs = filter(None, (match.group('attr') or '').split('.'))
  138. return functools.reduce(getattr, attrs, module)
  139. @property
  140. def module(self):
  141. match = self.pattern.match(self.value)
  142. return match.group('module')
  143. @property
  144. def attr(self):
  145. match = self.pattern.match(self.value)
  146. return match.group('attr')
  147. @property
  148. def extras(self):
  149. match = self.pattern.match(self.value)
  150. return list(re.finditer(r'\w+', match.group('extras') or ''))
  151. def _for(self, dist):
  152. self.dist = dist
  153. return self
  154. def __iter__(self):
  155. """
  156. Supply iter so one may construct dicts of EntryPoints by name.
  157. """
  158. msg = (
  159. "Construction of dict of EntryPoints is deprecated in "
  160. "favor of EntryPoints."
  161. )
  162. warnings.warn(msg, DeprecationWarning)
  163. return iter((self.name, self))
  164. def __reduce__(self):
  165. return (
  166. self.__class__,
  167. (self.name, self.value, self.group),
  168. )
  169. def matches(self, **params):
  170. attrs = (getattr(self, param) for param in params)
  171. return all(map(operator.eq, params.values(), attrs))
  172. class DeprecatedList(list):
  173. """
  174. Allow an otherwise immutable object to implement mutability
  175. for compatibility.
  176. >>> recwarn = getfixture('recwarn')
  177. >>> dl = DeprecatedList(range(3))
  178. >>> dl[0] = 1
  179. >>> dl.append(3)
  180. >>> del dl[3]
  181. >>> dl.reverse()
  182. >>> dl.sort()
  183. >>> dl.extend([4])
  184. >>> dl.pop(-1)
  185. 4
  186. >>> dl.remove(1)
  187. >>> dl += [5]
  188. >>> dl + [6]
  189. [1, 2, 5, 6]
  190. >>> dl + (6,)
  191. [1, 2, 5, 6]
  192. >>> dl.insert(0, 0)
  193. >>> dl
  194. [0, 1, 2, 5]
  195. >>> dl == [0, 1, 2, 5]
  196. True
  197. >>> dl == (0, 1, 2, 5)
  198. True
  199. >>> len(recwarn)
  200. 1
  201. """
  202. _warn = functools.partial(
  203. warnings.warn,
  204. "EntryPoints list interface is deprecated. Cast to list if needed.",
  205. DeprecationWarning,
  206. stacklevel=2,
  207. )
  208. def __setitem__(self, *args, **kwargs):
  209. self._warn()
  210. return super().__setitem__(*args, **kwargs)
  211. def __delitem__(self, *args, **kwargs):
  212. self._warn()
  213. return super().__delitem__(*args, **kwargs)
  214. def append(self, *args, **kwargs):
  215. self._warn()
  216. return super().append(*args, **kwargs)
  217. def reverse(self, *args, **kwargs):
  218. self._warn()
  219. return super().reverse(*args, **kwargs)
  220. def extend(self, *args, **kwargs):
  221. self._warn()
  222. return super().extend(*args, **kwargs)
  223. def pop(self, *args, **kwargs):
  224. self._warn()
  225. return super().pop(*args, **kwargs)
  226. def remove(self, *args, **kwargs):
  227. self._warn()
  228. return super().remove(*args, **kwargs)
  229. def __iadd__(self, *args, **kwargs):
  230. self._warn()
  231. return super().__iadd__(*args, **kwargs)
  232. def __add__(self, other):
  233. if not isinstance(other, tuple):
  234. self._warn()
  235. other = tuple(other)
  236. return self.__class__(tuple(self) + other)
  237. def insert(self, *args, **kwargs):
  238. self._warn()
  239. return super().insert(*args, **kwargs)
  240. def sort(self, *args, **kwargs):
  241. self._warn()
  242. return super().sort(*args, **kwargs)
  243. def __eq__(self, other):
  244. if not isinstance(other, tuple):
  245. self._warn()
  246. other = tuple(other)
  247. return tuple(self).__eq__(other)
  248. class EntryPoints(DeprecatedList):
  249. """
  250. An immutable collection of selectable EntryPoint objects.
  251. """
  252. __slots__ = ()
  253. def __getitem__(self, name): # -> EntryPoint:
  254. """
  255. Get the EntryPoint in self matching name.
  256. """
  257. if isinstance(name, int):
  258. warnings.warn(
  259. "Accessing entry points by index is deprecated. "
  260. "Cast to tuple if needed.",
  261. DeprecationWarning,
  262. stacklevel=2,
  263. )
  264. return super().__getitem__(name)
  265. try:
  266. return next(iter(self.select(name=name)))
  267. except StopIteration:
  268. raise KeyError(name)
  269. def select(self, **params):
  270. """
  271. Select entry points from self that match the
  272. given parameters (typically group and/or name).
  273. """
  274. return EntryPoints(ep for ep in self if ep.matches(**params))
  275. @property
  276. def names(self):
  277. """
  278. Return the set of all names of all entry points.
  279. """
  280. return set(ep.name for ep in self)
  281. @property
  282. def groups(self):
  283. """
  284. Return the set of all groups of all entry points.
  285. For coverage while SelectableGroups is present.
  286. >>> EntryPoints().groups
  287. set()
  288. """
  289. return set(ep.group for ep in self)
  290. @classmethod
  291. def _from_text_for(cls, text, dist):
  292. return cls(ep._for(dist) for ep in cls._from_text(text))
  293. @classmethod
  294. def _from_text(cls, text):
  295. return itertools.starmap(EntryPoint, cls._parse_groups(text or ''))
  296. @staticmethod
  297. def _parse_groups(text):
  298. return (
  299. (item.value.name, item.value.value, item.name)
  300. for item in Sectioned.section_pairs(text)
  301. )
  302. class Deprecated:
  303. """
  304. Compatibility add-in for mapping to indicate that
  305. mapping behavior is deprecated.
  306. >>> recwarn = getfixture('recwarn')
  307. >>> class DeprecatedDict(Deprecated, dict): pass
  308. >>> dd = DeprecatedDict(foo='bar')
  309. >>> dd.get('baz', None)
  310. >>> dd['foo']
  311. 'bar'
  312. >>> list(dd)
  313. ['foo']
  314. >>> list(dd.keys())
  315. ['foo']
  316. >>> 'foo' in dd
  317. True
  318. >>> list(dd.values())
  319. ['bar']
  320. >>> len(recwarn)
  321. 1
  322. """
  323. _warn = functools.partial(
  324. warnings.warn,
  325. "SelectableGroups dict interface is deprecated. Use select.",
  326. DeprecationWarning,
  327. stacklevel=2,
  328. )
  329. def __getitem__(self, name):
  330. self._warn()
  331. return super().__getitem__(name)
  332. def get(self, name, default=None):
  333. self._warn()
  334. return super().get(name, default)
  335. def __iter__(self):
  336. self._warn()
  337. return super().__iter__()
  338. def __contains__(self, *args):
  339. self._warn()
  340. return super().__contains__(*args)
  341. def keys(self):
  342. self._warn()
  343. return super().keys()
  344. def values(self):
  345. self._warn()
  346. return super().values()
  347. class SelectableGroups(Deprecated, dict):
  348. """
  349. A backward- and forward-compatible result from
  350. entry_points that fully implements the dict interface.
  351. """
  352. @classmethod
  353. def load(cls, eps):
  354. by_group = operator.attrgetter('group')
  355. ordered = sorted(eps, key=by_group)
  356. grouped = itertools.groupby(ordered, by_group)
  357. return cls((group, EntryPoints(eps)) for group, eps in grouped)
  358. @property
  359. def _all(self):
  360. """
  361. Reconstruct a list of all entrypoints from the groups.
  362. """
  363. groups = super(Deprecated, self).values()
  364. return EntryPoints(itertools.chain.from_iterable(groups))
  365. @property
  366. def groups(self):
  367. return self._all.groups
  368. @property
  369. def names(self):
  370. """
  371. for coverage:
  372. >>> SelectableGroups().names
  373. set()
  374. """
  375. return self._all.names
  376. def select(self, **params):
  377. if not params:
  378. return self
  379. return self._all.select(**params)
  380. class PackagePath(pathlib.PurePosixPath):
  381. """A reference to a path in a package"""
  382. def read_text(self, encoding='utf-8'):
  383. with self.locate().open(encoding=encoding) as stream:
  384. return stream.read()
  385. def read_binary(self):
  386. with self.locate().open('rb') as stream:
  387. return stream.read()
  388. def locate(self):
  389. """Return a path-like object for this path"""
  390. return self.dist.locate_file(self)
  391. class FileHash:
  392. def __init__(self, spec):
  393. self.mode, _, self.value = spec.partition('=')
  394. def __repr__(self):
  395. return f'<FileHash mode: {self.mode} value: {self.value}>'
  396. class Distribution:
  397. """A Python distribution package."""
  398. @abc.abstractmethod
  399. def read_text(self, filename):
  400. """Attempt to load metadata file given by the name.
  401. :param filename: The name of the file in the distribution info.
  402. :return: The text if found, otherwise None.
  403. """
  404. @abc.abstractmethod
  405. def locate_file(self, path):
  406. """
  407. Given a path to a file in this distribution, return a path
  408. to it.
  409. """
  410. @classmethod
  411. def from_name(cls, name):
  412. """Return the Distribution for the given package name.
  413. :param name: The name of the distribution package to search for.
  414. :return: The Distribution instance (or subclass thereof) for the named
  415. package, if found.
  416. :raises PackageNotFoundError: When the named package's distribution
  417. metadata cannot be found.
  418. """
  419. for resolver in cls._discover_resolvers():
  420. dists = resolver(DistributionFinder.Context(name=name))
  421. dist = next(iter(dists), None)
  422. if dist is not None:
  423. return dist
  424. else:
  425. raise PackageNotFoundError(name)
  426. @classmethod
  427. def discover(cls, **kwargs):
  428. """Return an iterable of Distribution objects for all packages.
  429. Pass a ``context`` or pass keyword arguments for constructing
  430. a context.
  431. :context: A ``DistributionFinder.Context`` object.
  432. :return: Iterable of Distribution objects for all packages.
  433. """
  434. context = kwargs.pop('context', None)
  435. if context and kwargs:
  436. raise ValueError("cannot accept context and kwargs")
  437. context = context or DistributionFinder.Context(**kwargs)
  438. return itertools.chain.from_iterable(
  439. resolver(context) for resolver in cls._discover_resolvers()
  440. )
  441. @staticmethod
  442. def at(path):
  443. """Return a Distribution for the indicated metadata path
  444. :param path: a string or path-like object
  445. :return: a concrete Distribution instance for the path
  446. """
  447. return PathDistribution(pathlib.Path(path))
  448. @staticmethod
  449. def _discover_resolvers():
  450. """Search the meta_path for resolvers."""
  451. declared = (
  452. getattr(finder, 'find_distributions', None) for finder in sys.meta_path
  453. )
  454. return filter(None, declared)
  455. @classmethod
  456. def _local(cls, root='.'):
  457. from pep517 import build, meta
  458. system = build.compat_system(root)
  459. builder = functools.partial(
  460. meta.build,
  461. source_dir=root,
  462. system=system,
  463. )
  464. return PathDistribution(zipfile.Path(meta.build_as_zip(builder)))
  465. @property
  466. def metadata(self) -> _meta.PackageMetadata:
  467. """Return the parsed metadata for this Distribution.
  468. The returned object will have keys that name the various bits of
  469. metadata. See PEP 566 for details.
  470. """
  471. text = (
  472. self.read_text('METADATA')
  473. or self.read_text('PKG-INFO')
  474. # This last clause is here to support old egg-info files. Its
  475. # effect is to just end up using the PathDistribution's self._path
  476. # (which points to the egg-info file) attribute unchanged.
  477. or self.read_text('')
  478. )
  479. return _adapters.Message(email.message_from_string(text))
  480. @property
  481. def name(self):
  482. """Return the 'Name' metadata for the distribution package."""
  483. return self.metadata['Name']
  484. @property
  485. def _normalized_name(self):
  486. """Return a normalized version of the name."""
  487. return Prepared.normalize(self.name)
  488. @property
  489. def version(self):
  490. """Return the 'Version' metadata for the distribution package."""
  491. return self.metadata['Version']
  492. @property
  493. def entry_points(self):
  494. return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
  495. @property
  496. def files(self):
  497. """Files in this distribution.
  498. :return: List of PackagePath for this distribution or None
  499. Result is `None` if the metadata file that enumerates files
  500. (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
  501. missing.
  502. Result may be empty if the metadata exists but is empty.
  503. """
  504. file_lines = self._read_files_distinfo() or self._read_files_egginfo()
  505. def make_file(name, hash=None, size_str=None):
  506. result = PackagePath(name)
  507. result.hash = FileHash(hash) if hash else None
  508. result.size = int(size_str) if size_str else None
  509. result.dist = self
  510. return result
  511. return file_lines and list(starmap(make_file, csv.reader(file_lines)))
  512. def _read_files_distinfo(self):
  513. """
  514. Read the lines of RECORD
  515. """
  516. text = self.read_text('RECORD')
  517. return text and text.splitlines()
  518. def _read_files_egginfo(self):
  519. """
  520. SOURCES.txt might contain literal commas, so wrap each line
  521. in quotes.
  522. """
  523. text = self.read_text('SOURCES.txt')
  524. return text and map('"{}"'.format, text.splitlines())
  525. @property
  526. def requires(self):
  527. """Generated requirements specified for this Distribution"""
  528. reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
  529. return reqs and list(reqs)
  530. def _read_dist_info_reqs(self):
  531. return self.metadata.get_all('Requires-Dist')
  532. def _read_egg_info_reqs(self):
  533. source = self.read_text('requires.txt')
  534. return source and self._deps_from_requires_text(source)
  535. @classmethod
  536. def _deps_from_requires_text(cls, source):
  537. return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source))
  538. @staticmethod
  539. def _convert_egg_info_reqs_to_simple_reqs(sections):
  540. """
  541. Historically, setuptools would solicit and store 'extra'
  542. requirements, including those with environment markers,
  543. in separate sections. More modern tools expect each
  544. dependency to be defined separately, with any relevant
  545. extras and environment markers attached directly to that
  546. requirement. This method converts the former to the
  547. latter. See _test_deps_from_requires_text for an example.
  548. """
  549. def make_condition(name):
  550. return name and f'extra == "{name}"'
  551. def parse_condition(section):
  552. section = section or ''
  553. extra, sep, markers = section.partition(':')
  554. if extra and markers:
  555. markers = f'({markers})'
  556. conditions = list(filter(None, [markers, make_condition(extra)]))
  557. return '; ' + ' and '.join(conditions) if conditions else ''
  558. for section in sections:
  559. yield section.value + parse_condition(section.name)
  560. class DistributionFinder(MetaPathFinder):
  561. """
  562. A MetaPathFinder capable of discovering installed distributions.
  563. """
  564. class Context:
  565. """
  566. Keyword arguments presented by the caller to
  567. ``distributions()`` or ``Distribution.discover()``
  568. to narrow the scope of a search for distributions
  569. in all DistributionFinders.
  570. Each DistributionFinder may expect any parameters
  571. and should attempt to honor the canonical
  572. parameters defined below when appropriate.
  573. """
  574. name = None
  575. """
  576. Specific name for which a distribution finder should match.
  577. A name of ``None`` matches all distributions.
  578. """
  579. def __init__(self, **kwargs):
  580. vars(self).update(kwargs)
  581. @property
  582. def path(self):
  583. """
  584. The sequence of directory path that a distribution finder
  585. should search.
  586. Typically refers to Python installed package paths such as
  587. "site-packages" directories and defaults to ``sys.path``.
  588. """
  589. return vars(self).get('path', sys.path)
  590. @abc.abstractmethod
  591. def find_distributions(self, context=Context()):
  592. """
  593. Find distributions.
  594. Return an iterable of all Distribution instances capable of
  595. loading the metadata for packages matching the ``context``,
  596. a DistributionFinder.Context instance.
  597. """
  598. class FastPath:
  599. """
  600. Micro-optimized class for searching a path for
  601. children.
  602. """
  603. @functools.lru_cache() # type: ignore
  604. def __new__(cls, root):
  605. return super().__new__(cls)
  606. def __init__(self, root):
  607. self.root = root
  608. self.base = os.path.basename(self.root).lower()
  609. def joinpath(self, child):
  610. return pathlib.Path(self.root, child)
  611. def children(self):
  612. with suppress(Exception):
  613. return os.listdir(self.root or '')
  614. with suppress(Exception):
  615. return self.zip_children()
  616. return []
  617. def zip_children(self):
  618. zip_path = zipfile.Path(self.root)
  619. names = zip_path.root.namelist()
  620. self.joinpath = zip_path.joinpath
  621. return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
  622. def search(self, name):
  623. return self.lookup(self.mtime).search(name)
  624. @property
  625. def mtime(self):
  626. with suppress(OSError):
  627. return os.stat(self.root).st_mtime
  628. self.lookup.cache_clear()
  629. @method_cache
  630. def lookup(self, mtime):
  631. return Lookup(self)
  632. class Lookup:
  633. def __init__(self, path: FastPath):
  634. base = os.path.basename(path.root).lower()
  635. base_is_egg = base.endswith(".egg")
  636. self.infos = FreezableDefaultDict(list)
  637. self.eggs = FreezableDefaultDict(list)
  638. for child in path.children():
  639. low = child.lower()
  640. if low.endswith((".dist-info", ".egg-info")):
  641. # rpartition is faster than splitext and suitable for this purpose.
  642. name = low.rpartition(".")[0].partition("-")[0]
  643. normalized = Prepared.normalize(name)
  644. self.infos[normalized].append(path.joinpath(child))
  645. elif base_is_egg and low == "egg-info":
  646. name = base.rpartition(".")[0].partition("-")[0]
  647. legacy_normalized = Prepared.legacy_normalize(name)
  648. self.eggs[legacy_normalized].append(path.joinpath(child))
  649. self.infos.freeze()
  650. self.eggs.freeze()
  651. def search(self, prepared):
  652. infos = (
  653. self.infos[prepared.normalized]
  654. if prepared
  655. else itertools.chain.from_iterable(self.infos.values())
  656. )
  657. eggs = (
  658. self.eggs[prepared.legacy_normalized]
  659. if prepared
  660. else itertools.chain.from_iterable(self.eggs.values())
  661. )
  662. return itertools.chain(infos, eggs)
  663. class Prepared:
  664. """
  665. A prepared search for metadata on a possibly-named package.
  666. """
  667. normalized = None
  668. legacy_normalized = None
  669. def __init__(self, name):
  670. self.name = name
  671. if name is None:
  672. return
  673. self.normalized = self.normalize(name)
  674. self.legacy_normalized = self.legacy_normalize(name)
  675. @staticmethod
  676. def normalize(name):
  677. """
  678. PEP 503 normalization plus dashes as underscores.
  679. """
  680. return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
  681. @staticmethod
  682. def legacy_normalize(name):
  683. """
  684. Normalize the package name as found in the convention in
  685. older packaging tools versions and specs.
  686. """
  687. return name.lower().replace('-', '_')
  688. def __bool__(self):
  689. return bool(self.name)
  690. class MetadataPathFinder(DistributionFinder):
  691. @classmethod
  692. def find_distributions(cls, context=DistributionFinder.Context()):
  693. """
  694. Find distributions.
  695. Return an iterable of all Distribution instances capable of
  696. loading the metadata for packages matching ``context.name``
  697. (or all names if ``None`` indicated) along the paths in the list
  698. of directories ``context.path``.
  699. """
  700. found = cls._search_paths(context.name, context.path)
  701. return map(PathDistribution, found)
  702. @classmethod
  703. def _search_paths(cls, name, paths):
  704. """Find metadata directories in paths heuristically."""
  705. prepared = Prepared(name)
  706. return itertools.chain.from_iterable(
  707. path.search(prepared) for path in map(FastPath, paths)
  708. )
  709. def invalidate_caches(cls):
  710. FastPath.__new__.cache_clear()
  711. class PathDistribution(Distribution):
  712. def __init__(self, path: SimplePath):
  713. """Construct a distribution.
  714. :param path: SimplePath indicating the metadata directory.
  715. """
  716. self._path = path
  717. def read_text(self, filename):
  718. with suppress(
  719. FileNotFoundError,
  720. IsADirectoryError,
  721. KeyError,
  722. NotADirectoryError,
  723. PermissionError,
  724. ):
  725. return self._path.joinpath(filename).read_text(encoding='utf-8')
  726. read_text.__doc__ = Distribution.read_text.__doc__
  727. def locate_file(self, path):
  728. return self._path.parent / path
  729. @property
  730. def _normalized_name(self):
  731. """
  732. Performance optimization: where possible, resolve the
  733. normalized name from the file system path.
  734. """
  735. stem = os.path.basename(str(self._path))
  736. return self._name_from_stem(stem) or super()._normalized_name
  737. def _name_from_stem(self, stem):
  738. name, ext = os.path.splitext(stem)
  739. if ext not in ('.dist-info', '.egg-info'):
  740. return
  741. name, sep, rest = stem.partition('-')
  742. return name
  743. def distribution(distribution_name):
  744. """Get the ``Distribution`` instance for the named package.
  745. :param distribution_name: The name of the distribution package as a string.
  746. :return: A ``Distribution`` instance (or subclass thereof).
  747. """
  748. return Distribution.from_name(distribution_name)
  749. def distributions(**kwargs):
  750. """Get all ``Distribution`` instances in the current environment.
  751. :return: An iterable of ``Distribution`` instances.
  752. """
  753. return Distribution.discover(**kwargs)
  754. def metadata(distribution_name) -> _meta.PackageMetadata:
  755. """Get the metadata for the named package.
  756. :param distribution_name: The name of the distribution package to query.
  757. :return: A PackageMetadata containing the parsed metadata.
  758. """
  759. return Distribution.from_name(distribution_name).metadata
  760. def version(distribution_name):
  761. """Get the version string for the named package.
  762. :param distribution_name: The name of the distribution package to query.
  763. :return: The version string for the package as defined in the package's
  764. "Version" metadata key.
  765. """
  766. return distribution(distribution_name).version
  767. def entry_points(**params) -> Union[EntryPoints, SelectableGroups]:
  768. """Return EntryPoint objects for all installed packages.
  769. Pass selection parameters (group or name) to filter the
  770. result to entry points matching those properties (see
  771. EntryPoints.select()).
  772. For compatibility, returns ``SelectableGroups`` object unless
  773. selection parameters are supplied. In the future, this function
  774. will return ``EntryPoints`` instead of ``SelectableGroups``
  775. even when no selection parameters are supplied.
  776. For maximum future compatibility, pass selection parameters
  777. or invoke ``.select`` with parameters on the result.
  778. :return: EntryPoints or SelectableGroups for all installed packages.
  779. """
  780. norm_name = operator.attrgetter('_normalized_name')
  781. unique = functools.partial(unique_everseen, key=norm_name)
  782. eps = itertools.chain.from_iterable(
  783. dist.entry_points for dist in unique(distributions())
  784. )
  785. return SelectableGroups.load(eps).select(**params)
  786. def files(distribution_name):
  787. """Return a list of files for the named package.
  788. :param distribution_name: The name of the distribution package to query.
  789. :return: List of files composing the distribution.
  790. """
  791. return distribution(distribution_name).files
  792. def requires(distribution_name):
  793. """
  794. Return a list of requirements for the named package.
  795. :return: An iterator of requirements, suitable for
  796. packaging.requirement.Requirement.
  797. """
  798. return distribution(distribution_name).requires
  799. def packages_distributions() -> Mapping[str, List[str]]:
  800. """
  801. Return a mapping of top-level packages to their
  802. distributions.
  803. >>> import collections.abc
  804. >>> pkgs = packages_distributions()
  805. >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
  806. True
  807. """
  808. pkg_to_dist = collections.defaultdict(list)
  809. for dist in distributions():
  810. for pkg in (dist.read_text('top_level.txt') or '').split():
  811. pkg_to_dist[pkg].append(dist.metadata['Name'])
  812. return dict(pkg_to_dist)