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