Michael Merickel
2018-10-19 d579f2104de139e0b0fc5d6c81aabb2f826e5e54
commit | author | age
407b33 1 import os
6da017 2 import pkg_resources
5bf23f 3 import sys
6da017 4
3b7334 5 from zope.interface import implementer
5bf23f 6
0c29cf 7 from pyramid.interfaces import IPackageOverrides, PHASE1_CONFIG
5bf23f 8
CM 9 from pyramid.exceptions import ConfigurationError
6da017 10 from pyramid.threadlocal import get_current_registry
5bf23f 11
d579f2 12 from pyramid.config.actions import action_method
0c29cf 13
6da017 14
CM 15 class OverrideProvider(pkg_resources.DefaultProvider):
16     def __init__(self, module):
17         pkg_resources.DefaultProvider.__init__(self, module)
18         self.module_name = module.__name__
19
20     def _get_overrides(self):
21         reg = get_current_registry()
22         overrides = reg.queryUtility(IPackageOverrides, self.module_name)
23         return overrides
25c64c 24
6da017 25     def get_resource_filename(self, manager, resource_name):
CM 26         """ Return a true filesystem path for resource_name,
27         co-ordinating the extraction with manager, if the resource
28         must be unpacked to the filesystem.
29         """
30         overrides = self._get_overrides()
31         if overrides is not None:
32             filename = overrides.get_filename(resource_name)
33             if filename is not None:
34                 return filename
35         return pkg_resources.DefaultProvider.get_resource_filename(
0c29cf 36             self, manager, resource_name
MM 37         )
25c64c 38
6da017 39     def get_resource_stream(self, manager, resource_name):
CM 40         """ Return a readable file-like object for resource_name."""
41         overrides = self._get_overrides()
42         if overrides is not None:
25c64c 43             stream = overrides.get_stream(resource_name)
6da017 44             if stream is not None:
CM 45                 return stream
46         return pkg_resources.DefaultProvider.get_resource_stream(
0c29cf 47             self, manager, resource_name
MM 48         )
6da017 49
CM 50     def get_resource_string(self, manager, resource_name):
51         """ Return a string containing the contents of resource_name."""
52         overrides = self._get_overrides()
53         if overrides is not None:
54             string = overrides.get_string(resource_name)
55             if string is not None:
56                 return string
57         return pkg_resources.DefaultProvider.get_resource_string(
0c29cf 58             self, manager, resource_name
MM 59         )
6da017 60
CM 61     def has_resource(self, resource_name):
62         overrides = self._get_overrides()
63         if overrides is not None:
64             result = overrides.has_resource(resource_name)
65             if result is not None:
66                 return result
0c29cf 67         return pkg_resources.DefaultProvider.has_resource(self, resource_name)
6da017 68
CM 69     def resource_isdir(self, resource_name):
70         overrides = self._get_overrides()
71         if overrides is not None:
72             result = overrides.isdir(resource_name)
73             if result is not None:
74                 return result
75         return pkg_resources.DefaultProvider.resource_isdir(
0c29cf 76             self, resource_name
MM 77         )
6da017 78
CM 79     def resource_listdir(self, resource_name):
80         overrides = self._get_overrides()
81         if overrides is not None:
82             result = overrides.listdir(resource_name)
83             if result is not None:
84                 return result
85         return pkg_resources.DefaultProvider.resource_listdir(
0c29cf 86             self, resource_name
MM 87         )
407b33 88
MM 89
3b7334 90 @implementer(IPackageOverrides)
bea48e 91 class PackageOverrides(object):
6da017 92     # pkg_resources arg in kw args below for testing
CM 93     def __init__(self, package, pkg_resources=pkg_resources):
bea48e 94         loader = self._real_loader = getattr(package, '__loader__', None)
TS 95         if isinstance(loader, self.__class__):
96             self._real_loader = None
6da017 97         # We register ourselves as a __loader__ *only* to support the
CM 98         # setuptools _find_adapter adapter lookup; this class doesn't
99         # actually support the PEP 302 loader "API".  This is
100         # excusable due to the following statement in the spec:
101         # ... Loader objects are not
102         # required to offer any useful functionality (any such functionality,
103         # such as the zipimport get_data() method mentioned above, is
104         # optional)...
105         # A __loader__ attribute is basically metadata, and setuptools
106         # uses it as such.
407b33 107         package.__loader__ = self
6da017 108         # we call register_loader_type for every instantiation of this
CM 109         # class; that's OK, it's idempotent to do it more than once.
110         pkg_resources.register_loader_type(self.__class__, OverrideProvider)
111         self.overrides = []
112         self.overridden_package_name = package.__name__
113
407b33 114     def insert(self, path, source):
6da017 115         if not path or path.endswith('/'):
407b33 116             override = DirectoryOverride(path, source)
6da017 117         else:
407b33 118             override = FileOverride(path, source)
6da017 119         self.overrides.insert(0, override)
CM 120         return override
121
407b33 122     def filtered_sources(self, resource_name):
6da017 123         for override in self.overrides:
CM 124             o = override(resource_name)
125             if o is not None:
407b33 126                 yield o
6da017 127
CM 128     def get_filename(self, resource_name):
407b33 129         for source, path in self.filtered_sources(resource_name):
MM 130             result = source.get_filename(path)
131             if result is not None:
132                 return result
6da017 133
CM 134     def get_stream(self, resource_name):
407b33 135         for source, path in self.filtered_sources(resource_name):
MM 136             result = source.get_stream(path)
137             if result is not None:
138                 return result
6da017 139
CM 140     def get_string(self, resource_name):
407b33 141         for source, path in self.filtered_sources(resource_name):
MM 142             result = source.get_string(path)
143             if result is not None:
144                 return result
6da017 145
CM 146     def has_resource(self, resource_name):
407b33 147         for source, path in self.filtered_sources(resource_name):
MM 148             if source.exists(path):
6da017 149                 return True
CM 150
151     def isdir(self, resource_name):
407b33 152         for source, path in self.filtered_sources(resource_name):
MM 153             result = source.isdir(path)
154             if result is not None:
155                 return result
6da017 156
CM 157     def listdir(self, resource_name):
407b33 158         for source, path in self.filtered_sources(resource_name):
MM 159             result = source.listdir(path)
160             if result is not None:
161                 return result
bea48e 162
TS 163     @property
164     def real_loader(self):
165         if self._real_loader is None:
166             raise NotImplementedError()
167         return self._real_loader
168
169     def get_data(self, path):
170         """ See IPEP302Loader.
171         """
172         return self.real_loader.get_data(path)
173
174     def is_package(self, fullname):
175         """ See IPEP302Loader.
176         """
177         return self.real_loader.is_package(fullname)
178
179     def get_code(self, fullname):
180         """ See IPEP302Loader.
181         """
182         return self.real_loader.get_code(fullname)
183
184     def get_source(self, fullname):
185         """ See IPEP302Loader.
186         """
187         return self.real_loader.get_source(fullname)
407b33 188
6da017 189
CM 190 class DirectoryOverride:
407b33 191     def __init__(self, path, source):
6da017 192         self.path = path
CM 193         self.pathlen = len(self.path)
407b33 194         self.source = source
6da017 195
CM 196     def __call__(self, resource_name):
197         if resource_name.startswith(self.path):
0c29cf 198             new_path = resource_name[self.pathlen :]
407b33 199             return self.source, new_path
0c29cf 200
6da017 201
CM 202 class FileOverride:
407b33 203     def __init__(self, path, source):
6da017 204         self.path = path
407b33 205         self.source = source
6da017 206
CM 207     def __call__(self, resource_name):
208         if resource_name == self.path:
407b33 209             return self.source, ''
MM 210
211
212 class PackageAssetSource(object):
213     """
214     An asset source relative to a package.
215
216     If this asset source is a file, then we expect the ``prefix`` to point
217     to the new name of the file, and the incoming ``resource_name`` will be
218     the empty string, as returned by the ``FileOverride``.
219
220     """
0c29cf 221
407b33 222     def __init__(self, package, prefix):
MM 223         self.package = package
e51295 224         if hasattr(package, '__name__'):
MA 225             self.pkg_name = package.__name__
226         else:
227             self.pkg_name = package
407b33 228         self.prefix = prefix
MM 229
230     def get_path(self, resource_name):
231         return '%s%s' % (self.prefix, resource_name)
232
233     def get_filename(self, resource_name):
234         path = self.get_path(resource_name)
e51295 235         if pkg_resources.resource_exists(self.pkg_name, path):
MA 236             return pkg_resources.resource_filename(self.pkg_name, path)
407b33 237
MM 238     def get_stream(self, resource_name):
239         path = self.get_path(resource_name)
e51295 240         if pkg_resources.resource_exists(self.pkg_name, path):
MA 241             return pkg_resources.resource_stream(self.pkg_name, path)
407b33 242
MM 243     def get_string(self, resource_name):
244         path = self.get_path(resource_name)
e51295 245         if pkg_resources.resource_exists(self.pkg_name, path):
MA 246             return pkg_resources.resource_string(self.pkg_name, path)
407b33 247
MM 248     def exists(self, resource_name):
249         path = self.get_path(resource_name)
e51295 250         if pkg_resources.resource_exists(self.pkg_name, path):
407b33 251             return True
MM 252
253     def isdir(self, resource_name):
254         path = self.get_path(resource_name)
e51295 255         if pkg_resources.resource_exists(self.pkg_name, path):
MA 256             return pkg_resources.resource_isdir(self.pkg_name, path)
407b33 257
MM 258     def listdir(self, resource_name):
259         path = self.get_path(resource_name)
e51295 260         if pkg_resources.resource_exists(self.pkg_name, path):
MA 261             return pkg_resources.resource_listdir(self.pkg_name, path)
407b33 262
MM 263
264 class FSAssetSource(object):
265     """
266     An asset source relative to a path in the filesystem.
267
268     """
0c29cf 269
407b33 270     def __init__(self, prefix):
MM 271         self.prefix = prefix
272
62222d 273     def get_path(self, resource_name):
407b33 274         if resource_name:
MM 275             path = os.path.join(self.prefix, resource_name)
276         else:
277             path = self.prefix
62222d 278         return path
407b33 279
62222d 280     def get_filename(self, resource_name):
MM 281         path = self.get_path(resource_name)
407b33 282         if os.path.exists(path):
MM 283             return path
284
285     def get_stream(self, resource_name):
286         path = self.get_filename(resource_name)
287         if path is not None:
288             return open(path, 'rb')
289
290     def get_string(self, resource_name):
291         stream = self.get_stream(resource_name)
292         if stream is not None:
293             with stream:
294                 return stream.read()
295
296     def exists(self, resource_name):
297         path = self.get_filename(resource_name)
298         if path is not None:
299             return True
300
301     def isdir(self, resource_name):
302         path = self.get_filename(resource_name)
303         if path is not None:
304             return os.path.isdir(path)
305
306     def listdir(self, resource_name):
307         path = self.get_filename(resource_name)
308         if path is not None:
309             return os.listdir(path)
6da017 310
5bf23f 311
CM 312 class AssetsConfiguratorMixin(object):
0c29cf 313     def _override(
MM 314         self, package, path, override_source, PackageOverrides=PackageOverrides
315     ):
5bf23f 316         pkg_name = package.__name__
CM 317         override = self.registry.queryUtility(IPackageOverrides, name=pkg_name)
318         if override is None:
319             override = PackageOverrides(package)
0c29cf 320             self.registry.registerUtility(
MM 321                 override, IPackageOverrides, name=pkg_name
322             )
407b33 323         override.insert(path, override_source)
5bf23f 324
CM 325     @action_method
326     def override_asset(self, to_override, override_with, _override=None):
327         """ Add a :app:`Pyramid` asset override to the current
328         configuration state.
329
407b33 330         ``to_override`` is an :term:`asset specification` to the
5bf23f 331         asset being overridden.
CM 332
407b33 333         ``override_with`` is an :term:`asset specification` to the
MM 334         asset that is performing the override. This may also be an absolute
335         path.
5bf23f 336
CM 337         See :ref:`assets_chapter` for more
338         information about asset overrides."""
339         if to_override == override_with:
407b33 340             raise ConfigurationError(
0c29cf 341                 'You cannot override an asset with itself'
MM 342             )
5bf23f 343
CM 344         package = to_override
345         path = ''
346         if ':' in to_override:
347             package, path = to_override.split(':', 1)
348
349         # *_isdir = override is package or directory
407b33 350         overridden_isdir = path == '' or path.endswith('/')
MM 351
352         if os.path.isabs(override_with):
353             override_source = FSAssetSource(override_with)
354             if not os.path.exists(override_with):
355                 raise ConfigurationError(
356                     'Cannot override asset with an absolute path that does '
0c29cf 357                     'not exist'
MM 358                 )
407b33 359             override_isdir = os.path.isdir(override_with)
MM 360             override_package = None
361             override_prefix = override_with
362         else:
363             override_package = override_with
364             override_prefix = ''
365             if ':' in override_with:
366                 override_package, override_prefix = override_with.split(':', 1)
367
368             __import__(override_package)
369             to_package = sys.modules[override_package]
370             override_source = PackageAssetSource(to_package, override_prefix)
371
0c29cf 372             override_isdir = override_prefix == '' or override_with.endswith(
MM 373                 '/'
407b33 374             )
5bf23f 375
CM 376         if overridden_isdir and (not override_isdir):
377             raise ConfigurationError(
378                 'A directory cannot be overridden with a file (put a '
0c29cf 379                 'slash at the end of override_with if necessary)'
MM 380             )
5bf23f 381
CM 382         if (not overridden_isdir) and override_isdir:
383             raise ConfigurationError(
384                 'A file cannot be overridden with a directory (put a '
0c29cf 385                 'slash at the end of to_override if necessary)'
MM 386             )
5bf23f 387
0c29cf 388         override = _override or self._override  # test jig
5bf23f 389
CM 390         def register():
391             __import__(package)
392             from_package = sys.modules[package]
407b33 393             override(from_package, path, override_source)
eb2fee 394
3b5ccb 395         intr = self.introspectable(
5e92f3 396             'asset overrides',
8a32e3 397             (package, override_package, path, override_prefix),
58c01f 398             '%s -> %s' % (to_override, override_with),
8a32e3 399             'asset override',
0c29cf 400         )
58c01f 401         intr['to_override'] = to_override
CM 402         intr['override_with'] = override_with
0c29cf 403         self.action(
MM 404             None, register, introspectables=(intr,), order=PHASE1_CONFIG
405         )
5bf23f 406
0c29cf 407     override_resource = override_asset  # bw compat