blob: 36bb42ee21d9c4d52460dd4edb1ae1004899b513 [file] [log] [blame]
Jason R. Coombs8ed65032019-09-12 10:29:11 +01001import os
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -04002import re
3import abc
4import csv
5import sys
6import email
7import pathlib
Jason R. Coombs8ed65032019-09-12 10:29:11 +01008import zipfile
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -04009import operator
10import functools
11import itertools
Jason R. Coombs136735c2020-01-11 10:37:28 -050012import posixpath
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -040013import collections
14
15from configparser import ConfigParser
16from contextlib import suppress
17from importlib import import_module
18from importlib.abc import MetaPathFinder
19from itertools import starmap
Jason R. Coombsdfdca852020-12-31 12:56:43 -050020from typing import Any, List, Optional, Protocol, TypeVar, Union
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -040021
22
23__all__ = [
24 'Distribution',
Jason R. Coombs17499d82019-09-10 14:53:31 +010025 'DistributionFinder',
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -040026 'PackageNotFoundError',
27 'distribution',
28 'distributions',
29 'entry_points',
30 'files',
31 'metadata',
32 'requires',
33 'version',
Jason R. Coombsdfdca852020-12-31 12:56:43 -050034]
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -040035
36
37class PackageNotFoundError(ModuleNotFoundError):
38 """The package was not found."""
39
Barry Warsaw96ddc582020-10-19 14:14:21 -070040 def __str__(self):
41 tmpl = "No package metadata was found for {self.name}"
42 return tmpl.format(**locals())
43
44 @property
45 def name(self):
Jason R. Coombsdfdca852020-12-31 12:56:43 -050046 (name,) = self.args
Barry Warsaw96ddc582020-10-19 14:14:21 -070047 return name
48
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -040049
Jason R. Coombsb7a01092019-12-10 20:05:10 -050050class EntryPoint(
51 collections.namedtuple('EntryPointBase', 'name value group')):
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -040052 """An entry point as defined by Python packaging conventions.
53
54 See `the packaging docs on entry points
55 <https://packaging.python.org/specifications/entry-points/>`_
56 for more information.
57 """
58
59 pattern = re.compile(
60 r'(?P<module>[\w.]+)\s*'
61 r'(:\s*(?P<attr>[\w.]+))?\s*'
62 r'(?P<extras>\[.*\])?\s*$'
Jason R. Coombsdfdca852020-12-31 12:56:43 -050063 )
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -040064 """
65 A regular expression describing the syntax for an entry point,
66 which might look like:
67
68 - module
69 - package.module
70 - package.module:attribute
71 - package.module:object.attribute
72 - package.module:attr [extra1, extra2]
73
74 Other combinations are possible as well.
75
76 The expression is lenient about whitespace around the ':',
77 following the attr, and following any extras.
78 """
79
Jason R. Coombsdfdca852020-12-31 12:56:43 -050080 dist: Optional['Distribution'] = None
81
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -040082 def load(self):
83 """Load the entry point from its definition. If only a module
84 is indicated by the value, return that module. Otherwise,
85 return the named object.
86 """
87 match = self.pattern.match(self.value)
88 module = import_module(match.group('module'))
89 attrs = filter(None, (match.group('attr') or '').split('.'))
90 return functools.reduce(getattr, attrs, module)
91
92 @property
Jason R. Coombs161541a2020-06-05 16:34:16 -040093 def module(self):
94 match = self.pattern.match(self.value)
95 return match.group('module')
96
97 @property
98 def attr(self):
99 match = self.pattern.match(self.value)
100 return match.group('attr')
101
102 @property
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400103 def extras(self):
104 match = self.pattern.match(self.value)
105 return list(re.finditer(r'\w+', match.group('extras') or ''))
106
107 @classmethod
108 def _from_config(cls, config):
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500109 return (
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400110 cls(name, value, group)
111 for group in config.sections()
112 for name, value in config.items(group)
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500113 )
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400114
115 @classmethod
116 def _from_text(cls, text):
Jason R. Coombs049460d2019-07-28 14:59:24 -0400117 config = ConfigParser(delimiters='=')
Anthony Sottile65e58602019-06-07 14:23:39 -0700118 # case sensitive: https://stackoverflow.com/q/1611799/812183
119 config.optionxform = str
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500120 config.read_string(text)
121 return cls._from_config(config)
122
123 @classmethod
124 def _from_text_for(cls, text, dist):
125 return (ep._for(dist) for ep in cls._from_text(text))
126
127 def _for(self, dist):
128 self.dist = dist
129 return self
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400130
131 def __iter__(self):
132 """
133 Supply iter so one may construct dicts of EntryPoints easily.
134 """
135 return iter((self.name, self))
136
Jason R. Coombsb7a01092019-12-10 20:05:10 -0500137 def __reduce__(self):
138 return (
139 self.__class__,
140 (self.name, self.value, self.group),
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500141 )
Jason R. Coombsb7a01092019-12-10 20:05:10 -0500142
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400143
144class PackagePath(pathlib.PurePosixPath):
145 """A reference to a path in a package"""
146
147 def read_text(self, encoding='utf-8'):
148 with self.locate().open(encoding=encoding) as stream:
149 return stream.read()
150
151 def read_binary(self):
152 with self.locate().open('rb') as stream:
153 return stream.read()
154
155 def locate(self):
156 """Return a path-like object for this path"""
157 return self.dist.locate_file(self)
158
159
160class FileHash:
161 def __init__(self, spec):
162 self.mode, _, self.value = spec.partition('=')
163
164 def __repr__(self):
165 return '<FileHash mode: {} value: {}>'.format(self.mode, self.value)
166
167
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500168_T = TypeVar("_T")
169
170
171class PackageMetadata(Protocol):
172 def __len__(self) -> int:
173 ... # pragma: no cover
174
175 def __contains__(self, item: str) -> bool:
176 ... # pragma: no cover
177
178 def __getitem__(self, key: str) -> str:
179 ... # pragma: no cover
180
181 def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
182 """
183 Return all values associated with a possibly multi-valued key.
184 """
185
186
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400187class Distribution:
188 """A Python distribution package."""
189
190 @abc.abstractmethod
191 def read_text(self, filename):
192 """Attempt to load metadata file given by the name.
193
194 :param filename: The name of the file in the distribution info.
195 :return: The text if found, otherwise None.
196 """
197
198 @abc.abstractmethod
199 def locate_file(self, path):
200 """
201 Given a path to a file in this distribution, return a path
202 to it.
203 """
204
205 @classmethod
206 def from_name(cls, name):
207 """Return the Distribution for the given package name.
208
209 :param name: The name of the distribution package to search for.
210 :return: The Distribution instance (or subclass thereof) for the named
211 package, if found.
212 :raises PackageNotFoundError: When the named package's distribution
213 metadata cannot be found.
214 """
215 for resolver in cls._discover_resolvers():
Jason R. Coombs17499d82019-09-10 14:53:31 +0100216 dists = resolver(DistributionFinder.Context(name=name))
Jason R. Coombs161541a2020-06-05 16:34:16 -0400217 dist = next(iter(dists), None)
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400218 if dist is not None:
219 return dist
220 else:
221 raise PackageNotFoundError(name)
222
223 @classmethod
Jason R. Coombs17499d82019-09-10 14:53:31 +0100224 def discover(cls, **kwargs):
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400225 """Return an iterable of Distribution objects for all packages.
226
Jason R. Coombs17499d82019-09-10 14:53:31 +0100227 Pass a ``context`` or pass keyword arguments for constructing
228 a context.
229
230 :context: A ``DistributionFinder.Context`` object.
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400231 :return: Iterable of Distribution objects for all packages.
232 """
Jason R. Coombs17499d82019-09-10 14:53:31 +0100233 context = kwargs.pop('context', None)
234 if context and kwargs:
235 raise ValueError("cannot accept context and kwargs")
236 context = context or DistributionFinder.Context(**kwargs)
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400237 return itertools.chain.from_iterable(
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500238 resolver(context) for resolver in cls._discover_resolvers()
239 )
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400240
241 @staticmethod
Jason R. Coombs17499d82019-09-10 14:53:31 +0100242 def at(path):
243 """Return a Distribution for the indicated metadata path
244
245 :param path: a string or path-like object
246 :return: a concrete Distribution instance for the path
247 """
248 return PathDistribution(pathlib.Path(path))
249
250 @staticmethod
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400251 def _discover_resolvers():
252 """Search the meta_path for resolvers."""
253 declared = (
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500254 getattr(finder, 'find_distributions', None) for finder in sys.meta_path
255 )
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400256 return filter(None, declared)
257
Jason R. Coombs161541a2020-06-05 16:34:16 -0400258 @classmethod
259 def _local(cls, root='.'):
260 from pep517 import build, meta
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500261
Jason R. Coombs161541a2020-06-05 16:34:16 -0400262 system = build.compat_system(root)
263 builder = functools.partial(
264 meta.build,
265 source_dir=root,
266 system=system,
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500267 )
Jason R. Coombs161541a2020-06-05 16:34:16 -0400268 return PathDistribution(zipfile.Path(meta.build_as_zip(builder)))
269
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400270 @property
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500271 def metadata(self) -> PackageMetadata:
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400272 """Return the parsed metadata for this Distribution.
273
274 The returned object will have keys that name the various bits of
275 metadata. See PEP 566 for details.
276 """
277 text = (
278 self.read_text('METADATA')
279 or self.read_text('PKG-INFO')
280 # This last clause is here to support old egg-info files. Its
281 # effect is to just end up using the PathDistribution's self._path
282 # (which points to the egg-info file) attribute unchanged.
283 or self.read_text('')
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500284 )
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400285 return email.message_from_string(text)
286
287 @property
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500288 def name(self):
289 """Return the 'Name' metadata for the distribution package."""
290 return self.metadata['Name']
291
292 @property
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400293 def version(self):
294 """Return the 'Version' metadata for the distribution package."""
295 return self.metadata['Version']
296
297 @property
298 def entry_points(self):
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500299 return list(EntryPoint._from_text_for(self.read_text('entry_points.txt'), self))
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400300
301 @property
302 def files(self):
Jason R. Coombs102e9b42019-09-02 11:08:03 -0400303 """Files in this distribution.
304
Jason R. Coombs17499d82019-09-10 14:53:31 +0100305 :return: List of PackagePath for this distribution or None
Jason R. Coombs102e9b42019-09-02 11:08:03 -0400306
307 Result is `None` if the metadata file that enumerates files
308 (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
309 missing.
310 Result may be empty if the metadata exists but is empty.
311 """
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400312 file_lines = self._read_files_distinfo() or self._read_files_egginfo()
313
314 def make_file(name, hash=None, size_str=None):
315 result = PackagePath(name)
316 result.hash = FileHash(hash) if hash else None
317 result.size = int(size_str) if size_str else None
318 result.dist = self
319 return result
320
Jason R. Coombs17499d82019-09-10 14:53:31 +0100321 return file_lines and list(starmap(make_file, csv.reader(file_lines)))
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400322
323 def _read_files_distinfo(self):
324 """
325 Read the lines of RECORD
326 """
327 text = self.read_text('RECORD')
328 return text and text.splitlines()
329
330 def _read_files_egginfo(self):
331 """
332 SOURCES.txt might contain literal commas, so wrap each line
333 in quotes.
334 """
335 text = self.read_text('SOURCES.txt')
336 return text and map('"{}"'.format, text.splitlines())
337
338 @property
339 def requires(self):
340 """Generated requirements specified for this Distribution"""
Jason R. Coombs17499d82019-09-10 14:53:31 +0100341 reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
342 return reqs and list(reqs)
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400343
344 def _read_dist_info_reqs(self):
Jason R. Coombs102e9b42019-09-02 11:08:03 -0400345 return self.metadata.get_all('Requires-Dist')
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400346
347 def _read_egg_info_reqs(self):
348 source = self.read_text('requires.txt')
349 return source and self._deps_from_requires_text(source)
350
351 @classmethod
352 def _deps_from_requires_text(cls, source):
353 section_pairs = cls._read_sections(source.splitlines())
354 sections = {
355 section: list(map(operator.itemgetter('line'), results))
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500356 for section, results in itertools.groupby(
357 section_pairs, operator.itemgetter('section')
358 )
359 }
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400360 return cls._convert_egg_info_reqs_to_simple_reqs(sections)
361
362 @staticmethod
363 def _read_sections(lines):
364 section = None
365 for line in filter(None, lines):
366 section_match = re.match(r'\[(.*)\]$', line)
367 if section_match:
368 section = section_match.group(1)
369 continue
370 yield locals()
371
372 @staticmethod
373 def _convert_egg_info_reqs_to_simple_reqs(sections):
374 """
375 Historically, setuptools would solicit and store 'extra'
376 requirements, including those with environment markers,
377 in separate sections. More modern tools expect each
378 dependency to be defined separately, with any relevant
379 extras and environment markers attached directly to that
380 requirement. This method converts the former to the
381 latter. See _test_deps_from_requires_text for an example.
382 """
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500383
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400384 def make_condition(name):
385 return name and 'extra == "{name}"'.format(name=name)
386
387 def parse_condition(section):
388 section = section or ''
389 extra, sep, markers = section.partition(':')
390 if extra and markers:
391 markers = '({markers})'.format(markers=markers)
392 conditions = list(filter(None, [markers, make_condition(extra)]))
393 return '; ' + ' and '.join(conditions) if conditions else ''
394
395 for section, deps in sections.items():
396 for dep in deps:
397 yield dep + parse_condition(section)
398
399
400class DistributionFinder(MetaPathFinder):
401 """
402 A MetaPathFinder capable of discovering installed distributions.
403 """
404
Jason R. Coombs17499d82019-09-10 14:53:31 +0100405 class Context:
Jason R. Coombsb7a01092019-12-10 20:05:10 -0500406 """
407 Keyword arguments presented by the caller to
408 ``distributions()`` or ``Distribution.discover()``
409 to narrow the scope of a search for distributions
410 in all DistributionFinders.
411
412 Each DistributionFinder may expect any parameters
413 and should attempt to honor the canonical
414 parameters defined below when appropriate.
415 """
Jason R. Coombs17499d82019-09-10 14:53:31 +0100416
417 name = None
418 """
419 Specific name for which a distribution finder should match.
Jason R. Coombsb7a01092019-12-10 20:05:10 -0500420 A name of ``None`` matches all distributions.
Jason R. Coombs17499d82019-09-10 14:53:31 +0100421 """
422
423 def __init__(self, **kwargs):
424 vars(self).update(kwargs)
425
426 @property
427 def path(self):
428 """
429 The path that a distribution finder should search.
Jason R. Coombsb7a01092019-12-10 20:05:10 -0500430
431 Typically refers to Python package paths and defaults
432 to ``sys.path``.
Jason R. Coombs17499d82019-09-10 14:53:31 +0100433 """
434 return vars(self).get('path', sys.path)
435
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400436 @abc.abstractmethod
Jason R. Coombs17499d82019-09-10 14:53:31 +0100437 def find_distributions(self, context=Context()):
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400438 """
439 Find distributions.
440
441 Return an iterable of all Distribution instances capable of
Jason R. Coombs17499d82019-09-10 14:53:31 +0100442 loading the metadata for packages matching the ``context``,
443 a DistributionFinder.Context instance.
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400444 """
445
446
Jason R. Coombs136735c2020-01-11 10:37:28 -0500447class FastPath:
448 """
449 Micro-optimized class for searching a path for
450 children.
451 """
452
453 def __init__(self, root):
454 self.root = root
Jason R. Coombs161541a2020-06-05 16:34:16 -0400455 self.base = os.path.basename(self.root).lower()
Jason R. Coombs136735c2020-01-11 10:37:28 -0500456
457 def joinpath(self, child):
458 return pathlib.Path(self.root, child)
459
460 def children(self):
461 with suppress(Exception):
462 return os.listdir(self.root or '')
463 with suppress(Exception):
464 return self.zip_children()
465 return []
466
467 def zip_children(self):
468 zip_path = zipfile.Path(self.root)
469 names = zip_path.root.namelist()
470 self.joinpath = zip_path.joinpath
471
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500472 return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
Jason R. Coombs136735c2020-01-11 10:37:28 -0500473
474 def search(self, name):
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500475 return (
476 self.joinpath(child)
477 for child in self.children()
478 if name.matches(child, self.base)
479 )
Jason R. Coombs136735c2020-01-11 10:37:28 -0500480
481
482class Prepared:
483 """
484 A prepared search for metadata on a possibly-named package.
485 """
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500486
487 normalized = None
Jason R. Coombs136735c2020-01-11 10:37:28 -0500488 suffixes = '.dist-info', '.egg-info'
489 exact_matches = [''][:0]
490
491 def __init__(self, name):
492 self.name = name
493 if name is None:
494 return
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500495 self.normalized = self.normalize(name)
496 self.exact_matches = [self.normalized + suffix for suffix in self.suffixes]
497
498 @staticmethod
499 def normalize(name):
500 """
501 PEP 503 normalization plus dashes as underscores.
502 """
503 return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
504
505 @staticmethod
506 def legacy_normalize(name):
507 """
508 Normalize the package name as found in the convention in
509 older packaging tools versions and specs.
510 """
511 return name.lower().replace('-', '_')
512
513 def matches(self, cand, base):
514 low = cand.lower()
515 pre, ext = os.path.splitext(low)
516 name, sep, rest = pre.partition('-')
517 return (
518 low in self.exact_matches
519 or ext in self.suffixes
520 and (not self.normalized or name.replace('.', '_') == self.normalized)
521 # legacy case:
522 or self.is_egg(base)
523 and low == 'egg-info'
524 )
525
526 def is_egg(self, base):
527 normalized = self.legacy_normalize(self.name or '')
528 prefix = normalized + '-' if normalized else ''
529 versionless_egg_name = normalized + '.egg' if self.name else ''
530 return (
531 base == versionless_egg_name
532 or base.startswith(prefix)
533 and base.endswith('.egg')
534 )
Jason R. Coombs136735c2020-01-11 10:37:28 -0500535
536
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100537class MetadataPathFinder(DistributionFinder):
538 @classmethod
539 def find_distributions(cls, context=DistributionFinder.Context()):
540 """
541 Find distributions.
542
543 Return an iterable of all Distribution instances capable of
544 loading the metadata for packages matching ``context.name``
545 (or all names if ``None`` indicated) along the paths in the list
546 of directories ``context.path``.
547 """
Jason R. Coombs136735c2020-01-11 10:37:28 -0500548 found = cls._search_paths(context.name, context.path)
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100549 return map(PathDistribution, found)
550
551 @classmethod
Jason R. Coombs136735c2020-01-11 10:37:28 -0500552 def _search_paths(cls, name, paths):
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100553 """Find metadata directories in paths heuristically."""
554 return itertools.chain.from_iterable(
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500555 path.search(Prepared(name)) for path in map(FastPath, paths)
556 )
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100557
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100558
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400559class PathDistribution(Distribution):
560 def __init__(self, path):
Jason R. Coombs102e9b42019-09-02 11:08:03 -0400561 """Construct a distribution from a path to the metadata directory.
562
563 :param path: A pathlib.Path or similar object supporting
564 .joinpath(), __div__, .parent, and .read_text().
565 """
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400566 self._path = path
567
568 def read_text(self, filename):
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500569 with suppress(
570 FileNotFoundError,
571 IsADirectoryError,
572 KeyError,
573 NotADirectoryError,
574 PermissionError,
575 ):
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400576 return self._path.joinpath(filename).read_text(encoding='utf-8')
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500577
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400578 read_text.__doc__ = Distribution.read_text.__doc__
579
580 def locate_file(self, path):
581 return self._path.parent / path
582
583
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100584def distribution(distribution_name):
585 """Get the ``Distribution`` instance for the named package.
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400586
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100587 :param distribution_name: The name of the distribution package as a string.
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400588 :return: A ``Distribution`` instance (or subclass thereof).
589 """
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100590 return Distribution.from_name(distribution_name)
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400591
592
Jason R. Coombs17499d82019-09-10 14:53:31 +0100593def distributions(**kwargs):
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400594 """Get all ``Distribution`` instances in the current environment.
595
596 :return: An iterable of ``Distribution`` instances.
597 """
Jason R. Coombs17499d82019-09-10 14:53:31 +0100598 return Distribution.discover(**kwargs)
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400599
600
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500601def metadata(distribution_name) -> PackageMetadata:
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100602 """Get the metadata for the named package.
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400603
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100604 :param distribution_name: The name of the distribution package to query.
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500605 :return: A PackageMetadata containing the parsed metadata.
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400606 """
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100607 return Distribution.from_name(distribution_name).metadata
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400608
609
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100610def version(distribution_name):
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400611 """Get the version string for the named package.
612
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100613 :param distribution_name: The name of the distribution package to query.
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400614 :return: The version string for the package as defined in the package's
615 "Version" metadata key.
616 """
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100617 return distribution(distribution_name).version
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400618
619
620def entry_points():
621 """Return EntryPoint objects for all installed packages.
622
623 :return: EntryPoint objects for all installed packages.
624 """
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500625 eps = itertools.chain.from_iterable(dist.entry_points for dist in distributions())
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400626 by_group = operator.attrgetter('group')
627 ordered = sorted(eps, key=by_group)
628 grouped = itertools.groupby(ordered, by_group)
Jason R. Coombsdfdca852020-12-31 12:56:43 -0500629 return {group: tuple(eps) for group, eps in grouped}
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400630
631
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100632def files(distribution_name):
633 """Return a list of files for the named package.
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400634
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100635 :param distribution_name: The name of the distribution package to query.
636 :return: List of files composing the distribution.
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400637 """
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100638 return distribution(distribution_name).files
639
640
641def requires(distribution_name):
642 """
643 Return a list of requirements for the named package.
Jason R. Coombs1bbf7b62019-05-24 19:59:01 -0400644
645 :return: An iterator of requirements, suitable for
646 packaging.requirement.Requirement.
647 """
Jason R. Coombs8ed65032019-09-12 10:29:11 +0100648 return distribution(distribution_name).requires