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