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