util.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. # -*- coding: utf-8 -*-
  2. """
  3. babel.util
  4. ~~~~~~~~~~
  5. Various utility classes and functions.
  6. :copyright: (c) 2013 by the Babel Team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. import codecs
  10. from datetime import timedelta, tzinfo
  11. import os
  12. import re
  13. import textwrap
  14. from babel._compat import izip, imap
  15. import pytz as _pytz
  16. from babel import localtime
  17. missing = object()
  18. def distinct(iterable):
  19. """Yield all items in an iterable collection that are distinct.
  20. Unlike when using sets for a similar effect, the original ordering of the
  21. items in the collection is preserved by this function.
  22. >>> print(list(distinct([1, 2, 1, 3, 4, 4])))
  23. [1, 2, 3, 4]
  24. >>> print(list(distinct('foobar')))
  25. ['f', 'o', 'b', 'a', 'r']
  26. :param iterable: the iterable collection providing the data
  27. """
  28. seen = set()
  29. for item in iter(iterable):
  30. if item not in seen:
  31. yield item
  32. seen.add(item)
  33. # Regexp to match python magic encoding line
  34. PYTHON_MAGIC_COMMENT_re = re.compile(
  35. br'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)', re.VERBOSE)
  36. def parse_encoding(fp):
  37. """Deduce the encoding of a source file from magic comment.
  38. It does this in the same way as the `Python interpreter`__
  39. .. __: https://docs.python.org/3.4/reference/lexical_analysis.html#encoding-declarations
  40. The ``fp`` argument should be a seekable file object.
  41. (From Jeff Dairiki)
  42. """
  43. pos = fp.tell()
  44. fp.seek(0)
  45. try:
  46. line1 = fp.readline()
  47. has_bom = line1.startswith(codecs.BOM_UTF8)
  48. if has_bom:
  49. line1 = line1[len(codecs.BOM_UTF8):]
  50. m = PYTHON_MAGIC_COMMENT_re.match(line1)
  51. if not m:
  52. try:
  53. import parser
  54. parser.suite(line1.decode('latin-1'))
  55. except (ImportError, SyntaxError, UnicodeEncodeError):
  56. # Either it's a real syntax error, in which case the source is
  57. # not valid python source, or line2 is a continuation of line1,
  58. # in which case we don't want to scan line2 for a magic
  59. # comment.
  60. pass
  61. else:
  62. line2 = fp.readline()
  63. m = PYTHON_MAGIC_COMMENT_re.match(line2)
  64. if has_bom:
  65. if m:
  66. magic_comment_encoding = m.group(1).decode('latin-1')
  67. if magic_comment_encoding != 'utf-8':
  68. raise SyntaxError(
  69. 'encoding problem: {0} with BOM'.format(
  70. magic_comment_encoding))
  71. return 'utf-8'
  72. elif m:
  73. return m.group(1).decode('latin-1')
  74. else:
  75. return None
  76. finally:
  77. fp.seek(pos)
  78. PYTHON_FUTURE_IMPORT_re = re.compile(
  79. r'from\s+__future__\s+import\s+\(*(.+)\)*')
  80. def parse_future_flags(fp, encoding='latin-1'):
  81. """Parse the compiler flags by :mod:`__future__` from the given Python
  82. code.
  83. """
  84. import __future__
  85. pos = fp.tell()
  86. fp.seek(0)
  87. flags = 0
  88. try:
  89. body = fp.read().decode(encoding)
  90. # Fix up the source to be (hopefully) parsable by regexpen.
  91. # This will likely do untoward things if the source code itself is broken.
  92. # (1) Fix `import (\n...` to be `import (...`.
  93. body = re.sub(r'import\s*\([\r\n]+', 'import (', body)
  94. # (2) Join line-ending commas with the next line.
  95. body = re.sub(r',\s*[\r\n]+', ', ', body)
  96. # (3) Remove backslash line continuations.
  97. body = re.sub(r'\\\s*[\r\n]+', ' ', body)
  98. for m in PYTHON_FUTURE_IMPORT_re.finditer(body):
  99. names = [x.strip().strip('()') for x in m.group(1).split(',')]
  100. for name in names:
  101. feature = getattr(__future__, name, None)
  102. if feature:
  103. flags |= feature.compiler_flag
  104. finally:
  105. fp.seek(pos)
  106. return flags
  107. def pathmatch(pattern, filename):
  108. """Extended pathname pattern matching.
  109. This function is similar to what is provided by the ``fnmatch`` module in
  110. the Python standard library, but:
  111. * can match complete (relative or absolute) path names, and not just file
  112. names, and
  113. * also supports a convenience pattern ("**") to match files at any
  114. directory level.
  115. Examples:
  116. >>> pathmatch('**.py', 'bar.py')
  117. True
  118. >>> pathmatch('**.py', 'foo/bar/baz.py')
  119. True
  120. >>> pathmatch('**.py', 'templates/index.html')
  121. False
  122. >>> pathmatch('**/templates/*.html', 'templates/index.html')
  123. True
  124. >>> pathmatch('**/templates/*.html', 'templates/foo/bar.html')
  125. False
  126. :param pattern: the glob pattern
  127. :param filename: the path name of the file to match against
  128. """
  129. symbols = {
  130. '?': '[^/]',
  131. '?/': '[^/]/',
  132. '*': '[^/]+',
  133. '*/': '[^/]+/',
  134. '**/': '(?:.+/)*?',
  135. '**': '(?:.+/)*?[^/]+',
  136. }
  137. buf = []
  138. for idx, part in enumerate(re.split('([?*]+/?)', pattern)):
  139. if idx % 2:
  140. buf.append(symbols[part])
  141. elif part:
  142. buf.append(re.escape(part))
  143. match = re.match(''.join(buf) + '$', filename.replace(os.sep, '/'))
  144. return match is not None
  145. class TextWrapper(textwrap.TextWrapper):
  146. wordsep_re = re.compile(
  147. r'(\s+|' # any whitespace
  148. r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))' # em-dash
  149. )
  150. def wraptext(text, width=70, initial_indent='', subsequent_indent=''):
  151. """Simple wrapper around the ``textwrap.wrap`` function in the standard
  152. library. This version does not wrap lines on hyphens in words.
  153. :param text: the text to wrap
  154. :param width: the maximum line width
  155. :param initial_indent: string that will be prepended to the first line of
  156. wrapped output
  157. :param subsequent_indent: string that will be prepended to all lines save
  158. the first of wrapped output
  159. """
  160. wrapper = TextWrapper(width=width, initial_indent=initial_indent,
  161. subsequent_indent=subsequent_indent,
  162. break_long_words=False)
  163. return wrapper.wrap(text)
  164. class odict(dict):
  165. """Ordered dict implementation.
  166. :see: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747
  167. """
  168. def __init__(self, data=None):
  169. dict.__init__(self, data or {})
  170. self._keys = list(dict.keys(self))
  171. def __delitem__(self, key):
  172. dict.__delitem__(self, key)
  173. self._keys.remove(key)
  174. def __setitem__(self, key, item):
  175. new_key = key not in self
  176. dict.__setitem__(self, key, item)
  177. if new_key:
  178. self._keys.append(key)
  179. def __iter__(self):
  180. return iter(self._keys)
  181. iterkeys = __iter__
  182. def clear(self):
  183. dict.clear(self)
  184. self._keys = []
  185. def copy(self):
  186. d = odict()
  187. d.update(self)
  188. return d
  189. def items(self):
  190. return zip(self._keys, self.values())
  191. def iteritems(self):
  192. return izip(self._keys, self.itervalues())
  193. def keys(self):
  194. return self._keys[:]
  195. def pop(self, key, default=missing):
  196. try:
  197. value = dict.pop(self, key)
  198. self._keys.remove(key)
  199. return value
  200. except KeyError as e:
  201. if default == missing:
  202. raise e
  203. else:
  204. return default
  205. def popitem(self, key):
  206. self._keys.remove(key)
  207. return dict.popitem(key)
  208. def setdefault(self, key, failobj=None):
  209. dict.setdefault(self, key, failobj)
  210. if key not in self._keys:
  211. self._keys.append(key)
  212. def update(self, dict):
  213. for (key, val) in dict.items():
  214. self[key] = val
  215. def values(self):
  216. return map(self.get, self._keys)
  217. def itervalues(self):
  218. return imap(self.get, self._keys)
  219. class FixedOffsetTimezone(tzinfo):
  220. """Fixed offset in minutes east from UTC."""
  221. def __init__(self, offset, name=None):
  222. self._offset = timedelta(minutes=offset)
  223. if name is None:
  224. name = 'Etc/GMT%+d' % offset
  225. self.zone = name
  226. def __str__(self):
  227. return self.zone
  228. def __repr__(self):
  229. return '<FixedOffset "%s" %s>' % (self.zone, self._offset)
  230. def utcoffset(self, dt):
  231. return self._offset
  232. def tzname(self, dt):
  233. return self.zone
  234. def dst(self, dt):
  235. return ZERO
  236. # Export the localtime functionality here because that's
  237. # where it was in the past.
  238. UTC = _pytz.utc
  239. LOCALTZ = localtime.LOCALTZ
  240. get_localzone = localtime.get_localzone
  241. STDOFFSET = localtime.STDOFFSET
  242. DSTOFFSET = localtime.DSTOFFSET
  243. DSTDIFF = localtime.DSTDIFF
  244. ZERO = localtime.ZERO