Michael Merickel
2018-10-17 e14661417e7ceb50d5cf83bbd6abd6b133e473ba
commit | author | age
434c05 1 import os
d9a76e 2 import pkg_resources
423b85 3 import sys
3c2ed7 4 import imp
CM 5
56df90 6 from zope.interface import implementer
CM 7
8 from pyramid.interfaces import IAssetDescriptor
9
10 from pyramid.compat import string_types
11
0c29cf 12 ignore_types = [imp.C_EXTENSION, imp.C_BUILTIN]
MM 13 init_names = [
14     '__init__%s' % x[0]
15     for x in imp.get_suffixes()
16     if x[0] and x[2] not in ignore_types
17 ]
18
434c05 19
d9a76e 20 def caller_path(path, level=2):
434c05 21     if not os.path.isabs(path):
25c64c 22         module = caller_module(level + 1)
d9a76e 23         prefix = package_path(module)
434c05 24         path = os.path.join(prefix, path)
CM 25     return path
26
0c29cf 27
a56564 28 def caller_module(level=2, sys=sys):
d9a76e 29     module_globals = sys._getframe(level).f_globals
a56564 30     module_name = module_globals.get('__name__') or '__main__'
d9a76e 31     module = sys.modules[module_name]
CM 32     return module
0c29cf 33
434c05 34
6efd81 35 def package_name(pkg_or_module):
CM 36     """ If this function is passed a module, return the dotted Python
37     package name of the package in which the module lives.  If this
38     function is passed a package, return the dotted Python package
39     name of the package itself."""
d79d13 40     if pkg_or_module is None or pkg_or_module.__name__ == '__main__':
45d08c 41         return '__main__'
6efd81 42     pkg_name = pkg_or_module.__name__
c062d5 43     pkg_filename = getattr(pkg_or_module, '__file__', None)
DH 44     if pkg_filename is None:
45         # Namespace packages do not have __init__.py* files,
46         # and so have no __file__ attribute
47         return pkg_name
6efd81 48     splitted = os.path.split(pkg_filename)
3c2ed7 49     if splitted[-1] in init_names:
6efd81 50         # it's a package
CM 51         return pkg_name
52     return pkg_name.rsplit('.', 1)[0]
53
0c29cf 54
39480c 55 def package_of(pkg_or_module):
CM 56     """ Return the package of a module or return the package itself """
57     pkg_name = package_name(pkg_or_module)
58     __import__(pkg_name)
59     return sys.modules[pkg_name]
60
0c29cf 61
5f4b80 62 def caller_package(level=2, caller_module=caller_module):
CM 63     # caller_module in arglist for tests
25c64c 64     module = caller_module(level + 1)
bfee0a 65     f = getattr(module, '__file__', '')
0c29cf 66     if ('__init__.py' in f) or ('__init__$py' in f):  # empty at >>>
5f4b80 67         # Module is a package
CM 68         return module
69     # Go up one level to get package
70     package_name = module.__name__.rsplit('.', 1)[0]
71     return sys.modules[package_name]
0c29cf 72
5f4b80 73
d9a76e 74 def package_path(package):
CM 75     # computing the abspath is actually kinda expensive so we memoize
76     # the result
cba2e1 77     prefix = getattr(package, '__abspath__', None)
d9a76e 78     if prefix is None:
CM 79         prefix = pkg_resources.resource_filename(package.__name__, '')
80         # pkg_resources doesn't care whether we feed it a package
81         # name or a module name within the package, the result
82         # will be the same: a directory name to the package itself
83         try:
cba2e1 84             package.__abspath__ = prefix
87b7b7 85         except Exception:
d9a76e 86             # this is only an optimization, ignore any error
CM 87             pass
88     return prefix
600ea3 89
0c29cf 90
078859 91 class _CALLER_PACKAGE(object):
0c29cf 92     def __repr__(self):  # pragma: no cover (for docs)
078859 93         return 'pyramid.path.CALLER_PACKAGE'
CM 94
0c29cf 95
078859 96 CALLER_PACKAGE = _CALLER_PACKAGE()
0c29cf 97
078859 98
56df90 99 class Resolver(object):
078859 100     def __init__(self, package=CALLER_PACKAGE):
CM 101         if package in (None, CALLER_PACKAGE):
102             self.package = package
56df90 103         else:
CM 104             if isinstance(package, string_types):
105                 try:
106                     __import__(package)
107                 except ImportError:
108                     raise ValueError(
109                         'The dotted name %r cannot be imported' % (package,)
0c29cf 110                     )
56df90 111                 package = sys.modules[package]
CM 112             self.package = package_of(package)
078859 113
CM 114     def get_package_name(self):
115         if self.package is CALLER_PACKAGE:
116             package_name = caller_package().__name__
117         else:
118             package_name = self.package.__name__
119         return package_name
120
121     def get_package(self):
122         if self.package is CALLER_PACKAGE:
123             package = caller_package()
124         else:
125             package = self.package
126         return package
127
56df90 128
CM 129 class AssetResolver(Resolver):
130     """ A class used to resolve an :term:`asset specification` to an
131     :term:`asset descriptor`.
132
d633c8 133     .. versionadded:: 1.3
56df90 134
CM 135     The constructor accepts a single argument named ``package`` which may be
136     any of:
137
138     - A fully qualified (not relative) dotted name to a module or package
139
140     - a Python module or package object
141
142     - The value ``None``
143
078859 144     - The constant value :attr:`pyramid.path.CALLER_PACKAGE`.
CM 145
146     The default value is :attr:`pyramid.path.CALLER_PACKAGE`.
147
56df90 148     The ``package`` is used when a relative asset specification is supplied
CM 149     to the :meth:`~pyramid.path.AssetResolver.resolve` method.  An asset
150     specification without a colon in it is treated as relative.
151
eb81eb 152     If ``package`` is ``None``, the resolver will
56df90 153     only be able to resolve fully qualified (not relative) asset
CM 154     specifications.  Any attempt to resolve a relative asset specification
eb81eb 155     will result in an :exc:`ValueError` exception.
078859 156
eb81eb 157     If ``package`` is :attr:`pyramid.path.CALLER_PACKAGE`,
TL 158     the resolver will treat relative asset specifications as
078859 159     relative to the caller of the :meth:`~pyramid.path.AssetResolver.resolve`
CM 160     method.
56df90 161
eb81eb 162     If ``package`` is a *module* or *module name* (as opposed to a package or
TL 163     package name), its containing package is computed and this
164     package is used to derive the package name (all names are resolved relative
56df90 165     to packages, never to modules).  For example, if the ``package`` argument
CM 166     to this type was passed the string ``xml.dom.expatbuilder``, and
167     ``template.pt`` is supplied to the
168     :meth:`~pyramid.path.AssetResolver.resolve` method, the resulting absolute
169     asset spec would be ``xml.minidom:template.pt``, because
170     ``xml.dom.expatbuilder`` is a module object, not a package object.
171
eb81eb 172     If ``package`` is a *package* or *package name* (as opposed to a module or
TL 173     module name), this package will be used to compute relative
56df90 174     asset specifications.  For example, if the ``package`` argument to this
CM 175     type was passed the string ``xml.dom``, and ``template.pt`` is supplied
176     to the :meth:`~pyramid.path.AssetResolver.resolve` method, the resulting
177     absolute asset spec would be ``xml.minidom:template.pt``.
178     """
0c29cf 179
56df90 180     def resolve(self, spec):
CM 181         """
182         Resolve the asset spec named as ``spec`` to an object that has the
183         attributes and methods described in
18659c 184         :class:`pyramid.interfaces.IAssetDescriptor`.
56df90 185
CM 186         If ``spec`` is an absolute filename
187         (e.g. ``/path/to/myproject/templates/foo.pt``) or an absolute asset
188         spec (e.g. ``myproject:templates.foo.pt``), an asset descriptor is
189         returned without taking into account the ``package`` passed to this
190         class' constructor.
191
192         If ``spec`` is a *relative* asset specification (an asset
193         specification without a ``:`` in it, e.g. ``templates/foo.pt``), the
08c221 194         ``package`` argument of the constructor is used as the package
56df90 195         portion of the asset spec.  For example:
CM 196
197         .. code-block:: python
198
199            a = AssetResolver('myproject')
200            resolver = a.resolve('templates/foo.pt')
edfc4f 201            print(resolver.abspath())
56df90 202            # -> /path/to/myproject/templates/foo.pt
CM 203
078859 204         If the AssetResolver is constructed without a ``package`` argument of
CM 205         ``None``, and a relative asset specification is passed to
206         ``resolve``, an :exc:`ValueError` exception is raised.
56df90 207         """
CM 208         if os.path.isabs(spec):
209             return FSAssetDescriptor(spec)
210         path = spec
211         if ':' in path:
078859 212             package_name, path = spec.split(':', 1)
56df90 213         else:
078859 214             if self.package is CALLER_PACKAGE:
CM 215                 package_name = caller_package().__name__
216             else:
217                 package_name = getattr(self.package, '__name__', None)
218             if package_name is None:
56df90 219                 raise ValueError(
CM 220                     'relative spec %r irresolveable without package' % (spec,)
221                 )
078859 222         return PkgResourcesAssetDescriptor(package_name, path)
0c29cf 223
56df90 224
CM 225 class DottedNameResolver(Resolver):
226     """ A class used to resolve a :term:`dotted Python name` to a package or
227     module object.
228
d633c8 229     .. versionadded:: 1.3
56df90 230
CM 231     The constructor accepts a single argument named ``package`` which may be
232     any of:
233
234     - A fully qualified (not relative) dotted name to a module or package
235
236     - a Python module or package object
237
238     - The value ``None``
239
078859 240     - The constant value :attr:`pyramid.path.CALLER_PACKAGE`.
CM 241
242     The default value is :attr:`pyramid.path.CALLER_PACKAGE`.
243
56df90 244     The ``package`` is used when a relative dotted name is supplied to the
CM 245     :meth:`~pyramid.path.DottedNameResolver.resolve` method.  A dotted name
246     which has a ``.`` (dot) or ``:`` (colon) as its first character is
247     treated as relative.
248
eb81eb 249     If ``package`` is ``None``, the resolver will only be able to resolve
TL 250     fully qualified (not relative) names.  Any attempt to resolve a
251     relative name will result in an :exc:`ValueError` exception.
078859 252
eb81eb 253     If ``package`` is :attr:`pyramid.path.CALLER_PACKAGE`,
TL 254     the resolver will treat relative dotted names as relative to
078859 255     the caller of the :meth:`~pyramid.path.DottedNameResolver.resolve`
CM 256     method.
56df90 257
eb81eb 258     If ``package`` is a *module* or *module name* (as opposed to a package or
TL 259     package name), its containing package is computed and this
56df90 260     package used to derive the package name (all names are resolved relative
CM 261     to packages, never to modules).  For example, if the ``package`` argument
262     to this type was passed the string ``xml.dom.expatbuilder``, and
263     ``.mindom`` is supplied to the
264     :meth:`~pyramid.path.DottedNameResolver.resolve` method, the resulting
265     import would be for ``xml.minidom``, because ``xml.dom.expatbuilder`` is
266     a module object, not a package object.
267
eb81eb 268     If ``package`` is a *package* or *package name* (as opposed to a module or
TL 269     module name), this package will be used to relative compute
56df90 270     dotted names.  For example, if the ``package`` argument to this type was
CM 271     passed the string ``xml.dom``, and ``.minidom`` is supplied to the
272     :meth:`~pyramid.path.DottedNameResolver.resolve` method, the resulting
273     import would be for ``xml.minidom``.
274     """
0c29cf 275
078859 276     def resolve(self, dotted):
56df90 277         """
CM 278         This method resolves a dotted name reference to a global Python
279         object (an object which can be imported) to the object itself.
280
281         Two dotted name styles are supported:
282
283         - ``pkg_resources``-style dotted names where non-module attributes
284           of a package are separated from the rest of the path using a ``:``
285           e.g. ``package.module:attr``.
286
287         - ``zope.dottedname``-style dotted names where non-module
288           attributes of a package are separated from the rest of the path
289           using a ``.`` e.g. ``package.module.attr``.
290
291         These styles can be used interchangeably.  If the supplied name
292         contains a ``:`` (colon), the ``pkg_resources`` resolution
293         mechanism will be chosen, otherwise the ``zope.dottedname``
294         resolution mechanism will be chosen.
295
078859 296         If the ``dotted`` argument passed to this method is not a string, a
56df90 297         :exc:`ValueError` will be raised.
078859 298
CM 299         When a dotted name cannot be resolved, a :exc:`ValueError` error is
300         raised.
301
302         Example:
303
304         .. code-block:: python
305
306            r = DottedNameResolver()
307            v = r.resolve('xml') # v is the xml module
308
56df90 309         """
078859 310         if not isinstance(dotted, string_types):
CM 311             raise ValueError('%r is not a string' % (dotted,))
312         package = self.package
313         if package is CALLER_PACKAGE:
314             package = caller_package()
315         return self._resolve(dotted, package)
56df90 316
CM 317     def maybe_resolve(self, dotted):
318         """
319         This method behaves just like
320         :meth:`~pyramid.path.DottedNameResolver.resolve`, except if the
078859 321         ``dotted`` value passed is not a string, it is simply returned.  For
56df90 322         example:
CM 323
324         .. code-block:: python
325
326            import xml
327            r = DottedNameResolver()
078859 328            v = r.maybe_resolve(xml)
56df90 329            # v is the xml module; no exception raised
CM 330         """
331         if isinstance(dotted, string_types):
078859 332             package = self.package
CM 333             if package is CALLER_PACKAGE:
334                 package = caller_package()
335             return self._resolve(dotted, package)
56df90 336         return dotted
CM 337
078859 338     def _resolve(self, dotted, package):
CM 339         if ':' in dotted:
340             return self._pkg_resources_style(dotted, package)
341         else:
342             return self._zope_dottedname_style(dotted, package)
56df90 343
078859 344     def _pkg_resources_style(self, value, package):
56df90 345         """ package.module:attr style """
0f9823 346         if value.startswith(('.', ':')):
078859 347             if not package:
56df90 348                 raise ValueError(
078859 349                     'relative name %r irresolveable without package' % (value,)
0c29cf 350                 )
56df90 351             if value in ['.', ':']:
078859 352                 value = package.__name__
56df90 353             else:
078859 354                 value = package.__name__ + value
e30c3b 355         # Calling EntryPoint.load with an argument is deprecated.
JD 356         # See https://pythonhosted.org/setuptools/history.html#id8
357         ep = pkg_resources.EntryPoint.parse('x=%s' % value)
358         if hasattr(ep, 'resolve'):
c04115 359             # setuptools>=10.2
JD 360             return ep.resolve()  # pragma: NO COVER
e30c3b 361         else:
c04115 362             return ep.load(False)  # pragma: NO COVER
56df90 363
078859 364     def _zope_dottedname_style(self, value, package):
56df90 365         """ package.module.attr style """
0c29cf 366         module = getattr(package, '__name__', None)  # package may be None
56df90 367         if not module:
CM 368             module = None
369         if value == '.':
370             if module is None:
371                 raise ValueError(
372                     'relative name %r irresolveable without package' % (value,)
373                 )
374             name = module.split('.')
375         else:
376             name = value.split('.')
377             if not name[0]:
378                 if module is None:
379                     raise ValueError(
380                         'relative name %r irresolveable without '
381                         'package' % (value,)
0c29cf 382                     )
56df90 383                 module = module.split('.')
CM 384                 name.pop(0)
385                 while not name[0]:
386                     module.pop()
387                     name.pop(0)
388                 name = module + name
389
390         used = name.pop(0)
391         found = __import__(used)
392         for n in name:
393             used += '.' + n
394             try:
395                 found = getattr(found, n)
396             except AttributeError:
397                 __import__(used)
0c29cf 398                 found = getattr(found, n)  # pragma: no cover
56df90 399
CM 400         return found
0c29cf 401
56df90 402
CM 403 @implementer(IAssetDescriptor)
404 class PkgResourcesAssetDescriptor(object):
405     pkg_resources = pkg_resources
406
407     def __init__(self, pkg_name, path):
408         self.pkg_name = pkg_name
409         self.path = path
410
411     def absspec(self):
412         return '%s:%s' % (self.pkg_name, self.path)
413
414     def abspath(self):
f7171c 415         return os.path.abspath(
0c29cf 416             self.pkg_resources.resource_filename(self.pkg_name, self.path)
MM 417         )
56df90 418
CM 419     def stream(self):
420         return self.pkg_resources.resource_stream(self.pkg_name, self.path)
421
422     def isdir(self):
423         return self.pkg_resources.resource_isdir(self.pkg_name, self.path)
424
425     def listdir(self):
426         return self.pkg_resources.resource_listdir(self.pkg_name, self.path)
427
428     def exists(self):
429         return self.pkg_resources.resource_exists(self.pkg_name, self.path)
430
0c29cf 431
56df90 432 @implementer(IAssetDescriptor)
CM 433 class FSAssetDescriptor(object):
434     def __init__(self, path):
435         self.path = os.path.abspath(path)
436
437     def absspec(self):
438         raise NotImplementedError
439
440     def abspath(self):
441         return self.path
442
443     def stream(self):
444         return open(self.path, 'rb')
445
446     def isdir(self):
447         return os.path.isdir(self.path)
448
449     def listdir(self):
450         return os.listdir(self.path)
451
452     def exists(self):
453         return os.path.exists(self.path)