__init__.py 28 KB

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