Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 1 | import io |
Jason R. Coombs | 8ed6503 | 2019-09-12 10:29:11 +0100 | [diff] [blame] | 2 | import os |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 3 | import re |
| 4 | import abc |
| 5 | import csv |
| 6 | import sys |
| 7 | import email |
| 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 |
| 11 | import functools |
| 12 | import itertools |
| 13 | import collections |
| 14 | |
| 15 | from configparser import ConfigParser |
| 16 | from contextlib import suppress |
| 17 | from importlib import import_module |
| 18 | from importlib.abc import MetaPathFinder |
| 19 | from itertools import starmap |
| 20 | |
| 21 | |
| 22 | __all__ = [ |
| 23 | 'Distribution', |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 24 | 'DistributionFinder', |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 25 | 'PackageNotFoundError', |
| 26 | 'distribution', |
| 27 | 'distributions', |
| 28 | 'entry_points', |
| 29 | 'files', |
| 30 | 'metadata', |
| 31 | 'requires', |
| 32 | 'version', |
| 33 | ] |
| 34 | |
| 35 | |
| 36 | class PackageNotFoundError(ModuleNotFoundError): |
| 37 | """The package was not found.""" |
| 38 | |
| 39 | |
Jason R. Coombs | b7a0109 | 2019-12-10 20:05:10 -0500 | [diff] [blame] | 40 | class EntryPoint( |
| 41 | collections.namedtuple('EntryPointBase', 'name value group')): |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 42 | """An entry point as defined by Python packaging conventions. |
| 43 | |
| 44 | See `the packaging docs on entry points |
| 45 | <https://packaging.python.org/specifications/entry-points/>`_ |
| 46 | for more information. |
| 47 | """ |
| 48 | |
| 49 | pattern = re.compile( |
| 50 | r'(?P<module>[\w.]+)\s*' |
| 51 | r'(:\s*(?P<attr>[\w.]+))?\s*' |
| 52 | r'(?P<extras>\[.*\])?\s*$' |
| 53 | ) |
| 54 | """ |
| 55 | A regular expression describing the syntax for an entry point, |
| 56 | which might look like: |
| 57 | |
| 58 | - module |
| 59 | - package.module |
| 60 | - package.module:attribute |
| 61 | - package.module:object.attribute |
| 62 | - package.module:attr [extra1, extra2] |
| 63 | |
| 64 | Other combinations are possible as well. |
| 65 | |
| 66 | The expression is lenient about whitespace around the ':', |
| 67 | following the attr, and following any extras. |
| 68 | """ |
| 69 | |
| 70 | def load(self): |
| 71 | """Load the entry point from its definition. If only a module |
| 72 | is indicated by the value, return that module. Otherwise, |
| 73 | return the named object. |
| 74 | """ |
| 75 | match = self.pattern.match(self.value) |
| 76 | module = import_module(match.group('module')) |
| 77 | attrs = filter(None, (match.group('attr') or '').split('.')) |
| 78 | return functools.reduce(getattr, attrs, module) |
| 79 | |
| 80 | @property |
| 81 | def extras(self): |
| 82 | match = self.pattern.match(self.value) |
| 83 | return list(re.finditer(r'\w+', match.group('extras') or '')) |
| 84 | |
| 85 | @classmethod |
| 86 | def _from_config(cls, config): |
| 87 | return [ |
| 88 | cls(name, value, group) |
| 89 | for group in config.sections() |
| 90 | for name, value in config.items(group) |
| 91 | ] |
| 92 | |
| 93 | @classmethod |
| 94 | def _from_text(cls, text): |
Jason R. Coombs | 049460d | 2019-07-28 14:59:24 -0400 | [diff] [blame] | 95 | config = ConfigParser(delimiters='=') |
Anthony Sottile | 65e5860 | 2019-06-07 14:23:39 -0700 | [diff] [blame] | 96 | # case sensitive: https://stackoverflow.com/q/1611799/812183 |
| 97 | config.optionxform = str |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 98 | try: |
| 99 | config.read_string(text) |
| 100 | except AttributeError: # pragma: nocover |
| 101 | # Python 2 has no read_string |
| 102 | config.readfp(io.StringIO(text)) |
| 103 | return EntryPoint._from_config(config) |
| 104 | |
| 105 | def __iter__(self): |
| 106 | """ |
| 107 | Supply iter so one may construct dicts of EntryPoints easily. |
| 108 | """ |
| 109 | return iter((self.name, self)) |
| 110 | |
Jason R. Coombs | b7a0109 | 2019-12-10 20:05:10 -0500 | [diff] [blame] | 111 | def __reduce__(self): |
| 112 | return ( |
| 113 | self.__class__, |
| 114 | (self.name, self.value, self.group), |
| 115 | ) |
| 116 | |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 117 | |
| 118 | class PackagePath(pathlib.PurePosixPath): |
| 119 | """A reference to a path in a package""" |
| 120 | |
| 121 | def read_text(self, encoding='utf-8'): |
| 122 | with self.locate().open(encoding=encoding) as stream: |
| 123 | return stream.read() |
| 124 | |
| 125 | def read_binary(self): |
| 126 | with self.locate().open('rb') as stream: |
| 127 | return stream.read() |
| 128 | |
| 129 | def locate(self): |
| 130 | """Return a path-like object for this path""" |
| 131 | return self.dist.locate_file(self) |
| 132 | |
| 133 | |
| 134 | class FileHash: |
| 135 | def __init__(self, spec): |
| 136 | self.mode, _, self.value = spec.partition('=') |
| 137 | |
| 138 | def __repr__(self): |
| 139 | return '<FileHash mode: {} value: {}>'.format(self.mode, self.value) |
| 140 | |
| 141 | |
| 142 | class Distribution: |
| 143 | """A Python distribution package.""" |
| 144 | |
| 145 | @abc.abstractmethod |
| 146 | def read_text(self, filename): |
| 147 | """Attempt to load metadata file given by the name. |
| 148 | |
| 149 | :param filename: The name of the file in the distribution info. |
| 150 | :return: The text if found, otherwise None. |
| 151 | """ |
| 152 | |
| 153 | @abc.abstractmethod |
| 154 | def locate_file(self, path): |
| 155 | """ |
| 156 | Given a path to a file in this distribution, return a path |
| 157 | to it. |
| 158 | """ |
| 159 | |
| 160 | @classmethod |
| 161 | def from_name(cls, name): |
| 162 | """Return the Distribution for the given package name. |
| 163 | |
| 164 | :param name: The name of the distribution package to search for. |
| 165 | :return: The Distribution instance (or subclass thereof) for the named |
| 166 | package, if found. |
| 167 | :raises PackageNotFoundError: When the named package's distribution |
| 168 | metadata cannot be found. |
| 169 | """ |
| 170 | for resolver in cls._discover_resolvers(): |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 171 | dists = resolver(DistributionFinder.Context(name=name)) |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 172 | dist = next(dists, None) |
| 173 | if dist is not None: |
| 174 | return dist |
| 175 | else: |
| 176 | raise PackageNotFoundError(name) |
| 177 | |
| 178 | @classmethod |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 179 | def discover(cls, **kwargs): |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 180 | """Return an iterable of Distribution objects for all packages. |
| 181 | |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 182 | Pass a ``context`` or pass keyword arguments for constructing |
| 183 | a context. |
| 184 | |
| 185 | :context: A ``DistributionFinder.Context`` object. |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 186 | :return: Iterable of Distribution objects for all packages. |
| 187 | """ |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 188 | context = kwargs.pop('context', None) |
| 189 | if context and kwargs: |
| 190 | raise ValueError("cannot accept context and kwargs") |
| 191 | context = context or DistributionFinder.Context(**kwargs) |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 192 | return itertools.chain.from_iterable( |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 193 | resolver(context) |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 194 | for resolver in cls._discover_resolvers() |
| 195 | ) |
| 196 | |
| 197 | @staticmethod |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 198 | def at(path): |
| 199 | """Return a Distribution for the indicated metadata path |
| 200 | |
| 201 | :param path: a string or path-like object |
| 202 | :return: a concrete Distribution instance for the path |
| 203 | """ |
| 204 | return PathDistribution(pathlib.Path(path)) |
| 205 | |
| 206 | @staticmethod |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 207 | def _discover_resolvers(): |
| 208 | """Search the meta_path for resolvers.""" |
| 209 | declared = ( |
| 210 | getattr(finder, 'find_distributions', None) |
| 211 | for finder in sys.meta_path |
| 212 | ) |
| 213 | return filter(None, declared) |
| 214 | |
| 215 | @property |
| 216 | def metadata(self): |
| 217 | """Return the parsed metadata for this Distribution. |
| 218 | |
| 219 | The returned object will have keys that name the various bits of |
| 220 | metadata. See PEP 566 for details. |
| 221 | """ |
| 222 | text = ( |
| 223 | self.read_text('METADATA') |
| 224 | or self.read_text('PKG-INFO') |
| 225 | # This last clause is here to support old egg-info files. Its |
| 226 | # effect is to just end up using the PathDistribution's self._path |
| 227 | # (which points to the egg-info file) attribute unchanged. |
| 228 | or self.read_text('') |
| 229 | ) |
| 230 | return email.message_from_string(text) |
| 231 | |
| 232 | @property |
| 233 | def version(self): |
| 234 | """Return the 'Version' metadata for the distribution package.""" |
| 235 | return self.metadata['Version'] |
| 236 | |
| 237 | @property |
| 238 | def entry_points(self): |
| 239 | return EntryPoint._from_text(self.read_text('entry_points.txt')) |
| 240 | |
| 241 | @property |
| 242 | def files(self): |
Jason R. Coombs | 102e9b4 | 2019-09-02 11:08:03 -0400 | [diff] [blame] | 243 | """Files in this distribution. |
| 244 | |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 245 | :return: List of PackagePath for this distribution or None |
Jason R. Coombs | 102e9b4 | 2019-09-02 11:08:03 -0400 | [diff] [blame] | 246 | |
| 247 | Result is `None` if the metadata file that enumerates files |
| 248 | (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is |
| 249 | missing. |
| 250 | Result may be empty if the metadata exists but is empty. |
| 251 | """ |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 252 | file_lines = self._read_files_distinfo() or self._read_files_egginfo() |
| 253 | |
| 254 | def make_file(name, hash=None, size_str=None): |
| 255 | result = PackagePath(name) |
| 256 | result.hash = FileHash(hash) if hash else None |
| 257 | result.size = int(size_str) if size_str else None |
| 258 | result.dist = self |
| 259 | return result |
| 260 | |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 261 | 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] | 262 | |
| 263 | def _read_files_distinfo(self): |
| 264 | """ |
| 265 | Read the lines of RECORD |
| 266 | """ |
| 267 | text = self.read_text('RECORD') |
| 268 | return text and text.splitlines() |
| 269 | |
| 270 | def _read_files_egginfo(self): |
| 271 | """ |
| 272 | SOURCES.txt might contain literal commas, so wrap each line |
| 273 | in quotes. |
| 274 | """ |
| 275 | text = self.read_text('SOURCES.txt') |
| 276 | return text and map('"{}"'.format, text.splitlines()) |
| 277 | |
| 278 | @property |
| 279 | def requires(self): |
| 280 | """Generated requirements specified for this Distribution""" |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 281 | reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs() |
| 282 | return reqs and list(reqs) |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 283 | |
| 284 | def _read_dist_info_reqs(self): |
Jason R. Coombs | 102e9b4 | 2019-09-02 11:08:03 -0400 | [diff] [blame] | 285 | return self.metadata.get_all('Requires-Dist') |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 286 | |
| 287 | def _read_egg_info_reqs(self): |
| 288 | source = self.read_text('requires.txt') |
| 289 | return source and self._deps_from_requires_text(source) |
| 290 | |
| 291 | @classmethod |
| 292 | def _deps_from_requires_text(cls, source): |
| 293 | section_pairs = cls._read_sections(source.splitlines()) |
| 294 | sections = { |
| 295 | section: list(map(operator.itemgetter('line'), results)) |
| 296 | for section, results in |
| 297 | itertools.groupby(section_pairs, operator.itemgetter('section')) |
| 298 | } |
| 299 | return cls._convert_egg_info_reqs_to_simple_reqs(sections) |
| 300 | |
| 301 | @staticmethod |
| 302 | def _read_sections(lines): |
| 303 | section = None |
| 304 | for line in filter(None, lines): |
| 305 | section_match = re.match(r'\[(.*)\]$', line) |
| 306 | if section_match: |
| 307 | section = section_match.group(1) |
| 308 | continue |
| 309 | yield locals() |
| 310 | |
| 311 | @staticmethod |
| 312 | def _convert_egg_info_reqs_to_simple_reqs(sections): |
| 313 | """ |
| 314 | Historically, setuptools would solicit and store 'extra' |
| 315 | requirements, including those with environment markers, |
| 316 | in separate sections. More modern tools expect each |
| 317 | dependency to be defined separately, with any relevant |
| 318 | extras and environment markers attached directly to that |
| 319 | requirement. This method converts the former to the |
| 320 | latter. See _test_deps_from_requires_text for an example. |
| 321 | """ |
| 322 | def make_condition(name): |
| 323 | return name and 'extra == "{name}"'.format(name=name) |
| 324 | |
| 325 | def parse_condition(section): |
| 326 | section = section or '' |
| 327 | extra, sep, markers = section.partition(':') |
| 328 | if extra and markers: |
| 329 | markers = '({markers})'.format(markers=markers) |
| 330 | conditions = list(filter(None, [markers, make_condition(extra)])) |
| 331 | return '; ' + ' and '.join(conditions) if conditions else '' |
| 332 | |
| 333 | for section, deps in sections.items(): |
| 334 | for dep in deps: |
| 335 | yield dep + parse_condition(section) |
| 336 | |
| 337 | |
| 338 | class DistributionFinder(MetaPathFinder): |
| 339 | """ |
| 340 | A MetaPathFinder capable of discovering installed distributions. |
| 341 | """ |
| 342 | |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 343 | class Context: |
Jason R. Coombs | b7a0109 | 2019-12-10 20:05:10 -0500 | [diff] [blame] | 344 | """ |
| 345 | Keyword arguments presented by the caller to |
| 346 | ``distributions()`` or ``Distribution.discover()`` |
| 347 | to narrow the scope of a search for distributions |
| 348 | in all DistributionFinders. |
| 349 | |
| 350 | Each DistributionFinder may expect any parameters |
| 351 | and should attempt to honor the canonical |
| 352 | parameters defined below when appropriate. |
| 353 | """ |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 354 | |
| 355 | name = None |
| 356 | """ |
| 357 | Specific name for which a distribution finder should match. |
Jason R. Coombs | b7a0109 | 2019-12-10 20:05:10 -0500 | [diff] [blame] | 358 | A name of ``None`` matches all distributions. |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 359 | """ |
| 360 | |
| 361 | def __init__(self, **kwargs): |
| 362 | vars(self).update(kwargs) |
| 363 | |
| 364 | @property |
| 365 | def path(self): |
| 366 | """ |
| 367 | The path that a distribution finder should search. |
Jason R. Coombs | b7a0109 | 2019-12-10 20:05:10 -0500 | [diff] [blame] | 368 | |
| 369 | Typically refers to Python package paths and defaults |
| 370 | to ``sys.path``. |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 371 | """ |
| 372 | return vars(self).get('path', sys.path) |
| 373 | |
| 374 | @property |
| 375 | def pattern(self): |
| 376 | return '.*' if self.name is None else re.escape(self.name) |
| 377 | |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 378 | @abc.abstractmethod |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 379 | def find_distributions(self, context=Context()): |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 380 | """ |
| 381 | Find distributions. |
| 382 | |
| 383 | Return an iterable of all Distribution instances capable of |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 384 | loading the metadata for packages matching the ``context``, |
| 385 | a DistributionFinder.Context instance. |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 386 | """ |
| 387 | |
| 388 | |
Jason R. Coombs | 8ed6503 | 2019-09-12 10:29:11 +0100 | [diff] [blame] | 389 | class MetadataPathFinder(DistributionFinder): |
| 390 | @classmethod |
| 391 | def find_distributions(cls, context=DistributionFinder.Context()): |
| 392 | """ |
| 393 | Find distributions. |
| 394 | |
| 395 | Return an iterable of all Distribution instances capable of |
| 396 | loading the metadata for packages matching ``context.name`` |
| 397 | (or all names if ``None`` indicated) along the paths in the list |
| 398 | of directories ``context.path``. |
| 399 | """ |
| 400 | found = cls._search_paths(context.pattern, context.path) |
| 401 | return map(PathDistribution, found) |
| 402 | |
| 403 | @classmethod |
| 404 | def _search_paths(cls, pattern, paths): |
| 405 | """Find metadata directories in paths heuristically.""" |
| 406 | return itertools.chain.from_iterable( |
| 407 | cls._search_path(path, pattern) |
| 408 | for path in map(cls._switch_path, paths) |
| 409 | ) |
| 410 | |
| 411 | @staticmethod |
| 412 | def _switch_path(path): |
| 413 | PYPY_OPEN_BUG = False |
| 414 | if not PYPY_OPEN_BUG or os.path.isfile(path): # pragma: no branch |
| 415 | with suppress(Exception): |
| 416 | return zipfile.Path(path) |
| 417 | return pathlib.Path(path) |
| 418 | |
| 419 | @classmethod |
| 420 | def _matches_info(cls, normalized, item): |
| 421 | template = r'{pattern}(-.*)?\.(dist|egg)-info' |
| 422 | manifest = template.format(pattern=normalized) |
| 423 | return re.match(manifest, item.name, flags=re.IGNORECASE) |
| 424 | |
| 425 | @classmethod |
| 426 | def _matches_legacy(cls, normalized, item): |
| 427 | template = r'{pattern}-.*\.egg[\\/]EGG-INFO' |
| 428 | manifest = template.format(pattern=normalized) |
| 429 | return re.search(manifest, str(item), flags=re.IGNORECASE) |
| 430 | |
| 431 | @classmethod |
| 432 | def _search_path(cls, root, pattern): |
| 433 | if not root.is_dir(): |
| 434 | return () |
| 435 | normalized = pattern.replace('-', '_') |
| 436 | return (item for item in root.iterdir() |
| 437 | if cls._matches_info(normalized, item) |
| 438 | or cls._matches_legacy(normalized, item)) |
| 439 | |
| 440 | |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 441 | class PathDistribution(Distribution): |
| 442 | def __init__(self, path): |
Jason R. Coombs | 102e9b4 | 2019-09-02 11:08:03 -0400 | [diff] [blame] | 443 | """Construct a distribution from a path to the metadata directory. |
| 444 | |
| 445 | :param path: A pathlib.Path or similar object supporting |
| 446 | .joinpath(), __div__, .parent, and .read_text(). |
| 447 | """ |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 448 | self._path = path |
| 449 | |
| 450 | def read_text(self, filename): |
Anthony Sottile | 8087831 | 2019-05-29 17:13:12 -0700 | [diff] [blame] | 451 | with suppress(FileNotFoundError, IsADirectoryError, KeyError, |
| 452 | NotADirectoryError, PermissionError): |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 453 | return self._path.joinpath(filename).read_text(encoding='utf-8') |
| 454 | read_text.__doc__ = Distribution.read_text.__doc__ |
| 455 | |
| 456 | def locate_file(self, path): |
| 457 | return self._path.parent / path |
| 458 | |
| 459 | |
Jason R. Coombs | 8ed6503 | 2019-09-12 10:29:11 +0100 | [diff] [blame] | 460 | def distribution(distribution_name): |
| 461 | """Get the ``Distribution`` instance for the named package. |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 462 | |
Jason R. Coombs | 8ed6503 | 2019-09-12 10:29:11 +0100 | [diff] [blame] | 463 | :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] | 464 | :return: A ``Distribution`` instance (or subclass thereof). |
| 465 | """ |
Jason R. Coombs | 8ed6503 | 2019-09-12 10:29:11 +0100 | [diff] [blame] | 466 | return Distribution.from_name(distribution_name) |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 467 | |
| 468 | |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 469 | def distributions(**kwargs): |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 470 | """Get all ``Distribution`` instances in the current environment. |
| 471 | |
| 472 | :return: An iterable of ``Distribution`` instances. |
| 473 | """ |
Jason R. Coombs | 17499d8 | 2019-09-10 14:53:31 +0100 | [diff] [blame] | 474 | return Distribution.discover(**kwargs) |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 475 | |
| 476 | |
Jason R. Coombs | 8ed6503 | 2019-09-12 10:29:11 +0100 | [diff] [blame] | 477 | def metadata(distribution_name): |
| 478 | """Get the metadata for the named package. |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 479 | |
Jason R. Coombs | 8ed6503 | 2019-09-12 10:29:11 +0100 | [diff] [blame] | 480 | :param distribution_name: The name of the distribution package to query. |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 481 | :return: An email.Message containing the parsed metadata. |
| 482 | """ |
Jason R. Coombs | 8ed6503 | 2019-09-12 10:29:11 +0100 | [diff] [blame] | 483 | return Distribution.from_name(distribution_name).metadata |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 484 | |
| 485 | |
Jason R. Coombs | 8ed6503 | 2019-09-12 10:29:11 +0100 | [diff] [blame] | 486 | def version(distribution_name): |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 487 | """Get the version string for the named package. |
| 488 | |
Jason R. Coombs | 8ed6503 | 2019-09-12 10:29:11 +0100 | [diff] [blame] | 489 | :param distribution_name: The name of the distribution package to query. |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 490 | :return: The version string for the package as defined in the package's |
| 491 | "Version" metadata key. |
| 492 | """ |
Jason R. Coombs | 8ed6503 | 2019-09-12 10:29:11 +0100 | [diff] [blame] | 493 | return distribution(distribution_name).version |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 494 | |
| 495 | |
| 496 | def entry_points(): |
| 497 | """Return EntryPoint objects for all installed packages. |
| 498 | |
| 499 | :return: EntryPoint objects for all installed packages. |
| 500 | """ |
| 501 | eps = itertools.chain.from_iterable( |
| 502 | dist.entry_points for dist in distributions()) |
| 503 | by_group = operator.attrgetter('group') |
| 504 | ordered = sorted(eps, key=by_group) |
| 505 | grouped = itertools.groupby(ordered, by_group) |
| 506 | return { |
| 507 | group: tuple(eps) |
| 508 | for group, eps in grouped |
| 509 | } |
| 510 | |
| 511 | |
Jason R. Coombs | 8ed6503 | 2019-09-12 10:29:11 +0100 | [diff] [blame] | 512 | def files(distribution_name): |
| 513 | """Return a list of files for the named package. |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 514 | |
Jason R. Coombs | 8ed6503 | 2019-09-12 10:29:11 +0100 | [diff] [blame] | 515 | :param distribution_name: The name of the distribution package to query. |
| 516 | :return: List of files composing the distribution. |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 517 | """ |
Jason R. Coombs | 8ed6503 | 2019-09-12 10:29:11 +0100 | [diff] [blame] | 518 | return distribution(distribution_name).files |
| 519 | |
| 520 | |
| 521 | def requires(distribution_name): |
| 522 | """ |
| 523 | Return a list of requirements for the named package. |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 524 | |
| 525 | :return: An iterator of requirements, suitable for |
| 526 | packaging.requirement.Requirement. |
| 527 | """ |
Jason R. Coombs | 8ed6503 | 2019-09-12 10:29:11 +0100 | [diff] [blame] | 528 | return distribution(distribution_name).requires |