utils.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. from webassets import six
  2. import contextlib
  3. import os
  4. import sys
  5. import re
  6. from itertools import takewhile
  7. from .exceptions import BundleError
  8. __all__ = ('md5_constructor', 'pickle', 'set', 'StringIO',
  9. 'common_path_prefix', 'working_directory', 'is_url')
  10. if sys.version_info >= (2, 5):
  11. import hashlib
  12. md5_constructor = hashlib.md5
  13. else:
  14. import md5
  15. md5_constructor = md5.new
  16. try:
  17. import cPickle as pickle
  18. except ImportError:
  19. import pickle
  20. try:
  21. set
  22. except NameError:
  23. from sets import Set as set
  24. else:
  25. set = set
  26. from webassets.six import StringIO
  27. try:
  28. from urllib import parse as urlparse
  29. except ImportError: # Python 2
  30. import urlparse
  31. import urllib
  32. def hash_func(data):
  33. from .cache import make_md5
  34. return make_md5(data)
  35. _directory_separator_re = re.compile(r"[/\\]+")
  36. def common_path_prefix(paths, sep=os.path.sep):
  37. """os.path.commonpath() is completely in the wrong place; it's
  38. useless with paths since it only looks at one character at a time,
  39. see http://bugs.python.org/issue10395
  40. This replacement is from:
  41. http://rosettacode.org/wiki/Find_Common_Directory_Path#Python
  42. """
  43. def allnamesequal(name):
  44. return all(n==name[0] for n in name[1:])
  45. # The regex splits the paths on both / and \ characters, whereas the
  46. # rosettacode.org algorithm only uses os.path.sep
  47. bydirectorylevels = zip(*[_directory_separator_re.split(p) for p in paths])
  48. return sep.join(x[0] for x in takewhile(allnamesequal, bydirectorylevels))
  49. @contextlib.contextmanager
  50. def working_directory(directory=None, filename=None):
  51. """A context manager which changes the working directory to the given
  52. path, and then changes it back to its previous value on exit.
  53. Filters will often find this helpful.
  54. Instead of a ``directory``, you may also give a ``filename``, and the
  55. working directory will be set to the directory that file is in.s
  56. """
  57. assert bool(directory) != bool(filename) # xor
  58. if not directory:
  59. directory = os.path.dirname(filename)
  60. prev_cwd = os.getcwd()
  61. os.chdir(directory)
  62. try:
  63. yield
  64. finally:
  65. os.chdir(prev_cwd)
  66. def make_option_resolver(clazz=None, attribute=None, classes=None,
  67. allow_none=True, desc=None):
  68. """Returns a function which can resolve an option to an object.
  69. The option may given as an instance or a class (of ``clazz``, or
  70. duck-typed with an attribute ``attribute``), or a string value referring
  71. to a class as defined by the registry in ``classes``.
  72. This support arguments, so an option may look like this:
  73. cache:/tmp/cachedir
  74. If this must instantiate a class, it will pass such an argument along,
  75. if given. In addition, if the class to be instantiated has a classmethod
  76. ``make()``, this method will be used as a factory, and will be given an
  77. Environment object (if one has been passed to the resolver). This allows
  78. classes that need it to initialize themselves based on an Environment.
  79. """
  80. assert clazz or attribute or classes
  81. desc_string = ' to %s' % desc if desc else None
  82. def instantiate(clazz, env, *a, **kw):
  83. # Create an instance of clazz, via the Factory if one is defined,
  84. # passing along the Environment, or creating the class directly.
  85. if hasattr(clazz, 'make'):
  86. # make() protocol is that if e.g. the get_manifest() resolver takes
  87. # an env, then the first argument of the factory is the env.
  88. args = (env,) + a if env is not None else a
  89. return clazz.make(*args, **kw)
  90. return clazz(*a, **kw)
  91. def resolve_option(option, env=None):
  92. the_clazz = clazz() if callable(clazz) and not isinstance(option, type) else clazz
  93. if not option and allow_none:
  94. return None
  95. # If the value has one of the support attributes (duck-typing).
  96. if attribute and hasattr(option, attribute):
  97. if isinstance(option, type):
  98. return instantiate(option, env)
  99. return option
  100. # If it is the class we support.
  101. if the_clazz and isinstance(option, the_clazz):
  102. return option
  103. elif isinstance(option, type) and issubclass(option, the_clazz):
  104. return instantiate(option, env)
  105. # If it is a string
  106. elif isinstance(option, six.string_types):
  107. parts = option.split(':', 1)
  108. key = parts[0]
  109. arg = parts[1] if len(parts) > 1 else None
  110. if key in classes:
  111. return instantiate(classes[key], env, *([arg] if arg else []))
  112. raise ValueError('%s cannot be resolved%s' % (option, desc_string))
  113. resolve_option.__doc__ = """Resolve ``option``%s.""" % desc_string
  114. return resolve_option
  115. def RegistryMetaclass(clazz=None, attribute=None, allow_none=True, desc=None):
  116. """Returns a metaclass which will keep a registry of all subclasses, keyed
  117. by their ``id`` attribute.
  118. The metaclass will also have a ``resolve`` method which can turn a string
  119. into an instance of one of the classes (based on ``make_option_resolver``).
  120. """
  121. def eq(self, other):
  122. """Return equality with config values that instantiate this."""
  123. return (hasattr(self, 'id') and self.id == other) or\
  124. id(self) == id(other)
  125. def unicode(self):
  126. return "%s" % (self.id if hasattr(self, 'id') else repr(self))
  127. class Metaclass(type):
  128. REGISTRY = {}
  129. def __new__(mcs, name, bases, attrs):
  130. if not '__eq__' in attrs:
  131. attrs['__eq__'] = eq
  132. if not '__unicode__' in attrs:
  133. attrs['__unicode__'] = unicode
  134. if not '__str__' in attrs:
  135. attrs['__str__'] = unicode
  136. new_klass = type.__new__(mcs, name, bases, attrs)
  137. if hasattr(new_klass, 'id'):
  138. mcs.REGISTRY[new_klass.id] = new_klass
  139. return new_klass
  140. resolve = staticmethod(make_option_resolver(
  141. clazz=clazz,
  142. attribute=attribute,
  143. allow_none=allow_none,
  144. desc=desc,
  145. classes=REGISTRY
  146. ))
  147. return Metaclass
  148. def cmp_debug_levels(level1, level2):
  149. """cmp() for debug levels, returns True if ``level1`` is higher
  150. than ``level2``."""
  151. level_ints = {False: 0, 'merge': 1, True: 2}
  152. try:
  153. cmp = lambda a, b: (a > b) - (a < b) # 333
  154. return cmp(level_ints[level1], level_ints[level2])
  155. except KeyError as e:
  156. # Not sure if a dependency on BundleError is proper here. Validating
  157. # debug values should probably be done on assign. But because this
  158. # needs to happen in two places (Environment and Bundle) we do it here.
  159. raise BundleError('Invalid debug value: %s' % e)
  160. def is_url(s):
  161. if not isinstance(s, str):
  162. return False
  163. parsed = urlparse.urlsplit(s)
  164. return bool(parsed.scheme and parsed.netloc) and len(parsed.scheme) > 1