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