1
# -*- coding: utf-8 -*-
3
# Copyright (C) 2013-2016 Vinay Sajip.
4
# Licensed to the Python Software Foundation under a contributor agreement.
5
# See LICENSE.txt and CONTRIBUTORS.txt.
7
from __future__ import unicode_literals
19
from . import DistlibException
20
from .util import cached_property, get_cache_base, path_to_cache_dir, Cache
22
logger = logging.getLogger(__name__)
25
cache = None # created when needed
28
class ResourceCache(Cache):
29
def __init__(self, base=None):
31
# Use native string to avoid issues on 2.x: see Python #20140.
32
base = os.path.join(get_cache_base(), str('resource-cache'))
33
super(ResourceCache, self).__init__(base)
35
def is_stale(self, resource, path):
37
Is the cache stale for the given resource?
39
:param resource: The :class:`Resource` being cached.
40
:param path: The path of the resource in the cache.
41
:return: True if the cache is stale.
43
# Cache invalidation is a hard problem :-)
46
def get(self, resource):
48
Get a resource into the cache,
50
:param resource: A :class:`Resource` instance.
51
:return: The pathname of the resource in the cache.
53
prefix, path = resource.finder.get_cache_info(resource)
57
result = os.path.join(self.base, self.prefix_to_dir(prefix), path)
58
dirname = os.path.dirname(result)
59
if not os.path.isdir(dirname):
61
if not os.path.exists(result):
64
stale = self.is_stale(resource, path)
66
# write the bytes of the resource to the cache location
67
with open(result, 'wb') as f:
68
f.write(resource.bytes)
72
class ResourceBase(object):
73
def __init__(self, finder, name):
78
class Resource(ResourceBase):
80
A class representing an in-package resource, such as a data file. This is
81
not normally instantiated by user code, but rather by a
82
:class:`ResourceFinder` which manages the resource.
84
is_container = False # Backwards compatibility
88
Get the resource as a stream.
90
This is not a property to make it obvious that it returns a new stream
93
return self.finder.get_stream(self)
99
cache = ResourceCache()
100
return cache.get(self)
104
return self.finder.get_bytes(self)
108
return self.finder.get_size(self)
111
class ResourceContainer(ResourceBase):
112
is_container = True # Backwards compatibility
116
return self.finder.get_resources(self)
119
class ResourceFinder(object):
121
Resource finder for file system resources.
124
if sys.platform.startswith('java'):
125
skipped_extensions = ('.pyc', '.pyo', '.class')
127
skipped_extensions = ('.pyc', '.pyo')
129
def __init__(self, module):
131
self.loader = getattr(module, '__loader__', None)
132
self.base = os.path.dirname(getattr(module, '__file__', ''))
134
def _adjust_path(self, path):
135
return os.path.realpath(path)
137
def _make_path(self, resource_name):
138
# Issue #50: need to preserve type of path on Python 2.x
139
# like os.path._get_sep
140
if isinstance(resource_name, bytes): # should only happen on 2.x
144
parts = resource_name.split(sep)
145
parts.insert(0, self.base)
146
result = os.path.join(*parts)
147
return self._adjust_path(result)
149
def _find(self, path):
150
return os.path.exists(path)
152
def get_cache_info(self, resource):
153
return None, resource.path
155
def find(self, resource_name):
156
path = self._make_path(resource_name)
157
if not self._find(path):
160
if self._is_directory(path):
161
result = ResourceContainer(self, resource_name)
163
result = Resource(self, resource_name)
167
def get_stream(self, resource):
168
return open(resource.path, 'rb')
170
def get_bytes(self, resource):
171
with open(resource.path, 'rb') as f:
174
def get_size(self, resource):
175
return os.path.getsize(resource.path)
177
def get_resources(self, resource):
179
return (f != '__pycache__' and not
180
f.endswith(self.skipped_extensions))
181
return set([f for f in os.listdir(resource.path) if allowed(f)])
183
def is_container(self, resource):
184
return self._is_directory(resource.path)
186
_is_directory = staticmethod(os.path.isdir)
188
def iterator(self, resource_name):
189
resource = self.find(resource_name)
190
if resource is not None:
193
resource = todo.pop(0)
195
if resource.is_container:
196
rname = resource.name
197
for name in resource.resources:
201
new_name = '/'.join([rname, name])
202
child = self.find(new_name)
203
if child.is_container:
209
class ZipResourceFinder(ResourceFinder):
211
Resource finder for resources in .zip files.
213
def __init__(self, module):
214
super(ZipResourceFinder, self).__init__(module)
215
archive = self.loader.archive
216
self.prefix_len = 1 + len(archive)
217
# PyPy doesn't have a _files attr on zipimporter, and you can't set one
218
if hasattr(self.loader, '_files'):
219
self._files = self.loader._files
221
self._files = zipimport._zip_directory_cache[archive]
222
self.index = sorted(self._files)
224
def _adjust_path(self, path):
227
def _find(self, path):
228
path = path[self.prefix_len:]
229
if path in self._files:
232
if path and path[-1] != os.sep:
234
i = bisect.bisect(self.index, path)
236
result = self.index[i].startswith(path)
240
logger.debug('_find failed: %r %r', path, self.loader.prefix)
242
logger.debug('_find worked: %r %r', path, self.loader.prefix)
245
def get_cache_info(self, resource):
246
prefix = self.loader.archive
247
path = resource.path[1 + len(prefix):]
250
def get_bytes(self, resource):
251
return self.loader.get_data(resource.path)
253
def get_stream(self, resource):
254
return io.BytesIO(self.get_bytes(resource))
256
def get_size(self, resource):
257
path = resource.path[self.prefix_len:]
258
return self._files[path][3]
260
def get_resources(self, resource):
261
path = resource.path[self.prefix_len:]
262
if path and path[-1] != os.sep:
266
i = bisect.bisect(self.index, path)
267
while i < len(self.index):
268
if not self.index[i].startswith(path):
270
s = self.index[i][plen:]
271
result.add(s.split(os.sep, 1)[0]) # only immediate children
275
def _is_directory(self, path):
276
path = path[self.prefix_len:]
277
if path and path[-1] != os.sep:
279
i = bisect.bisect(self.index, path)
281
result = self.index[i].startswith(path)
287
type(None): ResourceFinder,
288
zipimport.zipimporter: ZipResourceFinder
292
# In Python 3.6, _frozen_importlib -> _frozen_importlib_external
294
import _frozen_importlib_external as _fi
296
import _frozen_importlib as _fi
297
_finder_registry[_fi.SourceFileLoader] = ResourceFinder
298
_finder_registry[_fi.FileFinder] = ResourceFinder
300
except (ImportError, AttributeError):
304
def register_finder(loader, finder_maker):
305
_finder_registry[type(loader)] = finder_maker
312
Return a resource finder for a package.
313
:param package: The name of the package.
314
:return: A :class:`ResourceFinder` instance for the package.
316
if package in _finder_cache:
317
result = _finder_cache[package]
319
if package not in sys.modules:
321
module = sys.modules[package]
322
path = getattr(module, '__path__', None)
324
raise DistlibException('You cannot get a finder for a module, '
325
'only for a package')
326
loader = getattr(module, '__loader__', None)
327
finder_maker = _finder_registry.get(type(loader))
328
if finder_maker is None:
329
raise DistlibException('Unable to locate finder for %r' % package)
330
result = finder_maker(module)
331
_finder_cache[package] = result
335
_dummy_module = types.ModuleType(str('__dummy__'))
338
def finder_for_path(path):
340
Return a resource finder for a path, which should represent a container.
342
:param path: The path.
343
:return: A :class:`ResourceFinder` instance for the path.
346
# calls any path hooks, gets importer into cache
347
pkgutil.get_importer(path)
348
loader = sys.path_importer_cache.get(path)
349
finder = _finder_registry.get(type(loader))
351
module = _dummy_module
352
module.__file__ = os.path.join(path, '')
353
module.__loader__ = loader
354
result = finder(module)