resources.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2013 Vinay Sajip.
  4. # Licensed to the Python Software Foundation under a contributor agreement.
  5. # See LICENSE.txt and CONTRIBUTORS.txt.
  6. #
  7. from __future__ import unicode_literals
  8. import bisect
  9. import io
  10. import logging
  11. import os
  12. import pkgutil
  13. import shutil
  14. import sys
  15. import types
  16. import zipimport
  17. from . import DistlibException
  18. from .util import cached_property, get_cache_base, path_to_cache_dir, Cache
  19. logger = logging.getLogger(__name__)
  20. cache = None # created when needed
  21. class ResourceCache(Cache):
  22. def __init__(self, base=None):
  23. if base is None:
  24. # Use native string to avoid issues on 2.x: see Python #20140.
  25. base = os.path.join(get_cache_base(), str('resource-cache'))
  26. super(ResourceCache, self).__init__(base)
  27. def is_stale(self, resource, path):
  28. """
  29. Is the cache stale for the given resource?
  30. :param resource: The :class:`Resource` being cached.
  31. :param path: The path of the resource in the cache.
  32. :return: True if the cache is stale.
  33. """
  34. # Cache invalidation is a hard problem :-)
  35. return True
  36. def get(self, resource):
  37. """
  38. Get a resource into the cache,
  39. :param resource: A :class:`Resource` instance.
  40. :return: The pathname of the resource in the cache.
  41. """
  42. prefix, path = resource.finder.get_cache_info(resource)
  43. if prefix is None:
  44. result = path
  45. else:
  46. result = os.path.join(self.base, self.prefix_to_dir(prefix), path)
  47. dirname = os.path.dirname(result)
  48. if not os.path.isdir(dirname):
  49. os.makedirs(dirname)
  50. if not os.path.exists(result):
  51. stale = True
  52. else:
  53. stale = self.is_stale(resource, path)
  54. if stale:
  55. # write the bytes of the resource to the cache location
  56. with open(result, 'wb') as f:
  57. f.write(resource.bytes)
  58. return result
  59. class ResourceBase(object):
  60. def __init__(self, finder, name):
  61. self.finder = finder
  62. self.name = name
  63. class Resource(ResourceBase):
  64. """
  65. A class representing an in-package resource, such as a data file. This is
  66. not normally instantiated by user code, but rather by a
  67. :class:`ResourceFinder` which manages the resource.
  68. """
  69. is_container = False # Backwards compatibility
  70. def as_stream(self):
  71. """
  72. Get the resource as a stream.
  73. This is not a property to make it obvious that it returns a new stream
  74. each time.
  75. """
  76. return self.finder.get_stream(self)
  77. @cached_property
  78. def file_path(self):
  79. global cache
  80. if cache is None:
  81. cache = ResourceCache()
  82. return cache.get(self)
  83. @cached_property
  84. def bytes(self):
  85. return self.finder.get_bytes(self)
  86. @cached_property
  87. def size(self):
  88. return self.finder.get_size(self)
  89. class ResourceContainer(ResourceBase):
  90. is_container = True # Backwards compatibility
  91. @cached_property
  92. def resources(self):
  93. return self.finder.get_resources(self)
  94. class ResourceFinder(object):
  95. """
  96. Resource finder for file system resources.
  97. """
  98. def __init__(self, module):
  99. self.module = module
  100. self.loader = getattr(module, '__loader__', None)
  101. self.base = os.path.dirname(getattr(module, '__file__', ''))
  102. def _adjust_path(self, path):
  103. return os.path.realpath(path)
  104. def _make_path(self, resource_name):
  105. # Issue #50: need to preserve type of path on Python 2.x
  106. # like os.path._get_sep
  107. if isinstance(resource_name, bytes): # should only happen on 2.x
  108. sep = b'/'
  109. else:
  110. sep = '/'
  111. parts = resource_name.split(sep)
  112. parts.insert(0, self.base)
  113. result = os.path.join(*parts)
  114. return self._adjust_path(result)
  115. def _find(self, path):
  116. return os.path.exists(path)
  117. def get_cache_info(self, resource):
  118. return None, resource.path
  119. def find(self, resource_name):
  120. path = self._make_path(resource_name)
  121. if not self._find(path):
  122. result = None
  123. else:
  124. if self._is_directory(path):
  125. result = ResourceContainer(self, resource_name)
  126. else:
  127. result = Resource(self, resource_name)
  128. result.path = path
  129. return result
  130. def get_stream(self, resource):
  131. return open(resource.path, 'rb')
  132. def get_bytes(self, resource):
  133. with open(resource.path, 'rb') as f:
  134. return f.read()
  135. def get_size(self, resource):
  136. return os.path.getsize(resource.path)
  137. def get_resources(self, resource):
  138. def allowed(f):
  139. return f != '__pycache__' and not f.endswith(('.pyc', '.pyo'))
  140. return set([f for f in os.listdir(resource.path) if allowed(f)])
  141. def is_container(self, resource):
  142. return self._is_directory(resource.path)
  143. _is_directory = staticmethod(os.path.isdir)
  144. class ZipResourceFinder(ResourceFinder):
  145. """
  146. Resource finder for resources in .zip files.
  147. """
  148. def __init__(self, module):
  149. super(ZipResourceFinder, self).__init__(module)
  150. archive = self.loader.archive
  151. self.prefix_len = 1 + len(archive)
  152. # PyPy doesn't have a _files attr on zipimporter, and you can't set one
  153. if hasattr(self.loader, '_files'):
  154. self._files = self.loader._files
  155. else:
  156. self._files = zipimport._zip_directory_cache[archive]
  157. self.index = sorted(self._files)
  158. def _adjust_path(self, path):
  159. return path
  160. def _find(self, path):
  161. path = path[self.prefix_len:]
  162. if path in self._files:
  163. result = True
  164. else:
  165. if path and path[-1] != os.sep:
  166. path = path + os.sep
  167. i = bisect.bisect(self.index, path)
  168. try:
  169. result = self.index[i].startswith(path)
  170. except IndexError:
  171. result = False
  172. if not result:
  173. logger.debug('_find failed: %r %r', path, self.loader.prefix)
  174. else:
  175. logger.debug('_find worked: %r %r', path, self.loader.prefix)
  176. return result
  177. def get_cache_info(self, resource):
  178. prefix = self.loader.archive
  179. path = resource.path[1 + len(prefix):]
  180. return prefix, path
  181. def get_bytes(self, resource):
  182. return self.loader.get_data(resource.path)
  183. def get_stream(self, resource):
  184. return io.BytesIO(self.get_bytes(resource))
  185. def get_size(self, resource):
  186. path = resource.path[self.prefix_len:]
  187. return self._files[path][3]
  188. def get_resources(self, resource):
  189. path = resource.path[self.prefix_len:]
  190. if path and path[-1] != os.sep:
  191. path += os.sep
  192. plen = len(path)
  193. result = set()
  194. i = bisect.bisect(self.index, path)
  195. while i < len(self.index):
  196. if not self.index[i].startswith(path):
  197. break
  198. s = self.index[i][plen:]
  199. result.add(s.split(os.sep, 1)[0]) # only immediate children
  200. i += 1
  201. return result
  202. def _is_directory(self, path):
  203. path = path[self.prefix_len:]
  204. if path and path[-1] != os.sep:
  205. path += os.sep
  206. i = bisect.bisect(self.index, path)
  207. try:
  208. result = self.index[i].startswith(path)
  209. except IndexError:
  210. result = False
  211. return result
  212. _finder_registry = {
  213. type(None): ResourceFinder,
  214. zipimport.zipimporter: ZipResourceFinder
  215. }
  216. try:
  217. import _frozen_importlib
  218. _finder_registry[_frozen_importlib.SourceFileLoader] = ResourceFinder
  219. _finder_registry[_frozen_importlib.FileFinder] = ResourceFinder
  220. except (ImportError, AttributeError):
  221. pass
  222. def register_finder(loader, finder_maker):
  223. _finder_registry[type(loader)] = finder_maker
  224. _finder_cache = {}
  225. def finder(package):
  226. """
  227. Return a resource finder for a package.
  228. :param package: The name of the package.
  229. :return: A :class:`ResourceFinder` instance for the package.
  230. """
  231. if package in _finder_cache:
  232. result = _finder_cache[package]
  233. else:
  234. if package not in sys.modules:
  235. __import__(package)
  236. module = sys.modules[package]
  237. path = getattr(module, '__path__', None)
  238. if path is None:
  239. raise DistlibException('You cannot get a finder for a module, '
  240. 'only for a package')
  241. loader = getattr(module, '__loader__', None)
  242. finder_maker = _finder_registry.get(type(loader))
  243. if finder_maker is None:
  244. raise DistlibException('Unable to locate finder for %r' % package)
  245. result = finder_maker(module)
  246. _finder_cache[package] = result
  247. return result
  248. _dummy_module = types.ModuleType(str('__dummy__'))
  249. def finder_for_path(path):
  250. """
  251. Return a resource finder for a path, which should represent a container.
  252. :param path: The path.
  253. :return: A :class:`ResourceFinder` instance for the path.
  254. """
  255. result = None
  256. # calls any path hooks, gets importer into cache
  257. pkgutil.get_importer(path)
  258. loader = sys.path_importer_cache.get(path)
  259. finder = _finder_registry.get(type(loader))
  260. if finder:
  261. module = _dummy_module
  262. module.__file__ = os.path.join(path, '')
  263. module.__loader__ = loader
  264. result = finder(module)
  265. return result