1 # -*- coding: utf-8 -*-
3 # Copyright (C) 2013-2017 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
18 from . import DistlibException
19 from .util
import cached_property
, get_cache_base
, Cache
21 logger
= logging
.getLogger(__name__
)
24 cache
= None # created when needed
27 class ResourceCache(Cache
):
28 def __init__(self
, base
=None):
30 # Use native string to avoid issues on 2.x: see Python #20140.
31 base
= os
.path
.join(get_cache_base(), str('resource-cache'))
32 super(ResourceCache
, self
).__init
__(base
)
34 def is_stale(self
, resource
, path
):
36 Is the cache stale for the given resource?
38 :param resource: The :class:`Resource` being cached.
39 :param path: The path of the resource in the cache.
40 :return: True if the cache is stale.
42 # Cache invalidation is a hard problem :-)
45 def get(self
, resource
):
47 Get a resource into the cache,
49 :param resource: A :class:`Resource` instance.
50 :return: The pathname of the resource in the cache.
52 prefix
, path
= resource
.finder
.get_cache_info(resource
)
56 result
= os
.path
.join(self
.base
, self
.prefix_to_dir(prefix
), path
)
57 dirname
= os
.path
.dirname(result
)
58 if not os
.path
.isdir(dirname
):
60 if not os
.path
.exists(result
):
63 stale
= self
.is_stale(resource
, path
)
65 # write the bytes of the resource to the cache location
66 with open(result
, 'wb') as f
:
67 f
.write(resource
.bytes)
71 class ResourceBase(object):
72 def __init__(self
, finder
, name
):
77 class Resource(ResourceBase
):
79 A class representing an in-package resource, such as a data file. This is
80 not normally instantiated by user code, but rather by a
81 :class:`ResourceFinder` which manages the resource.
83 is_container
= False # Backwards compatibility
87 Get the resource as a stream.
89 This is not a property to make it obvious that it returns a new stream
92 return self
.finder
.get_stream(self
)
98 cache
= ResourceCache()
99 return cache
.get(self
)
103 return self
.finder
.get_bytes(self
)
107 return self
.finder
.get_size(self
)
110 class ResourceContainer(ResourceBase
):
111 is_container
= True # Backwards compatibility
115 return self
.finder
.get_resources(self
)
118 class ResourceFinder(object):
120 Resource finder for file system resources.
123 if sys
.platform
.startswith('java'):
124 skipped_extensions
= ('.pyc', '.pyo', '.class')
126 skipped_extensions
= ('.pyc', '.pyo')
128 def __init__(self
, module
):
130 self
.loader
= getattr(module
, '__loader__', None)
131 self
.base
= os
.path
.dirname(getattr(module
, '__file__', ''))
133 def _adjust_path(self
, path
):
134 return os
.path
.realpath(path
)
136 def _make_path(self
, resource_name
):
137 # Issue #50: need to preserve type of path on Python 2.x
138 # like os.path._get_sep
139 if isinstance(resource_name
, bytes): # should only happen on 2.x
143 parts
= resource_name
.split(sep
)
144 parts
.insert(0, self
.base
)
145 result
= os
.path
.join(*parts
)
146 return self
._adjust
_path
(result
)
148 def _find(self
, path
):
149 return os
.path
.exists(path
)
151 def get_cache_info(self
, resource
):
152 return None, resource
.path
154 def find(self
, resource_name
):
155 path
= self
._make
_path
(resource_name
)
156 if not self
._find
(path
):
159 if self
._is
_directory
(path
):
160 result
= ResourceContainer(self
, resource_name
)
162 result
= Resource(self
, resource_name
)
166 def get_stream(self
, resource
):
167 return open(resource
.path
, 'rb')
169 def get_bytes(self
, resource
):
170 with open(resource
.path
, 'rb') as f
:
173 def get_size(self
, resource
):
174 return os
.path
.getsize(resource
.path
)
176 def get_resources(self
, resource
):
178 return (f
!= '__pycache__' and not
179 f
.endswith(self
.skipped_extensions
))
180 return set([f
for f
in os
.listdir(resource
.path
) if allowed(f
)])
182 def is_container(self
, resource
):
183 return self
._is
_directory
(resource
.path
)
185 _is_directory
= staticmethod(os
.path
.isdir
)
187 def iterator(self
, resource_name
):
188 resource
= self
.find(resource_name
)
189 if resource
is not None:
192 resource
= todo
.pop(0)
194 if resource
.is_container
:
195 rname
= resource
.name
196 for name
in resource
.resources
:
200 new_name
= '/'.join([rname
, name
])
201 child
= self
.find(new_name
)
202 if child
.is_container
:
208 class ZipResourceFinder(ResourceFinder
):
210 Resource finder for resources in .zip files.
212 def __init__(self
, module
):
213 super(ZipResourceFinder
, self
).__init
__(module
)
214 archive
= self
.loader
.archive
215 self
.prefix_len
= 1 + len(archive
)
216 # PyPy doesn't have a _files attr on zipimporter, and you can't set one
217 if hasattr(self
.loader
, '_files'):
218 self
._files
= self
.loader
._files
220 self
._files
= zipimport
._zip
_directory
_cache
[archive
]
221 self
.index
= sorted(self
._files
)
223 def _adjust_path(self
, path
):
226 def _find(self
, path
):
227 path
= path
[self
.prefix_len
:]
228 if path
in self
._files
:
231 if path
and path
[-1] != os
.sep
:
233 i
= bisect
.bisect(self
.index
, path
)
235 result
= self
.index
[i
].startswith(path
)
239 logger
.debug('_find failed: %r %r', path
, self
.loader
.prefix
)
241 logger
.debug('_find worked: %r %r', path
, self
.loader
.prefix
)
244 def get_cache_info(self
, resource
):
245 prefix
= self
.loader
.archive
246 path
= resource
.path
[1 + len(prefix
):]
249 def get_bytes(self
, resource
):
250 return self
.loader
.get_data(resource
.path
)
252 def get_stream(self
, resource
):
253 return io
.BytesIO(self
.get_bytes(resource
))
255 def get_size(self
, resource
):
256 path
= resource
.path
[self
.prefix_len
:]
257 return self
._files
[path
][3]
259 def get_resources(self
, resource
):
260 path
= resource
.path
[self
.prefix_len
:]
261 if path
and path
[-1] != os
.sep
:
265 i
= bisect
.bisect(self
.index
, path
)
266 while i
< len(self
.index
):
267 if not self
.index
[i
].startswith(path
):
269 s
= self
.index
[i
][plen
:]
270 result
.add(s
.split(os
.sep
, 1)[0]) # only immediate children
274 def _is_directory(self
, path
):
275 path
= path
[self
.prefix_len
:]
276 if path
and path
[-1] != os
.sep
:
278 i
= bisect
.bisect(self
.index
, path
)
280 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 _finder_registry
[_fi
.SourcelessFileLoader
] = ResourceFinder
302 except (ImportError, AttributeError):
306 def register_finder(loader
, finder_maker
):
307 _finder_registry
[type(loader
)] = finder_maker
315 Return a resource finder for a package.
316 :param package: The name of the package.
317 :return: A :class:`ResourceFinder` instance for the package.
319 if package
in _finder_cache
:
320 result
= _finder_cache
[package
]
322 if package
not in sys
.modules
:
324 module
= sys
.modules
[package
]
325 path
= getattr(module
, '__path__', None)
327 raise DistlibException('You cannot get a finder for a module, '
328 'only for a package')
329 loader
= getattr(module
, '__loader__', None)
330 finder_maker
= _finder_registry
.get(type(loader
))
331 if finder_maker
is None:
332 raise DistlibException('Unable to locate finder for %r' % package
)
333 result
= finder_maker(module
)
334 _finder_cache
[package
] = result
338 _dummy_module
= types
.ModuleType(str('__dummy__'))
341 def finder_for_path(path
):
343 Return a resource finder for a path, which should represent a container.
345 :param path: The path.
346 :return: A :class:`ResourceFinder` instance for the path.
349 # calls any path hooks, gets importer into cache
350 pkgutil
.get_importer(path
)
351 loader
= sys
.path_importer_cache
.get(path
)
352 finder
= _finder_registry
.get(type(loader
))
354 module
= _dummy_module
355 module
.__file
__ = os
.path
.join(path
, '')
356 module
.__loader
__ = loader
357 result
= finder(module
)