Mercurial > repos > shellac > sam_consensus_v3
comparison env/lib/python3.9/site-packages/pip/_vendor/pkg_resources/__init__.py @ 0:4f3585e2f14b draft default tip
"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author | shellac |
---|---|
date | Mon, 22 Mar 2021 18:12:50 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4f3585e2f14b |
---|---|
1 # coding: utf-8 | |
2 """ | |
3 Package resource API | |
4 -------------------- | |
5 | |
6 A resource is a logical file contained within a package, or a logical | |
7 subdirectory thereof. The package resource API expects resource names | |
8 to have their path parts separated with ``/``, *not* whatever the local | |
9 path separator is. Do not use os.path operations to manipulate resource | |
10 names being passed into the API. | |
11 | |
12 The package resource API is designed to work with normal filesystem packages, | |
13 .egg files, and unpacked .egg files. It can also work in a limited way with | |
14 .zip files and with custom PEP 302 loaders that support the ``get_data()`` | |
15 method. | |
16 """ | |
17 | |
18 from __future__ import absolute_import | |
19 | |
20 import sys | |
21 import os | |
22 import io | |
23 import time | |
24 import re | |
25 import types | |
26 import zipfile | |
27 import zipimport | |
28 import warnings | |
29 import stat | |
30 import functools | |
31 import pkgutil | |
32 import operator | |
33 import platform | |
34 import collections | |
35 import plistlib | |
36 import email.parser | |
37 import errno | |
38 import tempfile | |
39 import textwrap | |
40 import itertools | |
41 import inspect | |
42 import ntpath | |
43 import posixpath | |
44 from pkgutil import get_importer | |
45 | |
46 try: | |
47 import _imp | |
48 except ImportError: | |
49 # Python 3.2 compatibility | |
50 import imp as _imp | |
51 | |
52 try: | |
53 FileExistsError | |
54 except NameError: | |
55 FileExistsError = OSError | |
56 | |
57 from pip._vendor import six | |
58 from pip._vendor.six.moves import urllib, map, filter | |
59 | |
60 # capture these to bypass sandboxing | |
61 from os import utime | |
62 try: | |
63 from os import mkdir, rename, unlink | |
64 WRITE_SUPPORT = True | |
65 except ImportError: | |
66 # no write support, probably under GAE | |
67 WRITE_SUPPORT = False | |
68 | |
69 from os import open as os_open | |
70 from os.path import isdir, split | |
71 | |
72 try: | |
73 import importlib.machinery as importlib_machinery | |
74 # access attribute to force import under delayed import mechanisms. | |
75 importlib_machinery.__name__ | |
76 except ImportError: | |
77 importlib_machinery = None | |
78 | |
79 from . import py31compat | |
80 from pip._vendor import appdirs | |
81 from pip._vendor import packaging | |
82 __import__('pip._vendor.packaging.version') | |
83 __import__('pip._vendor.packaging.specifiers') | |
84 __import__('pip._vendor.packaging.requirements') | |
85 __import__('pip._vendor.packaging.markers') | |
86 | |
87 | |
88 __metaclass__ = type | |
89 | |
90 | |
91 if (3, 0) < sys.version_info < (3, 5): | |
92 raise RuntimeError("Python 3.5 or later is required") | |
93 | |
94 if six.PY2: | |
95 # Those builtin exceptions are only defined in Python 3 | |
96 PermissionError = None | |
97 NotADirectoryError = None | |
98 | |
99 # declare some globals that will be defined later to | |
100 # satisfy the linters. | |
101 require = None | |
102 working_set = None | |
103 add_activation_listener = None | |
104 resources_stream = None | |
105 cleanup_resources = None | |
106 resource_dir = None | |
107 resource_stream = None | |
108 set_extraction_path = None | |
109 resource_isdir = None | |
110 resource_string = None | |
111 iter_entry_points = None | |
112 resource_listdir = None | |
113 resource_filename = None | |
114 resource_exists = None | |
115 _distribution_finders = None | |
116 _namespace_handlers = None | |
117 _namespace_packages = None | |
118 | |
119 | |
120 class PEP440Warning(RuntimeWarning): | |
121 """ | |
122 Used when there is an issue with a version or specifier not complying with | |
123 PEP 440. | |
124 """ | |
125 | |
126 | |
127 def parse_version(v): | |
128 try: | |
129 return packaging.version.Version(v) | |
130 except packaging.version.InvalidVersion: | |
131 return packaging.version.LegacyVersion(v) | |
132 | |
133 | |
134 _state_vars = {} | |
135 | |
136 | |
137 def _declare_state(vartype, **kw): | |
138 globals().update(kw) | |
139 _state_vars.update(dict.fromkeys(kw, vartype)) | |
140 | |
141 | |
142 def __getstate__(): | |
143 state = {} | |
144 g = globals() | |
145 for k, v in _state_vars.items(): | |
146 state[k] = g['_sget_' + v](g[k]) | |
147 return state | |
148 | |
149 | |
150 def __setstate__(state): | |
151 g = globals() | |
152 for k, v in state.items(): | |
153 g['_sset_' + _state_vars[k]](k, g[k], v) | |
154 return state | |
155 | |
156 | |
157 def _sget_dict(val): | |
158 return val.copy() | |
159 | |
160 | |
161 def _sset_dict(key, ob, state): | |
162 ob.clear() | |
163 ob.update(state) | |
164 | |
165 | |
166 def _sget_object(val): | |
167 return val.__getstate__() | |
168 | |
169 | |
170 def _sset_object(key, ob, state): | |
171 ob.__setstate__(state) | |
172 | |
173 | |
174 _sget_none = _sset_none = lambda *args: None | |
175 | |
176 | |
177 def get_supported_platform(): | |
178 """Return this platform's maximum compatible version. | |
179 | |
180 distutils.util.get_platform() normally reports the minimum version | |
181 of Mac OS X that would be required to *use* extensions produced by | |
182 distutils. But what we want when checking compatibility is to know the | |
183 version of Mac OS X that we are *running*. To allow usage of packages that | |
184 explicitly require a newer version of Mac OS X, we must also know the | |
185 current version of the OS. | |
186 | |
187 If this condition occurs for any other platform with a version in its | |
188 platform strings, this function should be extended accordingly. | |
189 """ | |
190 plat = get_build_platform() | |
191 m = macosVersionString.match(plat) | |
192 if m is not None and sys.platform == "darwin": | |
193 try: | |
194 plat = 'macosx-%s-%s' % ('.'.join(_macosx_vers()[:2]), m.group(3)) | |
195 except ValueError: | |
196 # not Mac OS X | |
197 pass | |
198 return plat | |
199 | |
200 | |
201 __all__ = [ | |
202 # Basic resource access and distribution/entry point discovery | |
203 'require', 'run_script', 'get_provider', 'get_distribution', | |
204 'load_entry_point', 'get_entry_map', 'get_entry_info', | |
205 'iter_entry_points', | |
206 'resource_string', 'resource_stream', 'resource_filename', | |
207 'resource_listdir', 'resource_exists', 'resource_isdir', | |
208 | |
209 # Environmental control | |
210 'declare_namespace', 'working_set', 'add_activation_listener', | |
211 'find_distributions', 'set_extraction_path', 'cleanup_resources', | |
212 'get_default_cache', | |
213 | |
214 # Primary implementation classes | |
215 'Environment', 'WorkingSet', 'ResourceManager', | |
216 'Distribution', 'Requirement', 'EntryPoint', | |
217 | |
218 # Exceptions | |
219 'ResolutionError', 'VersionConflict', 'DistributionNotFound', | |
220 'UnknownExtra', 'ExtractionError', | |
221 | |
222 # Warnings | |
223 'PEP440Warning', | |
224 | |
225 # Parsing functions and string utilities | |
226 'parse_requirements', 'parse_version', 'safe_name', 'safe_version', | |
227 'get_platform', 'compatible_platforms', 'yield_lines', 'split_sections', | |
228 'safe_extra', 'to_filename', 'invalid_marker', 'evaluate_marker', | |
229 | |
230 # filesystem utilities | |
231 'ensure_directory', 'normalize_path', | |
232 | |
233 # Distribution "precedence" constants | |
234 'EGG_DIST', 'BINARY_DIST', 'SOURCE_DIST', 'CHECKOUT_DIST', 'DEVELOP_DIST', | |
235 | |
236 # "Provider" interfaces, implementations, and registration/lookup APIs | |
237 'IMetadataProvider', 'IResourceProvider', 'FileMetadata', | |
238 'PathMetadata', 'EggMetadata', 'EmptyProvider', 'empty_provider', | |
239 'NullProvider', 'EggProvider', 'DefaultProvider', 'ZipProvider', | |
240 'register_finder', 'register_namespace_handler', 'register_loader_type', | |
241 'fixup_namespace_packages', 'get_importer', | |
242 | |
243 # Warnings | |
244 'PkgResourcesDeprecationWarning', | |
245 | |
246 # Deprecated/backward compatibility only | |
247 'run_main', 'AvailableDistributions', | |
248 ] | |
249 | |
250 | |
251 class ResolutionError(Exception): | |
252 """Abstract base for dependency resolution errors""" | |
253 | |
254 def __repr__(self): | |
255 return self.__class__.__name__ + repr(self.args) | |
256 | |
257 | |
258 class VersionConflict(ResolutionError): | |
259 """ | |
260 An already-installed version conflicts with the requested version. | |
261 | |
262 Should be initialized with the installed Distribution and the requested | |
263 Requirement. | |
264 """ | |
265 | |
266 _template = "{self.dist} is installed but {self.req} is required" | |
267 | |
268 @property | |
269 def dist(self): | |
270 return self.args[0] | |
271 | |
272 @property | |
273 def req(self): | |
274 return self.args[1] | |
275 | |
276 def report(self): | |
277 return self._template.format(**locals()) | |
278 | |
279 def with_context(self, required_by): | |
280 """ | |
281 If required_by is non-empty, return a version of self that is a | |
282 ContextualVersionConflict. | |
283 """ | |
284 if not required_by: | |
285 return self | |
286 args = self.args + (required_by,) | |
287 return ContextualVersionConflict(*args) | |
288 | |
289 | |
290 class ContextualVersionConflict(VersionConflict): | |
291 """ | |
292 A VersionConflict that accepts a third parameter, the set of the | |
293 requirements that required the installed Distribution. | |
294 """ | |
295 | |
296 _template = VersionConflict._template + ' by {self.required_by}' | |
297 | |
298 @property | |
299 def required_by(self): | |
300 return self.args[2] | |
301 | |
302 | |
303 class DistributionNotFound(ResolutionError): | |
304 """A requested distribution was not found""" | |
305 | |
306 _template = ("The '{self.req}' distribution was not found " | |
307 "and is required by {self.requirers_str}") | |
308 | |
309 @property | |
310 def req(self): | |
311 return self.args[0] | |
312 | |
313 @property | |
314 def requirers(self): | |
315 return self.args[1] | |
316 | |
317 @property | |
318 def requirers_str(self): | |
319 if not self.requirers: | |
320 return 'the application' | |
321 return ', '.join(self.requirers) | |
322 | |
323 def report(self): | |
324 return self._template.format(**locals()) | |
325 | |
326 def __str__(self): | |
327 return self.report() | |
328 | |
329 | |
330 class UnknownExtra(ResolutionError): | |
331 """Distribution doesn't have an "extra feature" of the given name""" | |
332 | |
333 | |
334 _provider_factories = {} | |
335 | |
336 PY_MAJOR = '{}.{}'.format(*sys.version_info) | |
337 EGG_DIST = 3 | |
338 BINARY_DIST = 2 | |
339 SOURCE_DIST = 1 | |
340 CHECKOUT_DIST = 0 | |
341 DEVELOP_DIST = -1 | |
342 | |
343 | |
344 def register_loader_type(loader_type, provider_factory): | |
345 """Register `provider_factory` to make providers for `loader_type` | |
346 | |
347 `loader_type` is the type or class of a PEP 302 ``module.__loader__``, | |
348 and `provider_factory` is a function that, passed a *module* object, | |
349 returns an ``IResourceProvider`` for that module. | |
350 """ | |
351 _provider_factories[loader_type] = provider_factory | |
352 | |
353 | |
354 def get_provider(moduleOrReq): | |
355 """Return an IResourceProvider for the named module or requirement""" | |
356 if isinstance(moduleOrReq, Requirement): | |
357 return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0] | |
358 try: | |
359 module = sys.modules[moduleOrReq] | |
360 except KeyError: | |
361 __import__(moduleOrReq) | |
362 module = sys.modules[moduleOrReq] | |
363 loader = getattr(module, '__loader__', None) | |
364 return _find_adapter(_provider_factories, loader)(module) | |
365 | |
366 | |
367 def _macosx_vers(_cache=[]): | |
368 if not _cache: | |
369 version = platform.mac_ver()[0] | |
370 # fallback for MacPorts | |
371 if version == '': | |
372 plist = '/System/Library/CoreServices/SystemVersion.plist' | |
373 if os.path.exists(plist): | |
374 if hasattr(plistlib, 'readPlist'): | |
375 plist_content = plistlib.readPlist(plist) | |
376 if 'ProductVersion' in plist_content: | |
377 version = plist_content['ProductVersion'] | |
378 | |
379 _cache.append(version.split('.')) | |
380 return _cache[0] | |
381 | |
382 | |
383 def _macosx_arch(machine): | |
384 return {'PowerPC': 'ppc', 'Power_Macintosh': 'ppc'}.get(machine, machine) | |
385 | |
386 | |
387 def get_build_platform(): | |
388 """Return this platform's string for platform-specific distributions | |
389 | |
390 XXX Currently this is the same as ``distutils.util.get_platform()``, but it | |
391 needs some hacks for Linux and Mac OS X. | |
392 """ | |
393 from sysconfig import get_platform | |
394 | |
395 plat = get_platform() | |
396 if sys.platform == "darwin" and not plat.startswith('macosx-'): | |
397 try: | |
398 version = _macosx_vers() | |
399 machine = os.uname()[4].replace(" ", "_") | |
400 return "macosx-%d.%d-%s" % ( | |
401 int(version[0]), int(version[1]), | |
402 _macosx_arch(machine), | |
403 ) | |
404 except ValueError: | |
405 # if someone is running a non-Mac darwin system, this will fall | |
406 # through to the default implementation | |
407 pass | |
408 return plat | |
409 | |
410 | |
411 macosVersionString = re.compile(r"macosx-(\d+)\.(\d+)-(.*)") | |
412 darwinVersionString = re.compile(r"darwin-(\d+)\.(\d+)\.(\d+)-(.*)") | |
413 # XXX backward compat | |
414 get_platform = get_build_platform | |
415 | |
416 | |
417 def compatible_platforms(provided, required): | |
418 """Can code for the `provided` platform run on the `required` platform? | |
419 | |
420 Returns true if either platform is ``None``, or the platforms are equal. | |
421 | |
422 XXX Needs compatibility checks for Linux and other unixy OSes. | |
423 """ | |
424 if provided is None or required is None or provided == required: | |
425 # easy case | |
426 return True | |
427 | |
428 # Mac OS X special cases | |
429 reqMac = macosVersionString.match(required) | |
430 if reqMac: | |
431 provMac = macosVersionString.match(provided) | |
432 | |
433 # is this a Mac package? | |
434 if not provMac: | |
435 # this is backwards compatibility for packages built before | |
436 # setuptools 0.6. All packages built after this point will | |
437 # use the new macosx designation. | |
438 provDarwin = darwinVersionString.match(provided) | |
439 if provDarwin: | |
440 dversion = int(provDarwin.group(1)) | |
441 macosversion = "%s.%s" % (reqMac.group(1), reqMac.group(2)) | |
442 if dversion == 7 and macosversion >= "10.3" or \ | |
443 dversion == 8 and macosversion >= "10.4": | |
444 return True | |
445 # egg isn't macosx or legacy darwin | |
446 return False | |
447 | |
448 # are they the same major version and machine type? | |
449 if provMac.group(1) != reqMac.group(1) or \ | |
450 provMac.group(3) != reqMac.group(3): | |
451 return False | |
452 | |
453 # is the required OS major update >= the provided one? | |
454 if int(provMac.group(2)) > int(reqMac.group(2)): | |
455 return False | |
456 | |
457 return True | |
458 | |
459 # XXX Linux and other platforms' special cases should go here | |
460 return False | |
461 | |
462 | |
463 def run_script(dist_spec, script_name): | |
464 """Locate distribution `dist_spec` and run its `script_name` script""" | |
465 ns = sys._getframe(1).f_globals | |
466 name = ns['__name__'] | |
467 ns.clear() | |
468 ns['__name__'] = name | |
469 require(dist_spec)[0].run_script(script_name, ns) | |
470 | |
471 | |
472 # backward compatibility | |
473 run_main = run_script | |
474 | |
475 | |
476 def get_distribution(dist): | |
477 """Return a current distribution object for a Requirement or string""" | |
478 if isinstance(dist, six.string_types): | |
479 dist = Requirement.parse(dist) | |
480 if isinstance(dist, Requirement): | |
481 dist = get_provider(dist) | |
482 if not isinstance(dist, Distribution): | |
483 raise TypeError("Expected string, Requirement, or Distribution", dist) | |
484 return dist | |
485 | |
486 | |
487 def load_entry_point(dist, group, name): | |
488 """Return `name` entry point of `group` for `dist` or raise ImportError""" | |
489 return get_distribution(dist).load_entry_point(group, name) | |
490 | |
491 | |
492 def get_entry_map(dist, group=None): | |
493 """Return the entry point map for `group`, or the full entry map""" | |
494 return get_distribution(dist).get_entry_map(group) | |
495 | |
496 | |
497 def get_entry_info(dist, group, name): | |
498 """Return the EntryPoint object for `group`+`name`, or ``None``""" | |
499 return get_distribution(dist).get_entry_info(group, name) | |
500 | |
501 | |
502 class IMetadataProvider: | |
503 def has_metadata(name): | |
504 """Does the package's distribution contain the named metadata?""" | |
505 | |
506 def get_metadata(name): | |
507 """The named metadata resource as a string""" | |
508 | |
509 def get_metadata_lines(name): | |
510 """Yield named metadata resource as list of non-blank non-comment lines | |
511 | |
512 Leading and trailing whitespace is stripped from each line, and lines | |
513 with ``#`` as the first non-blank character are omitted.""" | |
514 | |
515 def metadata_isdir(name): | |
516 """Is the named metadata a directory? (like ``os.path.isdir()``)""" | |
517 | |
518 def metadata_listdir(name): | |
519 """List of metadata names in the directory (like ``os.listdir()``)""" | |
520 | |
521 def run_script(script_name, namespace): | |
522 """Execute the named script in the supplied namespace dictionary""" | |
523 | |
524 | |
525 class IResourceProvider(IMetadataProvider): | |
526 """An object that provides access to package resources""" | |
527 | |
528 def get_resource_filename(manager, resource_name): | |
529 """Return a true filesystem path for `resource_name` | |
530 | |
531 `manager` must be an ``IResourceManager``""" | |
532 | |
533 def get_resource_stream(manager, resource_name): | |
534 """Return a readable file-like object for `resource_name` | |
535 | |
536 `manager` must be an ``IResourceManager``""" | |
537 | |
538 def get_resource_string(manager, resource_name): | |
539 """Return a string containing the contents of `resource_name` | |
540 | |
541 `manager` must be an ``IResourceManager``""" | |
542 | |
543 def has_resource(resource_name): | |
544 """Does the package contain the named resource?""" | |
545 | |
546 def resource_isdir(resource_name): | |
547 """Is the named resource a directory? (like ``os.path.isdir()``)""" | |
548 | |
549 def resource_listdir(resource_name): | |
550 """List of resource names in the directory (like ``os.listdir()``)""" | |
551 | |
552 | |
553 class WorkingSet: | |
554 """A collection of active distributions on sys.path (or a similar list)""" | |
555 | |
556 def __init__(self, entries=None): | |
557 """Create working set from list of path entries (default=sys.path)""" | |
558 self.entries = [] | |
559 self.entry_keys = {} | |
560 self.by_key = {} | |
561 self.callbacks = [] | |
562 | |
563 if entries is None: | |
564 entries = sys.path | |
565 | |
566 for entry in entries: | |
567 self.add_entry(entry) | |
568 | |
569 @classmethod | |
570 def _build_master(cls): | |
571 """ | |
572 Prepare the master working set. | |
573 """ | |
574 ws = cls() | |
575 try: | |
576 from __main__ import __requires__ | |
577 except ImportError: | |
578 # The main program does not list any requirements | |
579 return ws | |
580 | |
581 # ensure the requirements are met | |
582 try: | |
583 ws.require(__requires__) | |
584 except VersionConflict: | |
585 return cls._build_from_requirements(__requires__) | |
586 | |
587 return ws | |
588 | |
589 @classmethod | |
590 def _build_from_requirements(cls, req_spec): | |
591 """ | |
592 Build a working set from a requirement spec. Rewrites sys.path. | |
593 """ | |
594 # try it without defaults already on sys.path | |
595 # by starting with an empty path | |
596 ws = cls([]) | |
597 reqs = parse_requirements(req_spec) | |
598 dists = ws.resolve(reqs, Environment()) | |
599 for dist in dists: | |
600 ws.add(dist) | |
601 | |
602 # add any missing entries from sys.path | |
603 for entry in sys.path: | |
604 if entry not in ws.entries: | |
605 ws.add_entry(entry) | |
606 | |
607 # then copy back to sys.path | |
608 sys.path[:] = ws.entries | |
609 return ws | |
610 | |
611 def add_entry(self, entry): | |
612 """Add a path item to ``.entries``, finding any distributions on it | |
613 | |
614 ``find_distributions(entry, True)`` is used to find distributions | |
615 corresponding to the path entry, and they are added. `entry` is | |
616 always appended to ``.entries``, even if it is already present. | |
617 (This is because ``sys.path`` can contain the same value more than | |
618 once, and the ``.entries`` of the ``sys.path`` WorkingSet should always | |
619 equal ``sys.path``.) | |
620 """ | |
621 self.entry_keys.setdefault(entry, []) | |
622 self.entries.append(entry) | |
623 for dist in find_distributions(entry, True): | |
624 self.add(dist, entry, False) | |
625 | |
626 def __contains__(self, dist): | |
627 """True if `dist` is the active distribution for its project""" | |
628 return self.by_key.get(dist.key) == dist | |
629 | |
630 def find(self, req): | |
631 """Find a distribution matching requirement `req` | |
632 | |
633 If there is an active distribution for the requested project, this | |
634 returns it as long as it meets the version requirement specified by | |
635 `req`. But, if there is an active distribution for the project and it | |
636 does *not* meet the `req` requirement, ``VersionConflict`` is raised. | |
637 If there is no active distribution for the requested project, ``None`` | |
638 is returned. | |
639 """ | |
640 dist = self.by_key.get(req.key) | |
641 if dist is not None and dist not in req: | |
642 # XXX add more info | |
643 raise VersionConflict(dist, req) | |
644 return dist | |
645 | |
646 def iter_entry_points(self, group, name=None): | |
647 """Yield entry point objects from `group` matching `name` | |
648 | |
649 If `name` is None, yields all entry points in `group` from all | |
650 distributions in the working set, otherwise only ones matching | |
651 both `group` and `name` are yielded (in distribution order). | |
652 """ | |
653 return ( | |
654 entry | |
655 for dist in self | |
656 for entry in dist.get_entry_map(group).values() | |
657 if name is None or name == entry.name | |
658 ) | |
659 | |
660 def run_script(self, requires, script_name): | |
661 """Locate distribution for `requires` and run `script_name` script""" | |
662 ns = sys._getframe(1).f_globals | |
663 name = ns['__name__'] | |
664 ns.clear() | |
665 ns['__name__'] = name | |
666 self.require(requires)[0].run_script(script_name, ns) | |
667 | |
668 def __iter__(self): | |
669 """Yield distributions for non-duplicate projects in the working set | |
670 | |
671 The yield order is the order in which the items' path entries were | |
672 added to the working set. | |
673 """ | |
674 seen = {} | |
675 for item in self.entries: | |
676 if item not in self.entry_keys: | |
677 # workaround a cache issue | |
678 continue | |
679 | |
680 for key in self.entry_keys[item]: | |
681 if key not in seen: | |
682 seen[key] = 1 | |
683 yield self.by_key[key] | |
684 | |
685 def add(self, dist, entry=None, insert=True, replace=False): | |
686 """Add `dist` to working set, associated with `entry` | |
687 | |
688 If `entry` is unspecified, it defaults to the ``.location`` of `dist`. | |
689 On exit from this routine, `entry` is added to the end of the working | |
690 set's ``.entries`` (if it wasn't already present). | |
691 | |
692 `dist` is only added to the working set if it's for a project that | |
693 doesn't already have a distribution in the set, unless `replace=True`. | |
694 If it's added, any callbacks registered with the ``subscribe()`` method | |
695 will be called. | |
696 """ | |
697 if insert: | |
698 dist.insert_on(self.entries, entry, replace=replace) | |
699 | |
700 if entry is None: | |
701 entry = dist.location | |
702 keys = self.entry_keys.setdefault(entry, []) | |
703 keys2 = self.entry_keys.setdefault(dist.location, []) | |
704 if not replace and dist.key in self.by_key: | |
705 # ignore hidden distros | |
706 return | |
707 | |
708 self.by_key[dist.key] = dist | |
709 if dist.key not in keys: | |
710 keys.append(dist.key) | |
711 if dist.key not in keys2: | |
712 keys2.append(dist.key) | |
713 self._added_new(dist) | |
714 | |
715 def resolve(self, requirements, env=None, installer=None, | |
716 replace_conflicting=False, extras=None): | |
717 """List all distributions needed to (recursively) meet `requirements` | |
718 | |
719 `requirements` must be a sequence of ``Requirement`` objects. `env`, | |
720 if supplied, should be an ``Environment`` instance. If | |
721 not supplied, it defaults to all distributions available within any | |
722 entry or distribution in the working set. `installer`, if supplied, | |
723 will be invoked with each requirement that cannot be met by an | |
724 already-installed distribution; it should return a ``Distribution`` or | |
725 ``None``. | |
726 | |
727 Unless `replace_conflicting=True`, raises a VersionConflict exception | |
728 if | |
729 any requirements are found on the path that have the correct name but | |
730 the wrong version. Otherwise, if an `installer` is supplied it will be | |
731 invoked to obtain the correct version of the requirement and activate | |
732 it. | |
733 | |
734 `extras` is a list of the extras to be used with these requirements. | |
735 This is important because extra requirements may look like `my_req; | |
736 extra = "my_extra"`, which would otherwise be interpreted as a purely | |
737 optional requirement. Instead, we want to be able to assert that these | |
738 requirements are truly required. | |
739 """ | |
740 | |
741 # set up the stack | |
742 requirements = list(requirements)[::-1] | |
743 # set of processed requirements | |
744 processed = {} | |
745 # key -> dist | |
746 best = {} | |
747 to_activate = [] | |
748 | |
749 req_extras = _ReqExtras() | |
750 | |
751 # Mapping of requirement to set of distributions that required it; | |
752 # useful for reporting info about conflicts. | |
753 required_by = collections.defaultdict(set) | |
754 | |
755 while requirements: | |
756 # process dependencies breadth-first | |
757 req = requirements.pop(0) | |
758 if req in processed: | |
759 # Ignore cyclic or redundant dependencies | |
760 continue | |
761 | |
762 if not req_extras.markers_pass(req, extras): | |
763 continue | |
764 | |
765 dist = best.get(req.key) | |
766 if dist is None: | |
767 # Find the best distribution and add it to the map | |
768 dist = self.by_key.get(req.key) | |
769 if dist is None or (dist not in req and replace_conflicting): | |
770 ws = self | |
771 if env is None: | |
772 if dist is None: | |
773 env = Environment(self.entries) | |
774 else: | |
775 # Use an empty environment and workingset to avoid | |
776 # any further conflicts with the conflicting | |
777 # distribution | |
778 env = Environment([]) | |
779 ws = WorkingSet([]) | |
780 dist = best[req.key] = env.best_match( | |
781 req, ws, installer, | |
782 replace_conflicting=replace_conflicting | |
783 ) | |
784 if dist is None: | |
785 requirers = required_by.get(req, None) | |
786 raise DistributionNotFound(req, requirers) | |
787 to_activate.append(dist) | |
788 if dist not in req: | |
789 # Oops, the "best" so far conflicts with a dependency | |
790 dependent_req = required_by[req] | |
791 raise VersionConflict(dist, req).with_context(dependent_req) | |
792 | |
793 # push the new requirements onto the stack | |
794 new_requirements = dist.requires(req.extras)[::-1] | |
795 requirements.extend(new_requirements) | |
796 | |
797 # Register the new requirements needed by req | |
798 for new_requirement in new_requirements: | |
799 required_by[new_requirement].add(req.project_name) | |
800 req_extras[new_requirement] = req.extras | |
801 | |
802 processed[req] = True | |
803 | |
804 # return list of distros to activate | |
805 return to_activate | |
806 | |
807 def find_plugins( | |
808 self, plugin_env, full_env=None, installer=None, fallback=True): | |
809 """Find all activatable distributions in `plugin_env` | |
810 | |
811 Example usage:: | |
812 | |
813 distributions, errors = working_set.find_plugins( | |
814 Environment(plugin_dirlist) | |
815 ) | |
816 # add plugins+libs to sys.path | |
817 map(working_set.add, distributions) | |
818 # display errors | |
819 print('Could not load', errors) | |
820 | |
821 The `plugin_env` should be an ``Environment`` instance that contains | |
822 only distributions that are in the project's "plugin directory" or | |
823 directories. The `full_env`, if supplied, should be an ``Environment`` | |
824 contains all currently-available distributions. If `full_env` is not | |
825 supplied, one is created automatically from the ``WorkingSet`` this | |
826 method is called on, which will typically mean that every directory on | |
827 ``sys.path`` will be scanned for distributions. | |
828 | |
829 `installer` is a standard installer callback as used by the | |
830 ``resolve()`` method. The `fallback` flag indicates whether we should | |
831 attempt to resolve older versions of a plugin if the newest version | |
832 cannot be resolved. | |
833 | |
834 This method returns a 2-tuple: (`distributions`, `error_info`), where | |
835 `distributions` is a list of the distributions found in `plugin_env` | |
836 that were loadable, along with any other distributions that are needed | |
837 to resolve their dependencies. `error_info` is a dictionary mapping | |
838 unloadable plugin distributions to an exception instance describing the | |
839 error that occurred. Usually this will be a ``DistributionNotFound`` or | |
840 ``VersionConflict`` instance. | |
841 """ | |
842 | |
843 plugin_projects = list(plugin_env) | |
844 # scan project names in alphabetic order | |
845 plugin_projects.sort() | |
846 | |
847 error_info = {} | |
848 distributions = {} | |
849 | |
850 if full_env is None: | |
851 env = Environment(self.entries) | |
852 env += plugin_env | |
853 else: | |
854 env = full_env + plugin_env | |
855 | |
856 shadow_set = self.__class__([]) | |
857 # put all our entries in shadow_set | |
858 list(map(shadow_set.add, self)) | |
859 | |
860 for project_name in plugin_projects: | |
861 | |
862 for dist in plugin_env[project_name]: | |
863 | |
864 req = [dist.as_requirement()] | |
865 | |
866 try: | |
867 resolvees = shadow_set.resolve(req, env, installer) | |
868 | |
869 except ResolutionError as v: | |
870 # save error info | |
871 error_info[dist] = v | |
872 if fallback: | |
873 # try the next older version of project | |
874 continue | |
875 else: | |
876 # give up on this project, keep going | |
877 break | |
878 | |
879 else: | |
880 list(map(shadow_set.add, resolvees)) | |
881 distributions.update(dict.fromkeys(resolvees)) | |
882 | |
883 # success, no need to try any more versions of this project | |
884 break | |
885 | |
886 distributions = list(distributions) | |
887 distributions.sort() | |
888 | |
889 return distributions, error_info | |
890 | |
891 def require(self, *requirements): | |
892 """Ensure that distributions matching `requirements` are activated | |
893 | |
894 `requirements` must be a string or a (possibly-nested) sequence | |
895 thereof, specifying the distributions and versions required. The | |
896 return value is a sequence of the distributions that needed to be | |
897 activated to fulfill the requirements; all relevant distributions are | |
898 included, even if they were already activated in this working set. | |
899 """ | |
900 needed = self.resolve(parse_requirements(requirements)) | |
901 | |
902 for dist in needed: | |
903 self.add(dist) | |
904 | |
905 return needed | |
906 | |
907 def subscribe(self, callback, existing=True): | |
908 """Invoke `callback` for all distributions | |
909 | |
910 If `existing=True` (default), | |
911 call on all existing ones, as well. | |
912 """ | |
913 if callback in self.callbacks: | |
914 return | |
915 self.callbacks.append(callback) | |
916 if not existing: | |
917 return | |
918 for dist in self: | |
919 callback(dist) | |
920 | |
921 def _added_new(self, dist): | |
922 for callback in self.callbacks: | |
923 callback(dist) | |
924 | |
925 def __getstate__(self): | |
926 return ( | |
927 self.entries[:], self.entry_keys.copy(), self.by_key.copy(), | |
928 self.callbacks[:] | |
929 ) | |
930 | |
931 def __setstate__(self, e_k_b_c): | |
932 entries, keys, by_key, callbacks = e_k_b_c | |
933 self.entries = entries[:] | |
934 self.entry_keys = keys.copy() | |
935 self.by_key = by_key.copy() | |
936 self.callbacks = callbacks[:] | |
937 | |
938 | |
939 class _ReqExtras(dict): | |
940 """ | |
941 Map each requirement to the extras that demanded it. | |
942 """ | |
943 | |
944 def markers_pass(self, req, extras=None): | |
945 """ | |
946 Evaluate markers for req against each extra that | |
947 demanded it. | |
948 | |
949 Return False if the req has a marker and fails | |
950 evaluation. Otherwise, return True. | |
951 """ | |
952 extra_evals = ( | |
953 req.marker.evaluate({'extra': extra}) | |
954 for extra in self.get(req, ()) + (extras or (None,)) | |
955 ) | |
956 return not req.marker or any(extra_evals) | |
957 | |
958 | |
959 class Environment: | |
960 """Searchable snapshot of distributions on a search path""" | |
961 | |
962 def __init__( | |
963 self, search_path=None, platform=get_supported_platform(), | |
964 python=PY_MAJOR): | |
965 """Snapshot distributions available on a search path | |
966 | |
967 Any distributions found on `search_path` are added to the environment. | |
968 `search_path` should be a sequence of ``sys.path`` items. If not | |
969 supplied, ``sys.path`` is used. | |
970 | |
971 `platform` is an optional string specifying the name of the platform | |
972 that platform-specific distributions must be compatible with. If | |
973 unspecified, it defaults to the current platform. `python` is an | |
974 optional string naming the desired version of Python (e.g. ``'3.6'``); | |
975 it defaults to the current version. | |
976 | |
977 You may explicitly set `platform` (and/or `python`) to ``None`` if you | |
978 wish to map *all* distributions, not just those compatible with the | |
979 running platform or Python version. | |
980 """ | |
981 self._distmap = {} | |
982 self.platform = platform | |
983 self.python = python | |
984 self.scan(search_path) | |
985 | |
986 def can_add(self, dist): | |
987 """Is distribution `dist` acceptable for this environment? | |
988 | |
989 The distribution must match the platform and python version | |
990 requirements specified when this environment was created, or False | |
991 is returned. | |
992 """ | |
993 py_compat = ( | |
994 self.python is None | |
995 or dist.py_version is None | |
996 or dist.py_version == self.python | |
997 ) | |
998 return py_compat and compatible_platforms(dist.platform, self.platform) | |
999 | |
1000 def remove(self, dist): | |
1001 """Remove `dist` from the environment""" | |
1002 self._distmap[dist.key].remove(dist) | |
1003 | |
1004 def scan(self, search_path=None): | |
1005 """Scan `search_path` for distributions usable in this environment | |
1006 | |
1007 Any distributions found are added to the environment. | |
1008 `search_path` should be a sequence of ``sys.path`` items. If not | |
1009 supplied, ``sys.path`` is used. Only distributions conforming to | |
1010 the platform/python version defined at initialization are added. | |
1011 """ | |
1012 if search_path is None: | |
1013 search_path = sys.path | |
1014 | |
1015 for item in search_path: | |
1016 for dist in find_distributions(item): | |
1017 self.add(dist) | |
1018 | |
1019 def __getitem__(self, project_name): | |
1020 """Return a newest-to-oldest list of distributions for `project_name` | |
1021 | |
1022 Uses case-insensitive `project_name` comparison, assuming all the | |
1023 project's distributions use their project's name converted to all | |
1024 lowercase as their key. | |
1025 | |
1026 """ | |
1027 distribution_key = project_name.lower() | |
1028 return self._distmap.get(distribution_key, []) | |
1029 | |
1030 def add(self, dist): | |
1031 """Add `dist` if we ``can_add()`` it and it has not already been added | |
1032 """ | |
1033 if self.can_add(dist) and dist.has_version(): | |
1034 dists = self._distmap.setdefault(dist.key, []) | |
1035 if dist not in dists: | |
1036 dists.append(dist) | |
1037 dists.sort(key=operator.attrgetter('hashcmp'), reverse=True) | |
1038 | |
1039 def best_match( | |
1040 self, req, working_set, installer=None, replace_conflicting=False): | |
1041 """Find distribution best matching `req` and usable on `working_set` | |
1042 | |
1043 This calls the ``find(req)`` method of the `working_set` to see if a | |
1044 suitable distribution is already active. (This may raise | |
1045 ``VersionConflict`` if an unsuitable version of the project is already | |
1046 active in the specified `working_set`.) If a suitable distribution | |
1047 isn't active, this method returns the newest distribution in the | |
1048 environment that meets the ``Requirement`` in `req`. If no suitable | |
1049 distribution is found, and `installer` is supplied, then the result of | |
1050 calling the environment's ``obtain(req, installer)`` method will be | |
1051 returned. | |
1052 """ | |
1053 try: | |
1054 dist = working_set.find(req) | |
1055 except VersionConflict: | |
1056 if not replace_conflicting: | |
1057 raise | |
1058 dist = None | |
1059 if dist is not None: | |
1060 return dist | |
1061 for dist in self[req.key]: | |
1062 if dist in req: | |
1063 return dist | |
1064 # try to download/install | |
1065 return self.obtain(req, installer) | |
1066 | |
1067 def obtain(self, requirement, installer=None): | |
1068 """Obtain a distribution matching `requirement` (e.g. via download) | |
1069 | |
1070 Obtain a distro that matches requirement (e.g. via download). In the | |
1071 base ``Environment`` class, this routine just returns | |
1072 ``installer(requirement)``, unless `installer` is None, in which case | |
1073 None is returned instead. This method is a hook that allows subclasses | |
1074 to attempt other ways of obtaining a distribution before falling back | |
1075 to the `installer` argument.""" | |
1076 if installer is not None: | |
1077 return installer(requirement) | |
1078 | |
1079 def __iter__(self): | |
1080 """Yield the unique project names of the available distributions""" | |
1081 for key in self._distmap.keys(): | |
1082 if self[key]: | |
1083 yield key | |
1084 | |
1085 def __iadd__(self, other): | |
1086 """In-place addition of a distribution or environment""" | |
1087 if isinstance(other, Distribution): | |
1088 self.add(other) | |
1089 elif isinstance(other, Environment): | |
1090 for project in other: | |
1091 for dist in other[project]: | |
1092 self.add(dist) | |
1093 else: | |
1094 raise TypeError("Can't add %r to environment" % (other,)) | |
1095 return self | |
1096 | |
1097 def __add__(self, other): | |
1098 """Add an environment or distribution to an environment""" | |
1099 new = self.__class__([], platform=None, python=None) | |
1100 for env in self, other: | |
1101 new += env | |
1102 return new | |
1103 | |
1104 | |
1105 # XXX backward compatibility | |
1106 AvailableDistributions = Environment | |
1107 | |
1108 | |
1109 class ExtractionError(RuntimeError): | |
1110 """An error occurred extracting a resource | |
1111 | |
1112 The following attributes are available from instances of this exception: | |
1113 | |
1114 manager | |
1115 The resource manager that raised this exception | |
1116 | |
1117 cache_path | |
1118 The base directory for resource extraction | |
1119 | |
1120 original_error | |
1121 The exception instance that caused extraction to fail | |
1122 """ | |
1123 | |
1124 | |
1125 class ResourceManager: | |
1126 """Manage resource extraction and packages""" | |
1127 extraction_path = None | |
1128 | |
1129 def __init__(self): | |
1130 self.cached_files = {} | |
1131 | |
1132 def resource_exists(self, package_or_requirement, resource_name): | |
1133 """Does the named resource exist?""" | |
1134 return get_provider(package_or_requirement).has_resource(resource_name) | |
1135 | |
1136 def resource_isdir(self, package_or_requirement, resource_name): | |
1137 """Is the named resource an existing directory?""" | |
1138 return get_provider(package_or_requirement).resource_isdir( | |
1139 resource_name | |
1140 ) | |
1141 | |
1142 def resource_filename(self, package_or_requirement, resource_name): | |
1143 """Return a true filesystem path for specified resource""" | |
1144 return get_provider(package_or_requirement).get_resource_filename( | |
1145 self, resource_name | |
1146 ) | |
1147 | |
1148 def resource_stream(self, package_or_requirement, resource_name): | |
1149 """Return a readable file-like object for specified resource""" | |
1150 return get_provider(package_or_requirement).get_resource_stream( | |
1151 self, resource_name | |
1152 ) | |
1153 | |
1154 def resource_string(self, package_or_requirement, resource_name): | |
1155 """Return specified resource as a string""" | |
1156 return get_provider(package_or_requirement).get_resource_string( | |
1157 self, resource_name | |
1158 ) | |
1159 | |
1160 def resource_listdir(self, package_or_requirement, resource_name): | |
1161 """List the contents of the named resource directory""" | |
1162 return get_provider(package_or_requirement).resource_listdir( | |
1163 resource_name | |
1164 ) | |
1165 | |
1166 def extraction_error(self): | |
1167 """Give an error message for problems extracting file(s)""" | |
1168 | |
1169 old_exc = sys.exc_info()[1] | |
1170 cache_path = self.extraction_path or get_default_cache() | |
1171 | |
1172 tmpl = textwrap.dedent(""" | |
1173 Can't extract file(s) to egg cache | |
1174 | |
1175 The following error occurred while trying to extract file(s) | |
1176 to the Python egg cache: | |
1177 | |
1178 {old_exc} | |
1179 | |
1180 The Python egg cache directory is currently set to: | |
1181 | |
1182 {cache_path} | |
1183 | |
1184 Perhaps your account does not have write access to this directory? | |
1185 You can change the cache directory by setting the PYTHON_EGG_CACHE | |
1186 environment variable to point to an accessible directory. | |
1187 """).lstrip() | |
1188 err = ExtractionError(tmpl.format(**locals())) | |
1189 err.manager = self | |
1190 err.cache_path = cache_path | |
1191 err.original_error = old_exc | |
1192 raise err | |
1193 | |
1194 def get_cache_path(self, archive_name, names=()): | |
1195 """Return absolute location in cache for `archive_name` and `names` | |
1196 | |
1197 The parent directory of the resulting path will be created if it does | |
1198 not already exist. `archive_name` should be the base filename of the | |
1199 enclosing egg (which may not be the name of the enclosing zipfile!), | |
1200 including its ".egg" extension. `names`, if provided, should be a | |
1201 sequence of path name parts "under" the egg's extraction location. | |
1202 | |
1203 This method should only be called by resource providers that need to | |
1204 obtain an extraction location, and only for names they intend to | |
1205 extract, as it tracks the generated names for possible cleanup later. | |
1206 """ | |
1207 extract_path = self.extraction_path or get_default_cache() | |
1208 target_path = os.path.join(extract_path, archive_name + '-tmp', *names) | |
1209 try: | |
1210 _bypass_ensure_directory(target_path) | |
1211 except Exception: | |
1212 self.extraction_error() | |
1213 | |
1214 self._warn_unsafe_extraction_path(extract_path) | |
1215 | |
1216 self.cached_files[target_path] = 1 | |
1217 return target_path | |
1218 | |
1219 @staticmethod | |
1220 def _warn_unsafe_extraction_path(path): | |
1221 """ | |
1222 If the default extraction path is overridden and set to an insecure | |
1223 location, such as /tmp, it opens up an opportunity for an attacker to | |
1224 replace an extracted file with an unauthorized payload. Warn the user | |
1225 if a known insecure location is used. | |
1226 | |
1227 See Distribute #375 for more details. | |
1228 """ | |
1229 if os.name == 'nt' and not path.startswith(os.environ['windir']): | |
1230 # On Windows, permissions are generally restrictive by default | |
1231 # and temp directories are not writable by other users, so | |
1232 # bypass the warning. | |
1233 return | |
1234 mode = os.stat(path).st_mode | |
1235 if mode & stat.S_IWOTH or mode & stat.S_IWGRP: | |
1236 msg = ( | |
1237 "%s is writable by group/others and vulnerable to attack " | |
1238 "when " | |
1239 "used with get_resource_filename. Consider a more secure " | |
1240 "location (set with .set_extraction_path or the " | |
1241 "PYTHON_EGG_CACHE environment variable)." % path | |
1242 ) | |
1243 warnings.warn(msg, UserWarning) | |
1244 | |
1245 def postprocess(self, tempname, filename): | |
1246 """Perform any platform-specific postprocessing of `tempname` | |
1247 | |
1248 This is where Mac header rewrites should be done; other platforms don't | |
1249 have anything special they should do. | |
1250 | |
1251 Resource providers should call this method ONLY after successfully | |
1252 extracting a compressed resource. They must NOT call it on resources | |
1253 that are already in the filesystem. | |
1254 | |
1255 `tempname` is the current (temporary) name of the file, and `filename` | |
1256 is the name it will be renamed to by the caller after this routine | |
1257 returns. | |
1258 """ | |
1259 | |
1260 if os.name == 'posix': | |
1261 # Make the resource executable | |
1262 mode = ((os.stat(tempname).st_mode) | 0o555) & 0o7777 | |
1263 os.chmod(tempname, mode) | |
1264 | |
1265 def set_extraction_path(self, path): | |
1266 """Set the base path where resources will be extracted to, if needed. | |
1267 | |
1268 If you do not call this routine before any extractions take place, the | |
1269 path defaults to the return value of ``get_default_cache()``. (Which | |
1270 is based on the ``PYTHON_EGG_CACHE`` environment variable, with various | |
1271 platform-specific fallbacks. See that routine's documentation for more | |
1272 details.) | |
1273 | |
1274 Resources are extracted to subdirectories of this path based upon | |
1275 information given by the ``IResourceProvider``. You may set this to a | |
1276 temporary directory, but then you must call ``cleanup_resources()`` to | |
1277 delete the extracted files when done. There is no guarantee that | |
1278 ``cleanup_resources()`` will be able to remove all extracted files. | |
1279 | |
1280 (Note: you may not change the extraction path for a given resource | |
1281 manager once resources have been extracted, unless you first call | |
1282 ``cleanup_resources()``.) | |
1283 """ | |
1284 if self.cached_files: | |
1285 raise ValueError( | |
1286 "Can't change extraction path, files already extracted" | |
1287 ) | |
1288 | |
1289 self.extraction_path = path | |
1290 | |
1291 def cleanup_resources(self, force=False): | |
1292 """ | |
1293 Delete all extracted resource files and directories, returning a list | |
1294 of the file and directory names that could not be successfully removed. | |
1295 This function does not have any concurrency protection, so it should | |
1296 generally only be called when the extraction path is a temporary | |
1297 directory exclusive to a single process. This method is not | |
1298 automatically called; you must call it explicitly or register it as an | |
1299 ``atexit`` function if you wish to ensure cleanup of a temporary | |
1300 directory used for extractions. | |
1301 """ | |
1302 # XXX | |
1303 | |
1304 | |
1305 def get_default_cache(): | |
1306 """ | |
1307 Return the ``PYTHON_EGG_CACHE`` environment variable | |
1308 or a platform-relevant user cache dir for an app | |
1309 named "Python-Eggs". | |
1310 """ | |
1311 return ( | |
1312 os.environ.get('PYTHON_EGG_CACHE') | |
1313 or appdirs.user_cache_dir(appname='Python-Eggs') | |
1314 ) | |
1315 | |
1316 | |
1317 def safe_name(name): | |
1318 """Convert an arbitrary string to a standard distribution name | |
1319 | |
1320 Any runs of non-alphanumeric/. characters are replaced with a single '-'. | |
1321 """ | |
1322 return re.sub('[^A-Za-z0-9.]+', '-', name) | |
1323 | |
1324 | |
1325 def safe_version(version): | |
1326 """ | |
1327 Convert an arbitrary string to a standard version string | |
1328 """ | |
1329 try: | |
1330 # normalize the version | |
1331 return str(packaging.version.Version(version)) | |
1332 except packaging.version.InvalidVersion: | |
1333 version = version.replace(' ', '.') | |
1334 return re.sub('[^A-Za-z0-9.]+', '-', version) | |
1335 | |
1336 | |
1337 def safe_extra(extra): | |
1338 """Convert an arbitrary string to a standard 'extra' name | |
1339 | |
1340 Any runs of non-alphanumeric characters are replaced with a single '_', | |
1341 and the result is always lowercased. | |
1342 """ | |
1343 return re.sub('[^A-Za-z0-9.-]+', '_', extra).lower() | |
1344 | |
1345 | |
1346 def to_filename(name): | |
1347 """Convert a project or version name to its filename-escaped form | |
1348 | |
1349 Any '-' characters are currently replaced with '_'. | |
1350 """ | |
1351 return name.replace('-', '_') | |
1352 | |
1353 | |
1354 def invalid_marker(text): | |
1355 """ | |
1356 Validate text as a PEP 508 environment marker; return an exception | |
1357 if invalid or False otherwise. | |
1358 """ | |
1359 try: | |
1360 evaluate_marker(text) | |
1361 except SyntaxError as e: | |
1362 e.filename = None | |
1363 e.lineno = None | |
1364 return e | |
1365 return False | |
1366 | |
1367 | |
1368 def evaluate_marker(text, extra=None): | |
1369 """ | |
1370 Evaluate a PEP 508 environment marker. | |
1371 Return a boolean indicating the marker result in this environment. | |
1372 Raise SyntaxError if marker is invalid. | |
1373 | |
1374 This implementation uses the 'pyparsing' module. | |
1375 """ | |
1376 try: | |
1377 marker = packaging.markers.Marker(text) | |
1378 return marker.evaluate() | |
1379 except packaging.markers.InvalidMarker as e: | |
1380 raise SyntaxError(e) | |
1381 | |
1382 | |
1383 class NullProvider: | |
1384 """Try to implement resources and metadata for arbitrary PEP 302 loaders""" | |
1385 | |
1386 egg_name = None | |
1387 egg_info = None | |
1388 loader = None | |
1389 | |
1390 def __init__(self, module): | |
1391 self.loader = getattr(module, '__loader__', None) | |
1392 self.module_path = os.path.dirname(getattr(module, '__file__', '')) | |
1393 | |
1394 def get_resource_filename(self, manager, resource_name): | |
1395 return self._fn(self.module_path, resource_name) | |
1396 | |
1397 def get_resource_stream(self, manager, resource_name): | |
1398 return io.BytesIO(self.get_resource_string(manager, resource_name)) | |
1399 | |
1400 def get_resource_string(self, manager, resource_name): | |
1401 return self._get(self._fn(self.module_path, resource_name)) | |
1402 | |
1403 def has_resource(self, resource_name): | |
1404 return self._has(self._fn(self.module_path, resource_name)) | |
1405 | |
1406 def _get_metadata_path(self, name): | |
1407 return self._fn(self.egg_info, name) | |
1408 | |
1409 def has_metadata(self, name): | |
1410 if not self.egg_info: | |
1411 return self.egg_info | |
1412 | |
1413 path = self._get_metadata_path(name) | |
1414 return self._has(path) | |
1415 | |
1416 def get_metadata(self, name): | |
1417 if not self.egg_info: | |
1418 return "" | |
1419 path = self._get_metadata_path(name) | |
1420 value = self._get(path) | |
1421 if six.PY2: | |
1422 return value | |
1423 try: | |
1424 return value.decode('utf-8') | |
1425 except UnicodeDecodeError as exc: | |
1426 # Include the path in the error message to simplify | |
1427 # troubleshooting, and without changing the exception type. | |
1428 exc.reason += ' in {} file at path: {}'.format(name, path) | |
1429 raise | |
1430 | |
1431 def get_metadata_lines(self, name): | |
1432 return yield_lines(self.get_metadata(name)) | |
1433 | |
1434 def resource_isdir(self, resource_name): | |
1435 return self._isdir(self._fn(self.module_path, resource_name)) | |
1436 | |
1437 def metadata_isdir(self, name): | |
1438 return self.egg_info and self._isdir(self._fn(self.egg_info, name)) | |
1439 | |
1440 def resource_listdir(self, resource_name): | |
1441 return self._listdir(self._fn(self.module_path, resource_name)) | |
1442 | |
1443 def metadata_listdir(self, name): | |
1444 if self.egg_info: | |
1445 return self._listdir(self._fn(self.egg_info, name)) | |
1446 return [] | |
1447 | |
1448 def run_script(self, script_name, namespace): | |
1449 script = 'scripts/' + script_name | |
1450 if not self.has_metadata(script): | |
1451 raise ResolutionError( | |
1452 "Script {script!r} not found in metadata at {self.egg_info!r}" | |
1453 .format(**locals()), | |
1454 ) | |
1455 script_text = self.get_metadata(script).replace('\r\n', '\n') | |
1456 script_text = script_text.replace('\r', '\n') | |
1457 script_filename = self._fn(self.egg_info, script) | |
1458 namespace['__file__'] = script_filename | |
1459 if os.path.exists(script_filename): | |
1460 source = open(script_filename).read() | |
1461 code = compile(source, script_filename, 'exec') | |
1462 exec(code, namespace, namespace) | |
1463 else: | |
1464 from linecache import cache | |
1465 cache[script_filename] = ( | |
1466 len(script_text), 0, script_text.split('\n'), script_filename | |
1467 ) | |
1468 script_code = compile(script_text, script_filename, 'exec') | |
1469 exec(script_code, namespace, namespace) | |
1470 | |
1471 def _has(self, path): | |
1472 raise NotImplementedError( | |
1473 "Can't perform this operation for unregistered loader type" | |
1474 ) | |
1475 | |
1476 def _isdir(self, path): | |
1477 raise NotImplementedError( | |
1478 "Can't perform this operation for unregistered loader type" | |
1479 ) | |
1480 | |
1481 def _listdir(self, path): | |
1482 raise NotImplementedError( | |
1483 "Can't perform this operation for unregistered loader type" | |
1484 ) | |
1485 | |
1486 def _fn(self, base, resource_name): | |
1487 self._validate_resource_path(resource_name) | |
1488 if resource_name: | |
1489 return os.path.join(base, *resource_name.split('/')) | |
1490 return base | |
1491 | |
1492 @staticmethod | |
1493 def _validate_resource_path(path): | |
1494 """ | |
1495 Validate the resource paths according to the docs. | |
1496 https://setuptools.readthedocs.io/en/latest/pkg_resources.html#basic-resource-access | |
1497 | |
1498 >>> warned = getfixture('recwarn') | |
1499 >>> warnings.simplefilter('always') | |
1500 >>> vrp = NullProvider._validate_resource_path | |
1501 >>> vrp('foo/bar.txt') | |
1502 >>> bool(warned) | |
1503 False | |
1504 >>> vrp('../foo/bar.txt') | |
1505 >>> bool(warned) | |
1506 True | |
1507 >>> warned.clear() | |
1508 >>> vrp('/foo/bar.txt') | |
1509 >>> bool(warned) | |
1510 True | |
1511 >>> vrp('foo/../../bar.txt') | |
1512 >>> bool(warned) | |
1513 True | |
1514 >>> warned.clear() | |
1515 >>> vrp('foo/f../bar.txt') | |
1516 >>> bool(warned) | |
1517 False | |
1518 | |
1519 Windows path separators are straight-up disallowed. | |
1520 >>> vrp(r'\\foo/bar.txt') | |
1521 Traceback (most recent call last): | |
1522 ... | |
1523 ValueError: Use of .. or absolute path in a resource path \ | |
1524 is not allowed. | |
1525 | |
1526 >>> vrp(r'C:\\foo/bar.txt') | |
1527 Traceback (most recent call last): | |
1528 ... | |
1529 ValueError: Use of .. or absolute path in a resource path \ | |
1530 is not allowed. | |
1531 | |
1532 Blank values are allowed | |
1533 | |
1534 >>> vrp('') | |
1535 >>> bool(warned) | |
1536 False | |
1537 | |
1538 Non-string values are not. | |
1539 | |
1540 >>> vrp(None) | |
1541 Traceback (most recent call last): | |
1542 ... | |
1543 AttributeError: ... | |
1544 """ | |
1545 invalid = ( | |
1546 os.path.pardir in path.split(posixpath.sep) or | |
1547 posixpath.isabs(path) or | |
1548 ntpath.isabs(path) | |
1549 ) | |
1550 if not invalid: | |
1551 return | |
1552 | |
1553 msg = "Use of .. or absolute path in a resource path is not allowed." | |
1554 | |
1555 # Aggressively disallow Windows absolute paths | |
1556 if ntpath.isabs(path) and not posixpath.isabs(path): | |
1557 raise ValueError(msg) | |
1558 | |
1559 # for compatibility, warn; in future | |
1560 # raise ValueError(msg) | |
1561 warnings.warn( | |
1562 msg[:-1] + " and will raise exceptions in a future release.", | |
1563 DeprecationWarning, | |
1564 stacklevel=4, | |
1565 ) | |
1566 | |
1567 def _get(self, path): | |
1568 if hasattr(self.loader, 'get_data'): | |
1569 return self.loader.get_data(path) | |
1570 raise NotImplementedError( | |
1571 "Can't perform this operation for loaders without 'get_data()'" | |
1572 ) | |
1573 | |
1574 | |
1575 register_loader_type(object, NullProvider) | |
1576 | |
1577 | |
1578 class EggProvider(NullProvider): | |
1579 """Provider based on a virtual filesystem""" | |
1580 | |
1581 def __init__(self, module): | |
1582 NullProvider.__init__(self, module) | |
1583 self._setup_prefix() | |
1584 | |
1585 def _setup_prefix(self): | |
1586 # we assume here that our metadata may be nested inside a "basket" | |
1587 # of multiple eggs; that's why we use module_path instead of .archive | |
1588 path = self.module_path | |
1589 old = None | |
1590 while path != old: | |
1591 if _is_egg_path(path): | |
1592 self.egg_name = os.path.basename(path) | |
1593 self.egg_info = os.path.join(path, 'EGG-INFO') | |
1594 self.egg_root = path | |
1595 break | |
1596 old = path | |
1597 path, base = os.path.split(path) | |
1598 | |
1599 | |
1600 class DefaultProvider(EggProvider): | |
1601 """Provides access to package resources in the filesystem""" | |
1602 | |
1603 def _has(self, path): | |
1604 return os.path.exists(path) | |
1605 | |
1606 def _isdir(self, path): | |
1607 return os.path.isdir(path) | |
1608 | |
1609 def _listdir(self, path): | |
1610 return os.listdir(path) | |
1611 | |
1612 def get_resource_stream(self, manager, resource_name): | |
1613 return open(self._fn(self.module_path, resource_name), 'rb') | |
1614 | |
1615 def _get(self, path): | |
1616 with open(path, 'rb') as stream: | |
1617 return stream.read() | |
1618 | |
1619 @classmethod | |
1620 def _register(cls): | |
1621 loader_names = 'SourceFileLoader', 'SourcelessFileLoader', | |
1622 for name in loader_names: | |
1623 loader_cls = getattr(importlib_machinery, name, type(None)) | |
1624 register_loader_type(loader_cls, cls) | |
1625 | |
1626 | |
1627 DefaultProvider._register() | |
1628 | |
1629 | |
1630 class EmptyProvider(NullProvider): | |
1631 """Provider that returns nothing for all requests""" | |
1632 | |
1633 module_path = None | |
1634 | |
1635 _isdir = _has = lambda self, path: False | |
1636 | |
1637 def _get(self, path): | |
1638 return '' | |
1639 | |
1640 def _listdir(self, path): | |
1641 return [] | |
1642 | |
1643 def __init__(self): | |
1644 pass | |
1645 | |
1646 | |
1647 empty_provider = EmptyProvider() | |
1648 | |
1649 | |
1650 class ZipManifests(dict): | |
1651 """ | |
1652 zip manifest builder | |
1653 """ | |
1654 | |
1655 @classmethod | |
1656 def build(cls, path): | |
1657 """ | |
1658 Build a dictionary similar to the zipimport directory | |
1659 caches, except instead of tuples, store ZipInfo objects. | |
1660 | |
1661 Use a platform-specific path separator (os.sep) for the path keys | |
1662 for compatibility with pypy on Windows. | |
1663 """ | |
1664 with zipfile.ZipFile(path) as zfile: | |
1665 items = ( | |
1666 ( | |
1667 name.replace('/', os.sep), | |
1668 zfile.getinfo(name), | |
1669 ) | |
1670 for name in zfile.namelist() | |
1671 ) | |
1672 return dict(items) | |
1673 | |
1674 load = build | |
1675 | |
1676 | |
1677 class MemoizedZipManifests(ZipManifests): | |
1678 """ | |
1679 Memoized zipfile manifests. | |
1680 """ | |
1681 manifest_mod = collections.namedtuple('manifest_mod', 'manifest mtime') | |
1682 | |
1683 def load(self, path): | |
1684 """ | |
1685 Load a manifest at path or return a suitable manifest already loaded. | |
1686 """ | |
1687 path = os.path.normpath(path) | |
1688 mtime = os.stat(path).st_mtime | |
1689 | |
1690 if path not in self or self[path].mtime != mtime: | |
1691 manifest = self.build(path) | |
1692 self[path] = self.manifest_mod(manifest, mtime) | |
1693 | |
1694 return self[path].manifest | |
1695 | |
1696 | |
1697 class ZipProvider(EggProvider): | |
1698 """Resource support for zips and eggs""" | |
1699 | |
1700 eagers = None | |
1701 _zip_manifests = MemoizedZipManifests() | |
1702 | |
1703 def __init__(self, module): | |
1704 EggProvider.__init__(self, module) | |
1705 self.zip_pre = self.loader.archive + os.sep | |
1706 | |
1707 def _zipinfo_name(self, fspath): | |
1708 # Convert a virtual filename (full path to file) into a zipfile subpath | |
1709 # usable with the zipimport directory cache for our target archive | |
1710 fspath = fspath.rstrip(os.sep) | |
1711 if fspath == self.loader.archive: | |
1712 return '' | |
1713 if fspath.startswith(self.zip_pre): | |
1714 return fspath[len(self.zip_pre):] | |
1715 raise AssertionError( | |
1716 "%s is not a subpath of %s" % (fspath, self.zip_pre) | |
1717 ) | |
1718 | |
1719 def _parts(self, zip_path): | |
1720 # Convert a zipfile subpath into an egg-relative path part list. | |
1721 # pseudo-fs path | |
1722 fspath = self.zip_pre + zip_path | |
1723 if fspath.startswith(self.egg_root + os.sep): | |
1724 return fspath[len(self.egg_root) + 1:].split(os.sep) | |
1725 raise AssertionError( | |
1726 "%s is not a subpath of %s" % (fspath, self.egg_root) | |
1727 ) | |
1728 | |
1729 @property | |
1730 def zipinfo(self): | |
1731 return self._zip_manifests.load(self.loader.archive) | |
1732 | |
1733 def get_resource_filename(self, manager, resource_name): | |
1734 if not self.egg_name: | |
1735 raise NotImplementedError( | |
1736 "resource_filename() only supported for .egg, not .zip" | |
1737 ) | |
1738 # no need to lock for extraction, since we use temp names | |
1739 zip_path = self._resource_to_zip(resource_name) | |
1740 eagers = self._get_eager_resources() | |
1741 if '/'.join(self._parts(zip_path)) in eagers: | |
1742 for name in eagers: | |
1743 self._extract_resource(manager, self._eager_to_zip(name)) | |
1744 return self._extract_resource(manager, zip_path) | |
1745 | |
1746 @staticmethod | |
1747 def _get_date_and_size(zip_stat): | |
1748 size = zip_stat.file_size | |
1749 # ymdhms+wday, yday, dst | |
1750 date_time = zip_stat.date_time + (0, 0, -1) | |
1751 # 1980 offset already done | |
1752 timestamp = time.mktime(date_time) | |
1753 return timestamp, size | |
1754 | |
1755 def _extract_resource(self, manager, zip_path): | |
1756 | |
1757 if zip_path in self._index(): | |
1758 for name in self._index()[zip_path]: | |
1759 last = self._extract_resource( | |
1760 manager, os.path.join(zip_path, name) | |
1761 ) | |
1762 # return the extracted directory name | |
1763 return os.path.dirname(last) | |
1764 | |
1765 timestamp, size = self._get_date_and_size(self.zipinfo[zip_path]) | |
1766 | |
1767 if not WRITE_SUPPORT: | |
1768 raise IOError('"os.rename" and "os.unlink" are not supported ' | |
1769 'on this platform') | |
1770 try: | |
1771 | |
1772 real_path = manager.get_cache_path( | |
1773 self.egg_name, self._parts(zip_path) | |
1774 ) | |
1775 | |
1776 if self._is_current(real_path, zip_path): | |
1777 return real_path | |
1778 | |
1779 outf, tmpnam = _mkstemp( | |
1780 ".$extract", | |
1781 dir=os.path.dirname(real_path), | |
1782 ) | |
1783 os.write(outf, self.loader.get_data(zip_path)) | |
1784 os.close(outf) | |
1785 utime(tmpnam, (timestamp, timestamp)) | |
1786 manager.postprocess(tmpnam, real_path) | |
1787 | |
1788 try: | |
1789 rename(tmpnam, real_path) | |
1790 | |
1791 except os.error: | |
1792 if os.path.isfile(real_path): | |
1793 if self._is_current(real_path, zip_path): | |
1794 # the file became current since it was checked above, | |
1795 # so proceed. | |
1796 return real_path | |
1797 # Windows, del old file and retry | |
1798 elif os.name == 'nt': | |
1799 unlink(real_path) | |
1800 rename(tmpnam, real_path) | |
1801 return real_path | |
1802 raise | |
1803 | |
1804 except os.error: | |
1805 # report a user-friendly error | |
1806 manager.extraction_error() | |
1807 | |
1808 return real_path | |
1809 | |
1810 def _is_current(self, file_path, zip_path): | |
1811 """ | |
1812 Return True if the file_path is current for this zip_path | |
1813 """ | |
1814 timestamp, size = self._get_date_and_size(self.zipinfo[zip_path]) | |
1815 if not os.path.isfile(file_path): | |
1816 return False | |
1817 stat = os.stat(file_path) | |
1818 if stat.st_size != size or stat.st_mtime != timestamp: | |
1819 return False | |
1820 # check that the contents match | |
1821 zip_contents = self.loader.get_data(zip_path) | |
1822 with open(file_path, 'rb') as f: | |
1823 file_contents = f.read() | |
1824 return zip_contents == file_contents | |
1825 | |
1826 def _get_eager_resources(self): | |
1827 if self.eagers is None: | |
1828 eagers = [] | |
1829 for name in ('native_libs.txt', 'eager_resources.txt'): | |
1830 if self.has_metadata(name): | |
1831 eagers.extend(self.get_metadata_lines(name)) | |
1832 self.eagers = eagers | |
1833 return self.eagers | |
1834 | |
1835 def _index(self): | |
1836 try: | |
1837 return self._dirindex | |
1838 except AttributeError: | |
1839 ind = {} | |
1840 for path in self.zipinfo: | |
1841 parts = path.split(os.sep) | |
1842 while parts: | |
1843 parent = os.sep.join(parts[:-1]) | |
1844 if parent in ind: | |
1845 ind[parent].append(parts[-1]) | |
1846 break | |
1847 else: | |
1848 ind[parent] = [parts.pop()] | |
1849 self._dirindex = ind | |
1850 return ind | |
1851 | |
1852 def _has(self, fspath): | |
1853 zip_path = self._zipinfo_name(fspath) | |
1854 return zip_path in self.zipinfo or zip_path in self._index() | |
1855 | |
1856 def _isdir(self, fspath): | |
1857 return self._zipinfo_name(fspath) in self._index() | |
1858 | |
1859 def _listdir(self, fspath): | |
1860 return list(self._index().get(self._zipinfo_name(fspath), ())) | |
1861 | |
1862 def _eager_to_zip(self, resource_name): | |
1863 return self._zipinfo_name(self._fn(self.egg_root, resource_name)) | |
1864 | |
1865 def _resource_to_zip(self, resource_name): | |
1866 return self._zipinfo_name(self._fn(self.module_path, resource_name)) | |
1867 | |
1868 | |
1869 register_loader_type(zipimport.zipimporter, ZipProvider) | |
1870 | |
1871 | |
1872 class FileMetadata(EmptyProvider): | |
1873 """Metadata handler for standalone PKG-INFO files | |
1874 | |
1875 Usage:: | |
1876 | |
1877 metadata = FileMetadata("/path/to/PKG-INFO") | |
1878 | |
1879 This provider rejects all data and metadata requests except for PKG-INFO, | |
1880 which is treated as existing, and will be the contents of the file at | |
1881 the provided location. | |
1882 """ | |
1883 | |
1884 def __init__(self, path): | |
1885 self.path = path | |
1886 | |
1887 def _get_metadata_path(self, name): | |
1888 return self.path | |
1889 | |
1890 def has_metadata(self, name): | |
1891 return name == 'PKG-INFO' and os.path.isfile(self.path) | |
1892 | |
1893 def get_metadata(self, name): | |
1894 if name != 'PKG-INFO': | |
1895 raise KeyError("No metadata except PKG-INFO is available") | |
1896 | |
1897 with io.open(self.path, encoding='utf-8', errors="replace") as f: | |
1898 metadata = f.read() | |
1899 self._warn_on_replacement(metadata) | |
1900 return metadata | |
1901 | |
1902 def _warn_on_replacement(self, metadata): | |
1903 # Python 2.7 compat for: replacement_char = '�' | |
1904 replacement_char = b'\xef\xbf\xbd'.decode('utf-8') | |
1905 if replacement_char in metadata: | |
1906 tmpl = "{self.path} could not be properly decoded in UTF-8" | |
1907 msg = tmpl.format(**locals()) | |
1908 warnings.warn(msg) | |
1909 | |
1910 def get_metadata_lines(self, name): | |
1911 return yield_lines(self.get_metadata(name)) | |
1912 | |
1913 | |
1914 class PathMetadata(DefaultProvider): | |
1915 """Metadata provider for egg directories | |
1916 | |
1917 Usage:: | |
1918 | |
1919 # Development eggs: | |
1920 | |
1921 egg_info = "/path/to/PackageName.egg-info" | |
1922 base_dir = os.path.dirname(egg_info) | |
1923 metadata = PathMetadata(base_dir, egg_info) | |
1924 dist_name = os.path.splitext(os.path.basename(egg_info))[0] | |
1925 dist = Distribution(basedir, project_name=dist_name, metadata=metadata) | |
1926 | |
1927 # Unpacked egg directories: | |
1928 | |
1929 egg_path = "/path/to/PackageName-ver-pyver-etc.egg" | |
1930 metadata = PathMetadata(egg_path, os.path.join(egg_path,'EGG-INFO')) | |
1931 dist = Distribution.from_filename(egg_path, metadata=metadata) | |
1932 """ | |
1933 | |
1934 def __init__(self, path, egg_info): | |
1935 self.module_path = path | |
1936 self.egg_info = egg_info | |
1937 | |
1938 | |
1939 class EggMetadata(ZipProvider): | |
1940 """Metadata provider for .egg files""" | |
1941 | |
1942 def __init__(self, importer): | |
1943 """Create a metadata provider from a zipimporter""" | |
1944 | |
1945 self.zip_pre = importer.archive + os.sep | |
1946 self.loader = importer | |
1947 if importer.prefix: | |
1948 self.module_path = os.path.join(importer.archive, importer.prefix) | |
1949 else: | |
1950 self.module_path = importer.archive | |
1951 self._setup_prefix() | |
1952 | |
1953 | |
1954 _declare_state('dict', _distribution_finders={}) | |
1955 | |
1956 | |
1957 def register_finder(importer_type, distribution_finder): | |
1958 """Register `distribution_finder` to find distributions in sys.path items | |
1959 | |
1960 `importer_type` is the type or class of a PEP 302 "Importer" (sys.path item | |
1961 handler), and `distribution_finder` is a callable that, passed a path | |
1962 item and the importer instance, yields ``Distribution`` instances found on | |
1963 that path item. See ``pkg_resources.find_on_path`` for an example.""" | |
1964 _distribution_finders[importer_type] = distribution_finder | |
1965 | |
1966 | |
1967 def find_distributions(path_item, only=False): | |
1968 """Yield distributions accessible via `path_item`""" | |
1969 importer = get_importer(path_item) | |
1970 finder = _find_adapter(_distribution_finders, importer) | |
1971 return finder(importer, path_item, only) | |
1972 | |
1973 | |
1974 def find_eggs_in_zip(importer, path_item, only=False): | |
1975 """ | |
1976 Find eggs in zip files; possibly multiple nested eggs. | |
1977 """ | |
1978 if importer.archive.endswith('.whl'): | |
1979 # wheels are not supported with this finder | |
1980 # they don't have PKG-INFO metadata, and won't ever contain eggs | |
1981 return | |
1982 metadata = EggMetadata(importer) | |
1983 if metadata.has_metadata('PKG-INFO'): | |
1984 yield Distribution.from_filename(path_item, metadata=metadata) | |
1985 if only: | |
1986 # don't yield nested distros | |
1987 return | |
1988 for subitem in metadata.resource_listdir(''): | |
1989 if _is_egg_path(subitem): | |
1990 subpath = os.path.join(path_item, subitem) | |
1991 dists = find_eggs_in_zip(zipimport.zipimporter(subpath), subpath) | |
1992 for dist in dists: | |
1993 yield dist | |
1994 elif subitem.lower().endswith('.dist-info'): | |
1995 subpath = os.path.join(path_item, subitem) | |
1996 submeta = EggMetadata(zipimport.zipimporter(subpath)) | |
1997 submeta.egg_info = subpath | |
1998 yield Distribution.from_location(path_item, subitem, submeta) | |
1999 | |
2000 | |
2001 register_finder(zipimport.zipimporter, find_eggs_in_zip) | |
2002 | |
2003 | |
2004 def find_nothing(importer, path_item, only=False): | |
2005 return () | |
2006 | |
2007 | |
2008 register_finder(object, find_nothing) | |
2009 | |
2010 | |
2011 def _by_version_descending(names): | |
2012 """ | |
2013 Given a list of filenames, return them in descending order | |
2014 by version number. | |
2015 | |
2016 >>> names = 'bar', 'foo', 'Python-2.7.10.egg', 'Python-2.7.2.egg' | |
2017 >>> _by_version_descending(names) | |
2018 ['Python-2.7.10.egg', 'Python-2.7.2.egg', 'foo', 'bar'] | |
2019 >>> names = 'Setuptools-1.2.3b1.egg', 'Setuptools-1.2.3.egg' | |
2020 >>> _by_version_descending(names) | |
2021 ['Setuptools-1.2.3.egg', 'Setuptools-1.2.3b1.egg'] | |
2022 >>> names = 'Setuptools-1.2.3b1.egg', 'Setuptools-1.2.3.post1.egg' | |
2023 >>> _by_version_descending(names) | |
2024 ['Setuptools-1.2.3.post1.egg', 'Setuptools-1.2.3b1.egg'] | |
2025 """ | |
2026 def _by_version(name): | |
2027 """ | |
2028 Parse each component of the filename | |
2029 """ | |
2030 name, ext = os.path.splitext(name) | |
2031 parts = itertools.chain(name.split('-'), [ext]) | |
2032 return [packaging.version.parse(part) for part in parts] | |
2033 | |
2034 return sorted(names, key=_by_version, reverse=True) | |
2035 | |
2036 | |
2037 def find_on_path(importer, path_item, only=False): | |
2038 """Yield distributions accessible on a sys.path directory""" | |
2039 path_item = _normalize_cached(path_item) | |
2040 | |
2041 if _is_unpacked_egg(path_item): | |
2042 yield Distribution.from_filename( | |
2043 path_item, metadata=PathMetadata( | |
2044 path_item, os.path.join(path_item, 'EGG-INFO') | |
2045 ) | |
2046 ) | |
2047 return | |
2048 | |
2049 entries = safe_listdir(path_item) | |
2050 | |
2051 # for performance, before sorting by version, | |
2052 # screen entries for only those that will yield | |
2053 # distributions | |
2054 filtered = ( | |
2055 entry | |
2056 for entry in entries | |
2057 if dist_factory(path_item, entry, only) | |
2058 ) | |
2059 | |
2060 # scan for .egg and .egg-info in directory | |
2061 path_item_entries = _by_version_descending(filtered) | |
2062 for entry in path_item_entries: | |
2063 fullpath = os.path.join(path_item, entry) | |
2064 factory = dist_factory(path_item, entry, only) | |
2065 for dist in factory(fullpath): | |
2066 yield dist | |
2067 | |
2068 | |
2069 def dist_factory(path_item, entry, only): | |
2070 """ | |
2071 Return a dist_factory for a path_item and entry | |
2072 """ | |
2073 lower = entry.lower() | |
2074 is_meta = any(map(lower.endswith, ('.egg-info', '.dist-info'))) | |
2075 return ( | |
2076 distributions_from_metadata | |
2077 if is_meta else | |
2078 find_distributions | |
2079 if not only and _is_egg_path(entry) else | |
2080 resolve_egg_link | |
2081 if not only and lower.endswith('.egg-link') else | |
2082 NoDists() | |
2083 ) | |
2084 | |
2085 | |
2086 class NoDists: | |
2087 """ | |
2088 >>> bool(NoDists()) | |
2089 False | |
2090 | |
2091 >>> list(NoDists()('anything')) | |
2092 [] | |
2093 """ | |
2094 def __bool__(self): | |
2095 return False | |
2096 if six.PY2: | |
2097 __nonzero__ = __bool__ | |
2098 | |
2099 def __call__(self, fullpath): | |
2100 return iter(()) | |
2101 | |
2102 | |
2103 def safe_listdir(path): | |
2104 """ | |
2105 Attempt to list contents of path, but suppress some exceptions. | |
2106 """ | |
2107 try: | |
2108 return os.listdir(path) | |
2109 except (PermissionError, NotADirectoryError): | |
2110 pass | |
2111 except OSError as e: | |
2112 # Ignore the directory if does not exist, not a directory or | |
2113 # permission denied | |
2114 ignorable = ( | |
2115 e.errno in (errno.ENOTDIR, errno.EACCES, errno.ENOENT) | |
2116 # Python 2 on Windows needs to be handled this way :( | |
2117 or getattr(e, "winerror", None) == 267 | |
2118 ) | |
2119 if not ignorable: | |
2120 raise | |
2121 return () | |
2122 | |
2123 | |
2124 def distributions_from_metadata(path): | |
2125 root = os.path.dirname(path) | |
2126 if os.path.isdir(path): | |
2127 if len(os.listdir(path)) == 0: | |
2128 # empty metadata dir; skip | |
2129 return | |
2130 metadata = PathMetadata(root, path) | |
2131 else: | |
2132 metadata = FileMetadata(path) | |
2133 entry = os.path.basename(path) | |
2134 yield Distribution.from_location( | |
2135 root, entry, metadata, precedence=DEVELOP_DIST, | |
2136 ) | |
2137 | |
2138 | |
2139 def non_empty_lines(path): | |
2140 """ | |
2141 Yield non-empty lines from file at path | |
2142 """ | |
2143 with open(path) as f: | |
2144 for line in f: | |
2145 line = line.strip() | |
2146 if line: | |
2147 yield line | |
2148 | |
2149 | |
2150 def resolve_egg_link(path): | |
2151 """ | |
2152 Given a path to an .egg-link, resolve distributions | |
2153 present in the referenced path. | |
2154 """ | |
2155 referenced_paths = non_empty_lines(path) | |
2156 resolved_paths = ( | |
2157 os.path.join(os.path.dirname(path), ref) | |
2158 for ref in referenced_paths | |
2159 ) | |
2160 dist_groups = map(find_distributions, resolved_paths) | |
2161 return next(dist_groups, ()) | |
2162 | |
2163 | |
2164 register_finder(pkgutil.ImpImporter, find_on_path) | |
2165 | |
2166 if hasattr(importlib_machinery, 'FileFinder'): | |
2167 register_finder(importlib_machinery.FileFinder, find_on_path) | |
2168 | |
2169 _declare_state('dict', _namespace_handlers={}) | |
2170 _declare_state('dict', _namespace_packages={}) | |
2171 | |
2172 | |
2173 def register_namespace_handler(importer_type, namespace_handler): | |
2174 """Register `namespace_handler` to declare namespace packages | |
2175 | |
2176 `importer_type` is the type or class of a PEP 302 "Importer" (sys.path item | |
2177 handler), and `namespace_handler` is a callable like this:: | |
2178 | |
2179 def namespace_handler(importer, path_entry, moduleName, module): | |
2180 # return a path_entry to use for child packages | |
2181 | |
2182 Namespace handlers are only called if the importer object has already | |
2183 agreed that it can handle the relevant path item, and they should only | |
2184 return a subpath if the module __path__ does not already contain an | |
2185 equivalent subpath. For an example namespace handler, see | |
2186 ``pkg_resources.file_ns_handler``. | |
2187 """ | |
2188 _namespace_handlers[importer_type] = namespace_handler | |
2189 | |
2190 | |
2191 def _handle_ns(packageName, path_item): | |
2192 """Ensure that named package includes a subpath of path_item (if needed)""" | |
2193 | |
2194 importer = get_importer(path_item) | |
2195 if importer is None: | |
2196 return None | |
2197 | |
2198 # capture warnings due to #1111 | |
2199 with warnings.catch_warnings(): | |
2200 warnings.simplefilter("ignore") | |
2201 loader = importer.find_module(packageName) | |
2202 | |
2203 if loader is None: | |
2204 return None | |
2205 module = sys.modules.get(packageName) | |
2206 if module is None: | |
2207 module = sys.modules[packageName] = types.ModuleType(packageName) | |
2208 module.__path__ = [] | |
2209 _set_parent_ns(packageName) | |
2210 elif not hasattr(module, '__path__'): | |
2211 raise TypeError("Not a package:", packageName) | |
2212 handler = _find_adapter(_namespace_handlers, importer) | |
2213 subpath = handler(importer, path_item, packageName, module) | |
2214 if subpath is not None: | |
2215 path = module.__path__ | |
2216 path.append(subpath) | |
2217 loader.load_module(packageName) | |
2218 _rebuild_mod_path(path, packageName, module) | |
2219 return subpath | |
2220 | |
2221 | |
2222 def _rebuild_mod_path(orig_path, package_name, module): | |
2223 """ | |
2224 Rebuild module.__path__ ensuring that all entries are ordered | |
2225 corresponding to their sys.path order | |
2226 """ | |
2227 sys_path = [_normalize_cached(p) for p in sys.path] | |
2228 | |
2229 def safe_sys_path_index(entry): | |
2230 """ | |
2231 Workaround for #520 and #513. | |
2232 """ | |
2233 try: | |
2234 return sys_path.index(entry) | |
2235 except ValueError: | |
2236 return float('inf') | |
2237 | |
2238 def position_in_sys_path(path): | |
2239 """ | |
2240 Return the ordinal of the path based on its position in sys.path | |
2241 """ | |
2242 path_parts = path.split(os.sep) | |
2243 module_parts = package_name.count('.') + 1 | |
2244 parts = path_parts[:-module_parts] | |
2245 return safe_sys_path_index(_normalize_cached(os.sep.join(parts))) | |
2246 | |
2247 new_path = sorted(orig_path, key=position_in_sys_path) | |
2248 new_path = [_normalize_cached(p) for p in new_path] | |
2249 | |
2250 if isinstance(module.__path__, list): | |
2251 module.__path__[:] = new_path | |
2252 else: | |
2253 module.__path__ = new_path | |
2254 | |
2255 | |
2256 def declare_namespace(packageName): | |
2257 """Declare that package 'packageName' is a namespace package""" | |
2258 | |
2259 _imp.acquire_lock() | |
2260 try: | |
2261 if packageName in _namespace_packages: | |
2262 return | |
2263 | |
2264 path = sys.path | |
2265 parent, _, _ = packageName.rpartition('.') | |
2266 | |
2267 if parent: | |
2268 declare_namespace(parent) | |
2269 if parent not in _namespace_packages: | |
2270 __import__(parent) | |
2271 try: | |
2272 path = sys.modules[parent].__path__ | |
2273 except AttributeError: | |
2274 raise TypeError("Not a package:", parent) | |
2275 | |
2276 # Track what packages are namespaces, so when new path items are added, | |
2277 # they can be updated | |
2278 _namespace_packages.setdefault(parent or None, []).append(packageName) | |
2279 _namespace_packages.setdefault(packageName, []) | |
2280 | |
2281 for path_item in path: | |
2282 # Ensure all the parent's path items are reflected in the child, | |
2283 # if they apply | |
2284 _handle_ns(packageName, path_item) | |
2285 | |
2286 finally: | |
2287 _imp.release_lock() | |
2288 | |
2289 | |
2290 def fixup_namespace_packages(path_item, parent=None): | |
2291 """Ensure that previously-declared namespace packages include path_item""" | |
2292 _imp.acquire_lock() | |
2293 try: | |
2294 for package in _namespace_packages.get(parent, ()): | |
2295 subpath = _handle_ns(package, path_item) | |
2296 if subpath: | |
2297 fixup_namespace_packages(subpath, package) | |
2298 finally: | |
2299 _imp.release_lock() | |
2300 | |
2301 | |
2302 def file_ns_handler(importer, path_item, packageName, module): | |
2303 """Compute an ns-package subpath for a filesystem or zipfile importer""" | |
2304 | |
2305 subpath = os.path.join(path_item, packageName.split('.')[-1]) | |
2306 normalized = _normalize_cached(subpath) | |
2307 for item in module.__path__: | |
2308 if _normalize_cached(item) == normalized: | |
2309 break | |
2310 else: | |
2311 # Only return the path if it's not already there | |
2312 return subpath | |
2313 | |
2314 | |
2315 register_namespace_handler(pkgutil.ImpImporter, file_ns_handler) | |
2316 register_namespace_handler(zipimport.zipimporter, file_ns_handler) | |
2317 | |
2318 if hasattr(importlib_machinery, 'FileFinder'): | |
2319 register_namespace_handler(importlib_machinery.FileFinder, file_ns_handler) | |
2320 | |
2321 | |
2322 def null_ns_handler(importer, path_item, packageName, module): | |
2323 return None | |
2324 | |
2325 | |
2326 register_namespace_handler(object, null_ns_handler) | |
2327 | |
2328 | |
2329 def normalize_path(filename): | |
2330 """Normalize a file/dir name for comparison purposes""" | |
2331 return os.path.normcase(os.path.realpath(os.path.normpath(_cygwin_patch(filename)))) | |
2332 | |
2333 | |
2334 def _cygwin_patch(filename): # pragma: nocover | |
2335 """ | |
2336 Contrary to POSIX 2008, on Cygwin, getcwd (3) contains | |
2337 symlink components. Using | |
2338 os.path.abspath() works around this limitation. A fix in os.getcwd() | |
2339 would probably better, in Cygwin even more so, except | |
2340 that this seems to be by design... | |
2341 """ | |
2342 return os.path.abspath(filename) if sys.platform == 'cygwin' else filename | |
2343 | |
2344 | |
2345 def _normalize_cached(filename, _cache={}): | |
2346 try: | |
2347 return _cache[filename] | |
2348 except KeyError: | |
2349 _cache[filename] = result = normalize_path(filename) | |
2350 return result | |
2351 | |
2352 | |
2353 def _is_egg_path(path): | |
2354 """ | |
2355 Determine if given path appears to be an egg. | |
2356 """ | |
2357 return path.lower().endswith('.egg') | |
2358 | |
2359 | |
2360 def _is_unpacked_egg(path): | |
2361 """ | |
2362 Determine if given path appears to be an unpacked egg. | |
2363 """ | |
2364 return ( | |
2365 _is_egg_path(path) and | |
2366 os.path.isfile(os.path.join(path, 'EGG-INFO', 'PKG-INFO')) | |
2367 ) | |
2368 | |
2369 | |
2370 def _set_parent_ns(packageName): | |
2371 parts = packageName.split('.') | |
2372 name = parts.pop() | |
2373 if parts: | |
2374 parent = '.'.join(parts) | |
2375 setattr(sys.modules[parent], name, sys.modules[packageName]) | |
2376 | |
2377 | |
2378 def yield_lines(strs): | |
2379 """Yield non-empty/non-comment lines of a string or sequence""" | |
2380 if isinstance(strs, six.string_types): | |
2381 for s in strs.splitlines(): | |
2382 s = s.strip() | |
2383 # skip blank lines/comments | |
2384 if s and not s.startswith('#'): | |
2385 yield s | |
2386 else: | |
2387 for ss in strs: | |
2388 for s in yield_lines(ss): | |
2389 yield s | |
2390 | |
2391 | |
2392 MODULE = re.compile(r"\w+(\.\w+)*$").match | |
2393 EGG_NAME = re.compile( | |
2394 r""" | |
2395 (?P<name>[^-]+) ( | |
2396 -(?P<ver>[^-]+) ( | |
2397 -py(?P<pyver>[^-]+) ( | |
2398 -(?P<plat>.+) | |
2399 )? | |
2400 )? | |
2401 )? | |
2402 """, | |
2403 re.VERBOSE | re.IGNORECASE, | |
2404 ).match | |
2405 | |
2406 | |
2407 class EntryPoint: | |
2408 """Object representing an advertised importable object""" | |
2409 | |
2410 def __init__(self, name, module_name, attrs=(), extras=(), dist=None): | |
2411 if not MODULE(module_name): | |
2412 raise ValueError("Invalid module name", module_name) | |
2413 self.name = name | |
2414 self.module_name = module_name | |
2415 self.attrs = tuple(attrs) | |
2416 self.extras = tuple(extras) | |
2417 self.dist = dist | |
2418 | |
2419 def __str__(self): | |
2420 s = "%s = %s" % (self.name, self.module_name) | |
2421 if self.attrs: | |
2422 s += ':' + '.'.join(self.attrs) | |
2423 if self.extras: | |
2424 s += ' [%s]' % ','.join(self.extras) | |
2425 return s | |
2426 | |
2427 def __repr__(self): | |
2428 return "EntryPoint.parse(%r)" % str(self) | |
2429 | |
2430 def load(self, require=True, *args, **kwargs): | |
2431 """ | |
2432 Require packages for this EntryPoint, then resolve it. | |
2433 """ | |
2434 if not require or args or kwargs: | |
2435 warnings.warn( | |
2436 "Parameters to load are deprecated. Call .resolve and " | |
2437 ".require separately.", | |
2438 PkgResourcesDeprecationWarning, | |
2439 stacklevel=2, | |
2440 ) | |
2441 if require: | |
2442 self.require(*args, **kwargs) | |
2443 return self.resolve() | |
2444 | |
2445 def resolve(self): | |
2446 """ | |
2447 Resolve the entry point from its module and attrs. | |
2448 """ | |
2449 module = __import__(self.module_name, fromlist=['__name__'], level=0) | |
2450 try: | |
2451 return functools.reduce(getattr, self.attrs, module) | |
2452 except AttributeError as exc: | |
2453 raise ImportError(str(exc)) | |
2454 | |
2455 def require(self, env=None, installer=None): | |
2456 if self.extras and not self.dist: | |
2457 raise UnknownExtra("Can't require() without a distribution", self) | |
2458 | |
2459 # Get the requirements for this entry point with all its extras and | |
2460 # then resolve them. We have to pass `extras` along when resolving so | |
2461 # that the working set knows what extras we want. Otherwise, for | |
2462 # dist-info distributions, the working set will assume that the | |
2463 # requirements for that extra are purely optional and skip over them. | |
2464 reqs = self.dist.requires(self.extras) | |
2465 items = working_set.resolve(reqs, env, installer, extras=self.extras) | |
2466 list(map(working_set.add, items)) | |
2467 | |
2468 pattern = re.compile( | |
2469 r'\s*' | |
2470 r'(?P<name>.+?)\s*' | |
2471 r'=\s*' | |
2472 r'(?P<module>[\w.]+)\s*' | |
2473 r'(:\s*(?P<attr>[\w.]+))?\s*' | |
2474 r'(?P<extras>\[.*\])?\s*$' | |
2475 ) | |
2476 | |
2477 @classmethod | |
2478 def parse(cls, src, dist=None): | |
2479 """Parse a single entry point from string `src` | |
2480 | |
2481 Entry point syntax follows the form:: | |
2482 | |
2483 name = some.module:some.attr [extra1, extra2] | |
2484 | |
2485 The entry name and module name are required, but the ``:attrs`` and | |
2486 ``[extras]`` parts are optional | |
2487 """ | |
2488 m = cls.pattern.match(src) | |
2489 if not m: | |
2490 msg = "EntryPoint must be in 'name=module:attrs [extras]' format" | |
2491 raise ValueError(msg, src) | |
2492 res = m.groupdict() | |
2493 extras = cls._parse_extras(res['extras']) | |
2494 attrs = res['attr'].split('.') if res['attr'] else () | |
2495 return cls(res['name'], res['module'], attrs, extras, dist) | |
2496 | |
2497 @classmethod | |
2498 def _parse_extras(cls, extras_spec): | |
2499 if not extras_spec: | |
2500 return () | |
2501 req = Requirement.parse('x' + extras_spec) | |
2502 if req.specs: | |
2503 raise ValueError() | |
2504 return req.extras | |
2505 | |
2506 @classmethod | |
2507 def parse_group(cls, group, lines, dist=None): | |
2508 """Parse an entry point group""" | |
2509 if not MODULE(group): | |
2510 raise ValueError("Invalid group name", group) | |
2511 this = {} | |
2512 for line in yield_lines(lines): | |
2513 ep = cls.parse(line, dist) | |
2514 if ep.name in this: | |
2515 raise ValueError("Duplicate entry point", group, ep.name) | |
2516 this[ep.name] = ep | |
2517 return this | |
2518 | |
2519 @classmethod | |
2520 def parse_map(cls, data, dist=None): | |
2521 """Parse a map of entry point groups""" | |
2522 if isinstance(data, dict): | |
2523 data = data.items() | |
2524 else: | |
2525 data = split_sections(data) | |
2526 maps = {} | |
2527 for group, lines in data: | |
2528 if group is None: | |
2529 if not lines: | |
2530 continue | |
2531 raise ValueError("Entry points must be listed in groups") | |
2532 group = group.strip() | |
2533 if group in maps: | |
2534 raise ValueError("Duplicate group name", group) | |
2535 maps[group] = cls.parse_group(group, lines, dist) | |
2536 return maps | |
2537 | |
2538 | |
2539 def _remove_md5_fragment(location): | |
2540 if not location: | |
2541 return '' | |
2542 parsed = urllib.parse.urlparse(location) | |
2543 if parsed[-1].startswith('md5='): | |
2544 return urllib.parse.urlunparse(parsed[:-1] + ('',)) | |
2545 return location | |
2546 | |
2547 | |
2548 def _version_from_file(lines): | |
2549 """ | |
2550 Given an iterable of lines from a Metadata file, return | |
2551 the value of the Version field, if present, or None otherwise. | |
2552 """ | |
2553 def is_version_line(line): | |
2554 return line.lower().startswith('version:') | |
2555 version_lines = filter(is_version_line, lines) | |
2556 line = next(iter(version_lines), '') | |
2557 _, _, value = line.partition(':') | |
2558 return safe_version(value.strip()) or None | |
2559 | |
2560 | |
2561 class Distribution: | |
2562 """Wrap an actual or potential sys.path entry w/metadata""" | |
2563 PKG_INFO = 'PKG-INFO' | |
2564 | |
2565 def __init__( | |
2566 self, location=None, metadata=None, project_name=None, | |
2567 version=None, py_version=PY_MAJOR, platform=None, | |
2568 precedence=EGG_DIST): | |
2569 self.project_name = safe_name(project_name or 'Unknown') | |
2570 if version is not None: | |
2571 self._version = safe_version(version) | |
2572 self.py_version = py_version | |
2573 self.platform = platform | |
2574 self.location = location | |
2575 self.precedence = precedence | |
2576 self._provider = metadata or empty_provider | |
2577 | |
2578 @classmethod | |
2579 def from_location(cls, location, basename, metadata=None, **kw): | |
2580 project_name, version, py_version, platform = [None] * 4 | |
2581 basename, ext = os.path.splitext(basename) | |
2582 if ext.lower() in _distributionImpl: | |
2583 cls = _distributionImpl[ext.lower()] | |
2584 | |
2585 match = EGG_NAME(basename) | |
2586 if match: | |
2587 project_name, version, py_version, platform = match.group( | |
2588 'name', 'ver', 'pyver', 'plat' | |
2589 ) | |
2590 return cls( | |
2591 location, metadata, project_name=project_name, version=version, | |
2592 py_version=py_version, platform=platform, **kw | |
2593 )._reload_version() | |
2594 | |
2595 def _reload_version(self): | |
2596 return self | |
2597 | |
2598 @property | |
2599 def hashcmp(self): | |
2600 return ( | |
2601 self.parsed_version, | |
2602 self.precedence, | |
2603 self.key, | |
2604 _remove_md5_fragment(self.location), | |
2605 self.py_version or '', | |
2606 self.platform or '', | |
2607 ) | |
2608 | |
2609 def __hash__(self): | |
2610 return hash(self.hashcmp) | |
2611 | |
2612 def __lt__(self, other): | |
2613 return self.hashcmp < other.hashcmp | |
2614 | |
2615 def __le__(self, other): | |
2616 return self.hashcmp <= other.hashcmp | |
2617 | |
2618 def __gt__(self, other): | |
2619 return self.hashcmp > other.hashcmp | |
2620 | |
2621 def __ge__(self, other): | |
2622 return self.hashcmp >= other.hashcmp | |
2623 | |
2624 def __eq__(self, other): | |
2625 if not isinstance(other, self.__class__): | |
2626 # It's not a Distribution, so they are not equal | |
2627 return False | |
2628 return self.hashcmp == other.hashcmp | |
2629 | |
2630 def __ne__(self, other): | |
2631 return not self == other | |
2632 | |
2633 # These properties have to be lazy so that we don't have to load any | |
2634 # metadata until/unless it's actually needed. (i.e., some distributions | |
2635 # may not know their name or version without loading PKG-INFO) | |
2636 | |
2637 @property | |
2638 def key(self): | |
2639 try: | |
2640 return self._key | |
2641 except AttributeError: | |
2642 self._key = key = self.project_name.lower() | |
2643 return key | |
2644 | |
2645 @property | |
2646 def parsed_version(self): | |
2647 if not hasattr(self, "_parsed_version"): | |
2648 self._parsed_version = parse_version(self.version) | |
2649 | |
2650 return self._parsed_version | |
2651 | |
2652 def _warn_legacy_version(self): | |
2653 LV = packaging.version.LegacyVersion | |
2654 is_legacy = isinstance(self._parsed_version, LV) | |
2655 if not is_legacy: | |
2656 return | |
2657 | |
2658 # While an empty version is technically a legacy version and | |
2659 # is not a valid PEP 440 version, it's also unlikely to | |
2660 # actually come from someone and instead it is more likely that | |
2661 # it comes from setuptools attempting to parse a filename and | |
2662 # including it in the list. So for that we'll gate this warning | |
2663 # on if the version is anything at all or not. | |
2664 if not self.version: | |
2665 return | |
2666 | |
2667 tmpl = textwrap.dedent(""" | |
2668 '{project_name} ({version})' is being parsed as a legacy, | |
2669 non PEP 440, | |
2670 version. You may find odd behavior and sort order. | |
2671 In particular it will be sorted as less than 0.0. It | |
2672 is recommended to migrate to PEP 440 compatible | |
2673 versions. | |
2674 """).strip().replace('\n', ' ') | |
2675 | |
2676 warnings.warn(tmpl.format(**vars(self)), PEP440Warning) | |
2677 | |
2678 @property | |
2679 def version(self): | |
2680 try: | |
2681 return self._version | |
2682 except AttributeError: | |
2683 version = self._get_version() | |
2684 if version is None: | |
2685 path = self._get_metadata_path_for_display(self.PKG_INFO) | |
2686 msg = ( | |
2687 "Missing 'Version:' header and/or {} file at path: {}" | |
2688 ).format(self.PKG_INFO, path) | |
2689 raise ValueError(msg, self) | |
2690 | |
2691 return version | |
2692 | |
2693 @property | |
2694 def _dep_map(self): | |
2695 """ | |
2696 A map of extra to its list of (direct) requirements | |
2697 for this distribution, including the null extra. | |
2698 """ | |
2699 try: | |
2700 return self.__dep_map | |
2701 except AttributeError: | |
2702 self.__dep_map = self._filter_extras(self._build_dep_map()) | |
2703 return self.__dep_map | |
2704 | |
2705 @staticmethod | |
2706 def _filter_extras(dm): | |
2707 """ | |
2708 Given a mapping of extras to dependencies, strip off | |
2709 environment markers and filter out any dependencies | |
2710 not matching the markers. | |
2711 """ | |
2712 for extra in list(filter(None, dm)): | |
2713 new_extra = extra | |
2714 reqs = dm.pop(extra) | |
2715 new_extra, _, marker = extra.partition(':') | |
2716 fails_marker = marker and ( | |
2717 invalid_marker(marker) | |
2718 or not evaluate_marker(marker) | |
2719 ) | |
2720 if fails_marker: | |
2721 reqs = [] | |
2722 new_extra = safe_extra(new_extra) or None | |
2723 | |
2724 dm.setdefault(new_extra, []).extend(reqs) | |
2725 return dm | |
2726 | |
2727 def _build_dep_map(self): | |
2728 dm = {} | |
2729 for name in 'requires.txt', 'depends.txt': | |
2730 for extra, reqs in split_sections(self._get_metadata(name)): | |
2731 dm.setdefault(extra, []).extend(parse_requirements(reqs)) | |
2732 return dm | |
2733 | |
2734 def requires(self, extras=()): | |
2735 """List of Requirements needed for this distro if `extras` are used""" | |
2736 dm = self._dep_map | |
2737 deps = [] | |
2738 deps.extend(dm.get(None, ())) | |
2739 for ext in extras: | |
2740 try: | |
2741 deps.extend(dm[safe_extra(ext)]) | |
2742 except KeyError: | |
2743 raise UnknownExtra( | |
2744 "%s has no such extra feature %r" % (self, ext) | |
2745 ) | |
2746 return deps | |
2747 | |
2748 def _get_metadata_path_for_display(self, name): | |
2749 """ | |
2750 Return the path to the given metadata file, if available. | |
2751 """ | |
2752 try: | |
2753 # We need to access _get_metadata_path() on the provider object | |
2754 # directly rather than through this class's __getattr__() | |
2755 # since _get_metadata_path() is marked private. | |
2756 path = self._provider._get_metadata_path(name) | |
2757 | |
2758 # Handle exceptions e.g. in case the distribution's metadata | |
2759 # provider doesn't support _get_metadata_path(). | |
2760 except Exception: | |
2761 return '[could not detect]' | |
2762 | |
2763 return path | |
2764 | |
2765 def _get_metadata(self, name): | |
2766 if self.has_metadata(name): | |
2767 for line in self.get_metadata_lines(name): | |
2768 yield line | |
2769 | |
2770 def _get_version(self): | |
2771 lines = self._get_metadata(self.PKG_INFO) | |
2772 version = _version_from_file(lines) | |
2773 | |
2774 return version | |
2775 | |
2776 def activate(self, path=None, replace=False): | |
2777 """Ensure distribution is importable on `path` (default=sys.path)""" | |
2778 if path is None: | |
2779 path = sys.path | |
2780 self.insert_on(path, replace=replace) | |
2781 if path is sys.path: | |
2782 fixup_namespace_packages(self.location) | |
2783 for pkg in self._get_metadata('namespace_packages.txt'): | |
2784 if pkg in sys.modules: | |
2785 declare_namespace(pkg) | |
2786 | |
2787 def egg_name(self): | |
2788 """Return what this distribution's standard .egg filename should be""" | |
2789 filename = "%s-%s-py%s" % ( | |
2790 to_filename(self.project_name), to_filename(self.version), | |
2791 self.py_version or PY_MAJOR | |
2792 ) | |
2793 | |
2794 if self.platform: | |
2795 filename += '-' + self.platform | |
2796 return filename | |
2797 | |
2798 def __repr__(self): | |
2799 if self.location: | |
2800 return "%s (%s)" % (self, self.location) | |
2801 else: | |
2802 return str(self) | |
2803 | |
2804 def __str__(self): | |
2805 try: | |
2806 version = getattr(self, 'version', None) | |
2807 except ValueError: | |
2808 version = None | |
2809 version = version or "[unknown version]" | |
2810 return "%s %s" % (self.project_name, version) | |
2811 | |
2812 def __getattr__(self, attr): | |
2813 """Delegate all unrecognized public attributes to .metadata provider""" | |
2814 if attr.startswith('_'): | |
2815 raise AttributeError(attr) | |
2816 return getattr(self._provider, attr) | |
2817 | |
2818 def __dir__(self): | |
2819 return list( | |
2820 set(super(Distribution, self).__dir__()) | |
2821 | set( | |
2822 attr for attr in self._provider.__dir__() | |
2823 if not attr.startswith('_') | |
2824 ) | |
2825 ) | |
2826 | |
2827 if not hasattr(object, '__dir__'): | |
2828 # python 2.7 not supported | |
2829 del __dir__ | |
2830 | |
2831 @classmethod | |
2832 def from_filename(cls, filename, metadata=None, **kw): | |
2833 return cls.from_location( | |
2834 _normalize_cached(filename), os.path.basename(filename), metadata, | |
2835 **kw | |
2836 ) | |
2837 | |
2838 def as_requirement(self): | |
2839 """Return a ``Requirement`` that matches this distribution exactly""" | |
2840 if isinstance(self.parsed_version, packaging.version.Version): | |
2841 spec = "%s==%s" % (self.project_name, self.parsed_version) | |
2842 else: | |
2843 spec = "%s===%s" % (self.project_name, self.parsed_version) | |
2844 | |
2845 return Requirement.parse(spec) | |
2846 | |
2847 def load_entry_point(self, group, name): | |
2848 """Return the `name` entry point of `group` or raise ImportError""" | |
2849 ep = self.get_entry_info(group, name) | |
2850 if ep is None: | |
2851 raise ImportError("Entry point %r not found" % ((group, name),)) | |
2852 return ep.load() | |
2853 | |
2854 def get_entry_map(self, group=None): | |
2855 """Return the entry point map for `group`, or the full entry map""" | |
2856 try: | |
2857 ep_map = self._ep_map | |
2858 except AttributeError: | |
2859 ep_map = self._ep_map = EntryPoint.parse_map( | |
2860 self._get_metadata('entry_points.txt'), self | |
2861 ) | |
2862 if group is not None: | |
2863 return ep_map.get(group, {}) | |
2864 return ep_map | |
2865 | |
2866 def get_entry_info(self, group, name): | |
2867 """Return the EntryPoint object for `group`+`name`, or ``None``""" | |
2868 return self.get_entry_map(group).get(name) | |
2869 | |
2870 def insert_on(self, path, loc=None, replace=False): | |
2871 """Ensure self.location is on path | |
2872 | |
2873 If replace=False (default): | |
2874 - If location is already in path anywhere, do nothing. | |
2875 - Else: | |
2876 - If it's an egg and its parent directory is on path, | |
2877 insert just ahead of the parent. | |
2878 - Else: add to the end of path. | |
2879 If replace=True: | |
2880 - If location is already on path anywhere (not eggs) | |
2881 or higher priority than its parent (eggs) | |
2882 do nothing. | |
2883 - Else: | |
2884 - If it's an egg and its parent directory is on path, | |
2885 insert just ahead of the parent, | |
2886 removing any lower-priority entries. | |
2887 - Else: add it to the front of path. | |
2888 """ | |
2889 | |
2890 loc = loc or self.location | |
2891 if not loc: | |
2892 return | |
2893 | |
2894 nloc = _normalize_cached(loc) | |
2895 bdir = os.path.dirname(nloc) | |
2896 npath = [(p and _normalize_cached(p) or p) for p in path] | |
2897 | |
2898 for p, item in enumerate(npath): | |
2899 if item == nloc: | |
2900 if replace: | |
2901 break | |
2902 else: | |
2903 # don't modify path (even removing duplicates) if | |
2904 # found and not replace | |
2905 return | |
2906 elif item == bdir and self.precedence == EGG_DIST: | |
2907 # if it's an .egg, give it precedence over its directory | |
2908 # UNLESS it's already been added to sys.path and replace=False | |
2909 if (not replace) and nloc in npath[p:]: | |
2910 return | |
2911 if path is sys.path: | |
2912 self.check_version_conflict() | |
2913 path.insert(p, loc) | |
2914 npath.insert(p, nloc) | |
2915 break | |
2916 else: | |
2917 if path is sys.path: | |
2918 self.check_version_conflict() | |
2919 if replace: | |
2920 path.insert(0, loc) | |
2921 else: | |
2922 path.append(loc) | |
2923 return | |
2924 | |
2925 # p is the spot where we found or inserted loc; now remove duplicates | |
2926 while True: | |
2927 try: | |
2928 np = npath.index(nloc, p + 1) | |
2929 except ValueError: | |
2930 break | |
2931 else: | |
2932 del npath[np], path[np] | |
2933 # ha! | |
2934 p = np | |
2935 | |
2936 return | |
2937 | |
2938 def check_version_conflict(self): | |
2939 if self.key == 'setuptools': | |
2940 # ignore the inevitable setuptools self-conflicts :( | |
2941 return | |
2942 | |
2943 nsp = dict.fromkeys(self._get_metadata('namespace_packages.txt')) | |
2944 loc = normalize_path(self.location) | |
2945 for modname in self._get_metadata('top_level.txt'): | |
2946 if (modname not in sys.modules or modname in nsp | |
2947 or modname in _namespace_packages): | |
2948 continue | |
2949 if modname in ('pkg_resources', 'setuptools', 'site'): | |
2950 continue | |
2951 fn = getattr(sys.modules[modname], '__file__', None) | |
2952 if fn and (normalize_path(fn).startswith(loc) or | |
2953 fn.startswith(self.location)): | |
2954 continue | |
2955 issue_warning( | |
2956 "Module %s was already imported from %s, but %s is being added" | |
2957 " to sys.path" % (modname, fn, self.location), | |
2958 ) | |
2959 | |
2960 def has_version(self): | |
2961 try: | |
2962 self.version | |
2963 except ValueError: | |
2964 issue_warning("Unbuilt egg for " + repr(self)) | |
2965 return False | |
2966 return True | |
2967 | |
2968 def clone(self, **kw): | |
2969 """Copy this distribution, substituting in any changed keyword args""" | |
2970 names = 'project_name version py_version platform location precedence' | |
2971 for attr in names.split(): | |
2972 kw.setdefault(attr, getattr(self, attr, None)) | |
2973 kw.setdefault('metadata', self._provider) | |
2974 return self.__class__(**kw) | |
2975 | |
2976 @property | |
2977 def extras(self): | |
2978 return [dep for dep in self._dep_map if dep] | |
2979 | |
2980 | |
2981 class EggInfoDistribution(Distribution): | |
2982 def _reload_version(self): | |
2983 """ | |
2984 Packages installed by distutils (e.g. numpy or scipy), | |
2985 which uses an old safe_version, and so | |
2986 their version numbers can get mangled when | |
2987 converted to filenames (e.g., 1.11.0.dev0+2329eae to | |
2988 1.11.0.dev0_2329eae). These distributions will not be | |
2989 parsed properly | |
2990 downstream by Distribution and safe_version, so | |
2991 take an extra step and try to get the version number from | |
2992 the metadata file itself instead of the filename. | |
2993 """ | |
2994 md_version = self._get_version() | |
2995 if md_version: | |
2996 self._version = md_version | |
2997 return self | |
2998 | |
2999 | |
3000 class DistInfoDistribution(Distribution): | |
3001 """ | |
3002 Wrap an actual or potential sys.path entry | |
3003 w/metadata, .dist-info style. | |
3004 """ | |
3005 PKG_INFO = 'METADATA' | |
3006 EQEQ = re.compile(r"([\(,])\s*(\d.*?)\s*([,\)])") | |
3007 | |
3008 @property | |
3009 def _parsed_pkg_info(self): | |
3010 """Parse and cache metadata""" | |
3011 try: | |
3012 return self._pkg_info | |
3013 except AttributeError: | |
3014 metadata = self.get_metadata(self.PKG_INFO) | |
3015 self._pkg_info = email.parser.Parser().parsestr(metadata) | |
3016 return self._pkg_info | |
3017 | |
3018 @property | |
3019 def _dep_map(self): | |
3020 try: | |
3021 return self.__dep_map | |
3022 except AttributeError: | |
3023 self.__dep_map = self._compute_dependencies() | |
3024 return self.__dep_map | |
3025 | |
3026 def _compute_dependencies(self): | |
3027 """Recompute this distribution's dependencies.""" | |
3028 dm = self.__dep_map = {None: []} | |
3029 | |
3030 reqs = [] | |
3031 # Including any condition expressions | |
3032 for req in self._parsed_pkg_info.get_all('Requires-Dist') or []: | |
3033 reqs.extend(parse_requirements(req)) | |
3034 | |
3035 def reqs_for_extra(extra): | |
3036 for req in reqs: | |
3037 if not req.marker or req.marker.evaluate({'extra': extra}): | |
3038 yield req | |
3039 | |
3040 common = frozenset(reqs_for_extra(None)) | |
3041 dm[None].extend(common) | |
3042 | |
3043 for extra in self._parsed_pkg_info.get_all('Provides-Extra') or []: | |
3044 s_extra = safe_extra(extra.strip()) | |
3045 dm[s_extra] = list(frozenset(reqs_for_extra(extra)) - common) | |
3046 | |
3047 return dm | |
3048 | |
3049 | |
3050 _distributionImpl = { | |
3051 '.egg': Distribution, | |
3052 '.egg-info': EggInfoDistribution, | |
3053 '.dist-info': DistInfoDistribution, | |
3054 } | |
3055 | |
3056 | |
3057 def issue_warning(*args, **kw): | |
3058 level = 1 | |
3059 g = globals() | |
3060 try: | |
3061 # find the first stack frame that is *not* code in | |
3062 # the pkg_resources module, to use for the warning | |
3063 while sys._getframe(level).f_globals is g: | |
3064 level += 1 | |
3065 except ValueError: | |
3066 pass | |
3067 warnings.warn(stacklevel=level + 1, *args, **kw) | |
3068 | |
3069 | |
3070 class RequirementParseError(ValueError): | |
3071 def __str__(self): | |
3072 return ' '.join(self.args) | |
3073 | |
3074 | |
3075 def parse_requirements(strs): | |
3076 """Yield ``Requirement`` objects for each specification in `strs` | |
3077 | |
3078 `strs` must be a string, or a (possibly-nested) iterable thereof. | |
3079 """ | |
3080 # create a steppable iterator, so we can handle \-continuations | |
3081 lines = iter(yield_lines(strs)) | |
3082 | |
3083 for line in lines: | |
3084 # Drop comments -- a hash without a space may be in a URL. | |
3085 if ' #' in line: | |
3086 line = line[:line.find(' #')] | |
3087 # If there is a line continuation, drop it, and append the next line. | |
3088 if line.endswith('\\'): | |
3089 line = line[:-2].strip() | |
3090 try: | |
3091 line += next(lines) | |
3092 except StopIteration: | |
3093 return | |
3094 yield Requirement(line) | |
3095 | |
3096 | |
3097 class Requirement(packaging.requirements.Requirement): | |
3098 def __init__(self, requirement_string): | |
3099 """DO NOT CALL THIS UNDOCUMENTED METHOD; use Requirement.parse()!""" | |
3100 try: | |
3101 super(Requirement, self).__init__(requirement_string) | |
3102 except packaging.requirements.InvalidRequirement as e: | |
3103 raise RequirementParseError(str(e)) | |
3104 self.unsafe_name = self.name | |
3105 project_name = safe_name(self.name) | |
3106 self.project_name, self.key = project_name, project_name.lower() | |
3107 self.specs = [ | |
3108 (spec.operator, spec.version) for spec in self.specifier] | |
3109 self.extras = tuple(map(safe_extra, self.extras)) | |
3110 self.hashCmp = ( | |
3111 self.key, | |
3112 self.url, | |
3113 self.specifier, | |
3114 frozenset(self.extras), | |
3115 str(self.marker) if self.marker else None, | |
3116 ) | |
3117 self.__hash = hash(self.hashCmp) | |
3118 | |
3119 def __eq__(self, other): | |
3120 return ( | |
3121 isinstance(other, Requirement) and | |
3122 self.hashCmp == other.hashCmp | |
3123 ) | |
3124 | |
3125 def __ne__(self, other): | |
3126 return not self == other | |
3127 | |
3128 def __contains__(self, item): | |
3129 if isinstance(item, Distribution): | |
3130 if item.key != self.key: | |
3131 return False | |
3132 | |
3133 item = item.version | |
3134 | |
3135 # Allow prereleases always in order to match the previous behavior of | |
3136 # this method. In the future this should be smarter and follow PEP 440 | |
3137 # more accurately. | |
3138 return self.specifier.contains(item, prereleases=True) | |
3139 | |
3140 def __hash__(self): | |
3141 return self.__hash | |
3142 | |
3143 def __repr__(self): | |
3144 return "Requirement.parse(%r)" % str(self) | |
3145 | |
3146 @staticmethod | |
3147 def parse(s): | |
3148 req, = parse_requirements(s) | |
3149 return req | |
3150 | |
3151 | |
3152 def _always_object(classes): | |
3153 """ | |
3154 Ensure object appears in the mro even | |
3155 for old-style classes. | |
3156 """ | |
3157 if object not in classes: | |
3158 return classes + (object,) | |
3159 return classes | |
3160 | |
3161 | |
3162 def _find_adapter(registry, ob): | |
3163 """Return an adapter factory for `ob` from `registry`""" | |
3164 types = _always_object(inspect.getmro(getattr(ob, '__class__', type(ob)))) | |
3165 for t in types: | |
3166 if t in registry: | |
3167 return registry[t] | |
3168 | |
3169 | |
3170 def ensure_directory(path): | |
3171 """Ensure that the parent directory of `path` exists""" | |
3172 dirname = os.path.dirname(path) | |
3173 py31compat.makedirs(dirname, exist_ok=True) | |
3174 | |
3175 | |
3176 def _bypass_ensure_directory(path): | |
3177 """Sandbox-bypassing version of ensure_directory()""" | |
3178 if not WRITE_SUPPORT: | |
3179 raise IOError('"os.mkdir" not supported on this platform.') | |
3180 dirname, filename = split(path) | |
3181 if dirname and filename and not isdir(dirname): | |
3182 _bypass_ensure_directory(dirname) | |
3183 try: | |
3184 mkdir(dirname, 0o755) | |
3185 except FileExistsError: | |
3186 pass | |
3187 | |
3188 | |
3189 def split_sections(s): | |
3190 """Split a string or iterable thereof into (section, content) pairs | |
3191 | |
3192 Each ``section`` is a stripped version of the section header ("[section]") | |
3193 and each ``content`` is a list of stripped lines excluding blank lines and | |
3194 comment-only lines. If there are any such lines before the first section | |
3195 header, they're returned in a first ``section`` of ``None``. | |
3196 """ | |
3197 section = None | |
3198 content = [] | |
3199 for line in yield_lines(s): | |
3200 if line.startswith("["): | |
3201 if line.endswith("]"): | |
3202 if section or content: | |
3203 yield section, content | |
3204 section = line[1:-1].strip() | |
3205 content = [] | |
3206 else: | |
3207 raise ValueError("Invalid section heading", line) | |
3208 else: | |
3209 content.append(line) | |
3210 | |
3211 # wrap up last segment | |
3212 yield section, content | |
3213 | |
3214 | |
3215 def _mkstemp(*args, **kw): | |
3216 old_open = os.open | |
3217 try: | |
3218 # temporarily bypass sandboxing | |
3219 os.open = os_open | |
3220 return tempfile.mkstemp(*args, **kw) | |
3221 finally: | |
3222 # and then put it back | |
3223 os.open = old_open | |
3224 | |
3225 | |
3226 # Silence the PEP440Warning by default, so that end users don't get hit by it | |
3227 # randomly just because they use pkg_resources. We want to append the rule | |
3228 # because we want earlier uses of filterwarnings to take precedence over this | |
3229 # one. | |
3230 warnings.filterwarnings("ignore", category=PEP440Warning, append=True) | |
3231 | |
3232 | |
3233 # from jaraco.functools 1.3 | |
3234 def _call_aside(f, *args, **kwargs): | |
3235 f(*args, **kwargs) | |
3236 return f | |
3237 | |
3238 | |
3239 @_call_aside | |
3240 def _initialize(g=globals()): | |
3241 "Set up global resource manager (deliberately not state-saved)" | |
3242 manager = ResourceManager() | |
3243 g['_manager'] = manager | |
3244 g.update( | |
3245 (name, getattr(manager, name)) | |
3246 for name in dir(manager) | |
3247 if not name.startswith('_') | |
3248 ) | |
3249 | |
3250 | |
3251 @_call_aside | |
3252 def _initialize_master_working_set(): | |
3253 """ | |
3254 Prepare the master working set and make the ``require()`` | |
3255 API available. | |
3256 | |
3257 This function has explicit effects on the global state | |
3258 of pkg_resources. It is intended to be invoked once at | |
3259 the initialization of this module. | |
3260 | |
3261 Invocation by other packages is unsupported and done | |
3262 at their own risk. | |
3263 """ | |
3264 working_set = WorkingSet._build_master() | |
3265 _declare_state('object', working_set=working_set) | |
3266 | |
3267 require = working_set.require | |
3268 iter_entry_points = working_set.iter_entry_points | |
3269 add_activation_listener = working_set.subscribe | |
3270 run_script = working_set.run_script | |
3271 # backward compatibility | |
3272 run_main = run_script | |
3273 # Activate all distributions already on sys.path with replace=False and | |
3274 # ensure that all distributions added to the working set in the future | |
3275 # (e.g. by calling ``require()``) will get activated as well, | |
3276 # with higher priority (replace=True). | |
3277 tuple( | |
3278 dist.activate(replace=False) | |
3279 for dist in working_set | |
3280 ) | |
3281 add_activation_listener( | |
3282 lambda dist: dist.activate(replace=True), | |
3283 existing=False, | |
3284 ) | |
3285 working_set.entries = [] | |
3286 # match order | |
3287 list(map(working_set.add_entry, sys.path)) | |
3288 globals().update(locals()) | |
3289 | |
3290 class PkgResourcesDeprecationWarning(Warning): | |
3291 """ | |
3292 Base class for warning about deprecations in ``pkg_resources`` | |
3293 | |
3294 This class is not derived from ``DeprecationWarning``, and as such is | |
3295 visible by default. | |
3296 """ |