util.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2007 Edgewall Software
  4. # All rights reserved.
  5. #
  6. # This software is licensed as described in the file COPYING, which
  7. # you should have received as part of this distribution. The terms
  8. # are also available at http://babel.edgewall.org/wiki/License.
  9. #
  10. # This software consists of voluntary contributions made by many
  11. # individuals. For the exact contribution history, see the revision
  12. # history and logs, available at http://babel.edgewall.org/log/.
  13. """Various utility classes and functions."""
  14. import codecs
  15. from datetime import timedelta, tzinfo
  16. import os
  17. import re
  18. try:
  19. set = set
  20. except NameError:
  21. from sets import Set as set
  22. import textwrap
  23. import time
  24. from itertools import izip, imap
  25. missing = object()
  26. __all__ = ['distinct', 'pathmatch', 'relpath', 'wraptext', 'odict', 'UTC',
  27. 'LOCALTZ']
  28. __docformat__ = 'restructuredtext en'
  29. def distinct(iterable):
  30. """Yield all items in an iterable collection that are distinct.
  31. Unlike when using sets for a similar effect, the original ordering of the
  32. items in the collection is preserved by this function.
  33. >>> print list(distinct([1, 2, 1, 3, 4, 4]))
  34. [1, 2, 3, 4]
  35. >>> print list(distinct('foobar'))
  36. ['f', 'o', 'b', 'a', 'r']
  37. :param iterable: the iterable collection providing the data
  38. :return: the distinct items in the collection
  39. :rtype: ``iterator``
  40. """
  41. seen = set()
  42. for item in iter(iterable):
  43. if item not in seen:
  44. yield item
  45. seen.add(item)
  46. # Regexp to match python magic encoding line
  47. PYTHON_MAGIC_COMMENT_re = re.compile(
  48. r'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)', re.VERBOSE)
  49. def parse_encoding(fp):
  50. """Deduce the encoding of a source file from magic comment.
  51. It does this in the same way as the `Python interpreter`__
  52. .. __: http://docs.python.org/ref/encodings.html
  53. The ``fp`` argument should be a seekable file object.
  54. (From Jeff Dairiki)
  55. """
  56. pos = fp.tell()
  57. fp.seek(0)
  58. try:
  59. line1 = fp.readline()
  60. has_bom = line1.startswith(codecs.BOM_UTF8)
  61. if has_bom:
  62. line1 = line1[len(codecs.BOM_UTF8):]
  63. m = PYTHON_MAGIC_COMMENT_re.match(line1)
  64. if not m:
  65. try:
  66. import parser
  67. parser.suite(line1)
  68. except (ImportError, SyntaxError):
  69. # Either it's a real syntax error, in which case the source is
  70. # not valid python source, or line2 is a continuation of line1,
  71. # in which case we don't want to scan line2 for a magic
  72. # comment.
  73. pass
  74. else:
  75. line2 = fp.readline()
  76. m = PYTHON_MAGIC_COMMENT_re.match(line2)
  77. if has_bom:
  78. if m:
  79. raise SyntaxError(
  80. "python refuses to compile code with both a UTF8 "
  81. "byte-order-mark and a magic encoding comment")
  82. return 'utf_8'
  83. elif m:
  84. return m.group(1)
  85. else:
  86. return None
  87. finally:
  88. fp.seek(pos)
  89. def pathmatch(pattern, filename):
  90. """Extended pathname pattern matching.
  91. This function is similar to what is provided by the ``fnmatch`` module in
  92. the Python standard library, but:
  93. * can match complete (relative or absolute) path names, and not just file
  94. names, and
  95. * also supports a convenience pattern ("**") to match files at any
  96. directory level.
  97. Examples:
  98. >>> pathmatch('**.py', 'bar.py')
  99. True
  100. >>> pathmatch('**.py', 'foo/bar/baz.py')
  101. True
  102. >>> pathmatch('**.py', 'templates/index.html')
  103. False
  104. >>> pathmatch('**/templates/*.html', 'templates/index.html')
  105. True
  106. >>> pathmatch('**/templates/*.html', 'templates/foo/bar.html')
  107. False
  108. :param pattern: the glob pattern
  109. :param filename: the path name of the file to match against
  110. :return: `True` if the path name matches the pattern, `False` otherwise
  111. :rtype: `bool`
  112. """
  113. symbols = {
  114. '?': '[^/]',
  115. '?/': '[^/]/',
  116. '*': '[^/]+',
  117. '*/': '[^/]+/',
  118. '**/': '(?:.+/)*?',
  119. '**': '(?:.+/)*?[^/]+',
  120. }
  121. buf = []
  122. for idx, part in enumerate(re.split('([?*]+/?)', pattern)):
  123. if idx % 2:
  124. buf.append(symbols[part])
  125. elif part:
  126. buf.append(re.escape(part))
  127. match = re.match(''.join(buf) + '$', filename.replace(os.sep, '/'))
  128. return match is not None
  129. class TextWrapper(textwrap.TextWrapper):
  130. wordsep_re = re.compile(
  131. r'(\s+|' # any whitespace
  132. r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))' # em-dash
  133. )
  134. def wraptext(text, width=70, initial_indent='', subsequent_indent=''):
  135. """Simple wrapper around the ``textwrap.wrap`` function in the standard
  136. library. This version does not wrap lines on hyphens in words.
  137. :param text: the text to wrap
  138. :param width: the maximum line width
  139. :param initial_indent: string that will be prepended to the first line of
  140. wrapped output
  141. :param subsequent_indent: string that will be prepended to all lines save
  142. the first of wrapped output
  143. :return: a list of lines
  144. :rtype: `list`
  145. """
  146. wrapper = TextWrapper(width=width, initial_indent=initial_indent,
  147. subsequent_indent=subsequent_indent,
  148. break_long_words=False)
  149. return wrapper.wrap(text)
  150. class odict(dict):
  151. """Ordered dict implementation.
  152. :see: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747
  153. """
  154. def __init__(self, data=None):
  155. dict.__init__(self, data or {})
  156. self._keys = dict.keys(self)
  157. def __delitem__(self, key):
  158. dict.__delitem__(self, key)
  159. self._keys.remove(key)
  160. def __setitem__(self, key, item):
  161. dict.__setitem__(self, key, item)
  162. if key not in self._keys:
  163. self._keys.append(key)
  164. def __iter__(self):
  165. return iter(self._keys)
  166. iterkeys = __iter__
  167. def clear(self):
  168. dict.clear(self)
  169. self._keys = []
  170. def copy(self):
  171. d = odict()
  172. d.update(self)
  173. return d
  174. def items(self):
  175. return zip(self._keys, self.values())
  176. def iteritems(self):
  177. return izip(self._keys, self.itervalues())
  178. def keys(self):
  179. return self._keys[:]
  180. def pop(self, key, default=missing):
  181. if default is missing:
  182. return dict.pop(self, key)
  183. elif key not in self:
  184. return default
  185. self._keys.remove(key)
  186. return dict.pop(self, key, default)
  187. def popitem(self, key):
  188. self._keys.remove(key)
  189. return dict.popitem(key)
  190. def setdefault(self, key, failobj = None):
  191. dict.setdefault(self, key, failobj)
  192. if key not in self._keys:
  193. self._keys.append(key)
  194. def update(self, dict):
  195. for (key, val) in dict.items():
  196. self[key] = val
  197. def values(self):
  198. return map(self.get, self._keys)
  199. def itervalues(self):
  200. return imap(self.get, self._keys)
  201. try:
  202. relpath = os.path.relpath
  203. except AttributeError:
  204. def relpath(path, start='.'):
  205. """Compute the relative path to one path from another.
  206. >>> relpath('foo/bar.txt', '').replace(os.sep, '/')
  207. 'foo/bar.txt'
  208. >>> relpath('foo/bar.txt', 'foo').replace(os.sep, '/')
  209. 'bar.txt'
  210. >>> relpath('foo/bar.txt', 'baz').replace(os.sep, '/')
  211. '../foo/bar.txt'
  212. :return: the relative path
  213. :rtype: `basestring`
  214. """
  215. start_list = os.path.abspath(start).split(os.sep)
  216. path_list = os.path.abspath(path).split(os.sep)
  217. # Work out how much of the filepath is shared by start and path.
  218. i = len(os.path.commonprefix([start_list, path_list]))
  219. rel_list = [os.path.pardir] * (len(start_list) - i) + path_list[i:]
  220. return os.path.join(*rel_list)
  221. try:
  222. from operator import attrgetter, itemgetter
  223. except ImportError:
  224. def itemgetter(name):
  225. def _getitem(obj):
  226. return obj[name]
  227. return _getitem
  228. try:
  229. ''.rsplit
  230. def rsplit(a_string, sep=None, maxsplit=None):
  231. return a_string.rsplit(sep, maxsplit)
  232. except AttributeError:
  233. def rsplit(a_string, sep=None, maxsplit=None):
  234. parts = a_string.split(sep)
  235. if maxsplit is None or len(parts) <= maxsplit:
  236. return parts
  237. maxsplit_index = len(parts) - maxsplit
  238. non_splitted_part = sep.join(parts[:maxsplit_index])
  239. splitted = parts[maxsplit_index:]
  240. return [non_splitted_part] + splitted
  241. ZERO = timedelta(0)
  242. class FixedOffsetTimezone(tzinfo):
  243. """Fixed offset in minutes east from UTC."""
  244. def __init__(self, offset, name=None):
  245. self._offset = timedelta(minutes=offset)
  246. if name is None:
  247. name = 'Etc/GMT+%d' % offset
  248. self.zone = name
  249. def __str__(self):
  250. return self.zone
  251. def __repr__(self):
  252. return '<FixedOffset "%s" %s>' % (self.zone, self._offset)
  253. def utcoffset(self, dt):
  254. return self._offset
  255. def tzname(self, dt):
  256. return self.zone
  257. def dst(self, dt):
  258. return ZERO
  259. try:
  260. from pytz import UTC
  261. except ImportError:
  262. UTC = FixedOffsetTimezone(0, 'UTC')
  263. """`tzinfo` object for UTC (Universal Time).
  264. :type: `tzinfo`
  265. """
  266. STDOFFSET = timedelta(seconds = -time.timezone)
  267. if time.daylight:
  268. DSTOFFSET = timedelta(seconds = -time.altzone)
  269. else:
  270. DSTOFFSET = STDOFFSET
  271. DSTDIFF = DSTOFFSET - STDOFFSET
  272. class LocalTimezone(tzinfo):
  273. def utcoffset(self, dt):
  274. if self._isdst(dt):
  275. return DSTOFFSET
  276. else:
  277. return STDOFFSET
  278. def dst(self, dt):
  279. if self._isdst(dt):
  280. return DSTDIFF
  281. else:
  282. return ZERO
  283. def tzname(self, dt):
  284. return time.tzname[self._isdst(dt)]
  285. def _isdst(self, dt):
  286. tt = (dt.year, dt.month, dt.day,
  287. dt.hour, dt.minute, dt.second,
  288. dt.weekday(), 0, -1)
  289. stamp = time.mktime(tt)
  290. tt = time.localtime(stamp)
  291. return tt.tm_isdst > 0
  292. LOCALTZ = LocalTimezone()
  293. """`tzinfo` object for local time-zone.
  294. :type: `tzinfo`
  295. """