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