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