cmlenz@1: # -*- coding: utf-8 -*- cmlenz@1: # cmlenz@1: # Copyright (C) 2007 Edgewall Software cmlenz@1: # All rights reserved. cmlenz@1: # cmlenz@1: # This software is licensed as described in the file COPYING, which cmlenz@1: # you should have received as part of this distribution. The terms cmlenz@1: # are also available at http://babel.edgewall.org/wiki/License. cmlenz@1: # cmlenz@1: # This software consists of voluntary contributions made by many cmlenz@1: # individuals. For the exact contribution history, see the revision cmlenz@1: # history and logs, available at http://babel.edgewall.org/log/. cmlenz@1: cmlenz@1: """Various utility classes and functions.""" cmlenz@1: pjenvey@164: import codecs cmlenz@30: from datetime import timedelta, tzinfo cmlenz@1: import os cmlenz@1: import re cmlenz@227: try: fschwarz@507: set = set cmlenz@227: except NameError: cmlenz@227: from sets import Set as set cmlenz@316: import textwrap cmlenz@95: import time cmlenz@346: from itertools import izip, imap cmlenz@346: missing = object() cmlenz@1: cmlenz@316: __all__ = ['distinct', 'pathmatch', 'relpath', 'wraptext', 'odict', 'UTC', cmlenz@316: 'LOCALTZ'] cmlenz@1: __docformat__ = 'restructuredtext en' cmlenz@1: cmlenz@369: cmlenz@227: def distinct(iterable): cmlenz@227: """Yield all items in an iterable collection that are distinct. cmlenz@227: cmlenz@227: Unlike when using sets for a similar effect, the original ordering of the cmlenz@227: items in the collection is preserved by this function. cmlenz@227: cmlenz@227: >>> print list(distinct([1, 2, 1, 3, 4, 4])) cmlenz@227: [1, 2, 3, 4] cmlenz@227: >>> print list(distinct('foobar')) cmlenz@227: ['f', 'o', 'b', 'a', 'r'] cmlenz@227: cmlenz@227: :param iterable: the iterable collection providing the data cmlenz@227: :return: the distinct items in the collection cmlenz@227: :rtype: ``iterator`` cmlenz@227: """ cmlenz@227: seen = set() cmlenz@227: for item in iter(iterable): cmlenz@227: if item not in seen: cmlenz@227: yield item cmlenz@227: seen.add(item) cmlenz@227: pjenvey@164: # Regexp to match python magic encoding line pjenvey@164: PYTHON_MAGIC_COMMENT_re = re.compile( pjenvey@164: r'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)', re.VERBOSE) pjenvey@164: def parse_encoding(fp): pjenvey@164: """Deduce the encoding of a source file from magic comment. pjenvey@164: pjenvey@164: It does this in the same way as the `Python interpreter`__ pjenvey@164: pjenvey@164: .. __: http://docs.python.org/ref/encodings.html pjenvey@164: pjenvey@164: The ``fp`` argument should be a seekable file object. pjenvey@164: pjenvey@164: (From Jeff Dairiki) pjenvey@164: """ pjenvey@164: pos = fp.tell() pjenvey@164: fp.seek(0) pjenvey@164: try: pjenvey@164: line1 = fp.readline() pjenvey@164: has_bom = line1.startswith(codecs.BOM_UTF8) pjenvey@164: if has_bom: pjenvey@164: line1 = line1[len(codecs.BOM_UTF8):] pjenvey@164: pjenvey@164: m = PYTHON_MAGIC_COMMENT_re.match(line1) pjenvey@164: if not m: pjenvey@164: try: cmlenz@346: import parser pjenvey@164: parser.suite(line1) cmlenz@346: except (ImportError, SyntaxError): pjenvey@164: # Either it's a real syntax error, in which case the source is pjenvey@164: # not valid python source, or line2 is a continuation of line1, pjenvey@164: # in which case we don't want to scan line2 for a magic pjenvey@164: # comment. pjenvey@164: pass pjenvey@164: else: pjenvey@164: line2 = fp.readline() pjenvey@164: m = PYTHON_MAGIC_COMMENT_re.match(line2) pjenvey@164: pjenvey@164: if has_bom: pjenvey@164: if m: pjenvey@164: raise SyntaxError( pjenvey@164: "python refuses to compile code with both a UTF8 " pjenvey@164: "byte-order-mark and a magic encoding comment") pjenvey@164: return 'utf_8' pjenvey@164: elif m: pjenvey@164: return m.group(1) pjenvey@164: else: pjenvey@164: return None pjenvey@164: finally: pjenvey@164: fp.seek(pos) pjenvey@164: cmlenz@44: def pathmatch(pattern, filename): cmlenz@44: """Extended pathname pattern matching. cmlenz@1: cmlenz@44: This function is similar to what is provided by the ``fnmatch`` module in cmlenz@44: the Python standard library, but: cmlenz@44: cmlenz@44: * can match complete (relative or absolute) path names, and not just file cmlenz@44: names, and cmlenz@44: * also supports a convenience pattern ("**") to match files at any cmlenz@44: directory level. cmlenz@44: cmlenz@44: Examples: cmlenz@44: cmlenz@44: >>> pathmatch('**.py', 'bar.py') cmlenz@44: True cmlenz@44: >>> pathmatch('**.py', 'foo/bar/baz.py') cmlenz@44: True cmlenz@44: >>> pathmatch('**.py', 'templates/index.html') cmlenz@44: False cmlenz@47: cmlenz@44: >>> pathmatch('**/templates/*.html', 'templates/index.html') cmlenz@44: True cmlenz@44: >>> pathmatch('**/templates/*.html', 'templates/foo/bar.html') cmlenz@44: False cmlenz@1: cmlenz@1: :param pattern: the glob pattern cmlenz@44: :param filename: the path name of the file to match against cmlenz@44: :return: `True` if the path name matches the pattern, `False` otherwise cmlenz@44: :rtype: `bool` cmlenz@1: """ cmlenz@1: symbols = { cmlenz@1: '?': '[^/]', cmlenz@1: '?/': '[^/]/', cmlenz@1: '*': '[^/]+', cmlenz@1: '*/': '[^/]+/', cmlenz@1: '**/': '(?:.+/)*?', cmlenz@44: '**': '(?:.+/)*?[^/]+', cmlenz@1: } cmlenz@1: buf = [] cmlenz@1: for idx, part in enumerate(re.split('([?*]+/?)', pattern)): cmlenz@1: if idx % 2: cmlenz@1: buf.append(symbols[part]) cmlenz@1: elif part: cmlenz@1: buf.append(re.escape(part)) cmlenz@134: match = re.match(''.join(buf) + '$', filename.replace(os.sep, '/')) cmlenz@134: return match is not None cmlenz@1: cmlenz@29: cmlenz@316: class TextWrapper(textwrap.TextWrapper): cmlenz@316: wordsep_re = re.compile( cmlenz@316: r'(\s+|' # any whitespace cmlenz@316: r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))' # em-dash cmlenz@316: ) cmlenz@316: cmlenz@316: cmlenz@316: def wraptext(text, width=70, initial_indent='', subsequent_indent=''): cmlenz@316: """Simple wrapper around the ``textwrap.wrap`` function in the standard cmlenz@316: library. This version does not wrap lines on hyphens in words. cmlenz@316: cmlenz@316: :param text: the text to wrap cmlenz@316: :param width: the maximum line width cmlenz@316: :param initial_indent: string that will be prepended to the first line of cmlenz@316: wrapped output cmlenz@316: :param subsequent_indent: string that will be prepended to all lines save cmlenz@316: the first of wrapped output cmlenz@316: :return: a list of lines cmlenz@316: :rtype: `list` cmlenz@316: """ cmlenz@316: wrapper = TextWrapper(width=width, initial_indent=initial_indent, cmlenz@316: subsequent_indent=subsequent_indent, cmlenz@316: break_long_words=False) cmlenz@316: return wrapper.wrap(text) cmlenz@316: cmlenz@316: cmlenz@56: class odict(dict): cmlenz@56: """Ordered dict implementation. cmlenz@56: cmlenz@227: :see: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747 cmlenz@56: """ cmlenz@62: def __init__(self, data=None): cmlenz@62: dict.__init__(self, data or {}) cmlenz@163: self._keys = dict.keys(self) cmlenz@56: cmlenz@56: def __delitem__(self, key): cmlenz@56: dict.__delitem__(self, key) cmlenz@56: self._keys.remove(key) cmlenz@56: cmlenz@56: def __setitem__(self, key, item): cmlenz@56: dict.__setitem__(self, key, item) cmlenz@56: if key not in self._keys: cmlenz@56: self._keys.append(key) cmlenz@56: cmlenz@56: def __iter__(self): cmlenz@56: return iter(self._keys) cmlenz@346: iterkeys = __iter__ cmlenz@56: cmlenz@56: def clear(self): cmlenz@56: dict.clear(self) cmlenz@56: self._keys = [] cmlenz@56: cmlenz@56: def copy(self): cmlenz@56: d = odict() cmlenz@56: d.update(self) cmlenz@56: return d cmlenz@56: cmlenz@56: def items(self): cmlenz@56: return zip(self._keys, self.values()) cmlenz@56: cmlenz@346: def iteritems(self): cmlenz@346: return izip(self._keys, self.itervalues()) cmlenz@346: cmlenz@56: def keys(self): cmlenz@56: return self._keys[:] cmlenz@56: cmlenz@346: def pop(self, key, default=missing): cmlenz@346: if default is missing: cmlenz@346: return dict.pop(self, key) cmlenz@346: elif key not in self: cmlenz@165: return default cmlenz@165: self._keys.remove(key) cmlenz@346: return dict.pop(self, key, default) cmlenz@346: cmlenz@346: def popitem(self, key): cmlenz@346: self._keys.remove(key) cmlenz@346: return dict.popitem(key) cmlenz@165: cmlenz@56: def setdefault(self, key, failobj = None): cmlenz@56: dict.setdefault(self, key, failobj) cmlenz@56: if key not in self._keys: cmlenz@56: self._keys.append(key) cmlenz@56: cmlenz@56: def update(self, dict): cmlenz@56: for (key, val) in dict.items(): cmlenz@56: self[key] = val cmlenz@56: cmlenz@56: def values(self): cmlenz@56: return map(self.get, self._keys) cmlenz@56: cmlenz@346: def itervalues(self): cmlenz@346: return imap(self.get, self._keys) cmlenz@346: cmlenz@56: cmlenz@1: try: cmlenz@1: relpath = os.path.relpath cmlenz@1: except AttributeError: cmlenz@1: def relpath(path, start='.'): cmlenz@29: """Compute the relative path to one path from another. cmlenz@29: cmlenz@130: >>> relpath('foo/bar.txt', '').replace(os.sep, '/') cmlenz@44: 'foo/bar.txt' cmlenz@130: >>> relpath('foo/bar.txt', 'foo').replace(os.sep, '/') cmlenz@44: 'bar.txt' cmlenz@130: >>> relpath('foo/bar.txt', 'baz').replace(os.sep, '/') cmlenz@44: '../foo/bar.txt' cmlenz@44: cmlenz@29: :return: the relative path cmlenz@29: :rtype: `basestring` cmlenz@29: """ cmlenz@1: start_list = os.path.abspath(start).split(os.sep) cmlenz@1: path_list = os.path.abspath(path).split(os.sep) cmlenz@1: cmlenz@1: # Work out how much of the filepath is shared by start and path. cmlenz@1: i = len(os.path.commonprefix([start_list, path_list])) cmlenz@1: cmlenz@1: rel_list = [os.path.pardir] * (len(start_list) - i) + path_list[i:] cmlenz@1: return os.path.join(*rel_list) cmlenz@29: cmlenz@106: ZERO = timedelta(0) cmlenz@106: cmlenz@106: cmlenz@106: class FixedOffsetTimezone(tzinfo): cmlenz@106: """Fixed offset in minutes east from UTC.""" cmlenz@106: cmlenz@106: def __init__(self, offset, name=None): cmlenz@106: self._offset = timedelta(minutes=offset) cmlenz@106: if name is None: cmlenz@106: name = 'Etc/GMT+%d' % offset cmlenz@106: self.zone = name cmlenz@106: cmlenz@106: def __str__(self): cmlenz@106: return self.zone cmlenz@106: cmlenz@106: def __repr__(self): cmlenz@106: return '' % (self.zone, self._offset) cmlenz@106: cmlenz@106: def utcoffset(self, dt): cmlenz@106: return self._offset cmlenz@106: cmlenz@106: def tzname(self, dt): cmlenz@106: return self.zone cmlenz@106: cmlenz@106: def dst(self, dt): cmlenz@106: return ZERO cmlenz@106: cmlenz@106: cmlenz@29: try: cmlenz@29: from pytz import UTC cmlenz@29: except ImportError: cmlenz@106: UTC = FixedOffsetTimezone(0, 'UTC') cmlenz@29: """`tzinfo` object for UTC (Universal Time). cmlenz@29: cmlenz@29: :type: `tzinfo` cmlenz@29: """ cmlenz@95: cmlenz@95: STDOFFSET = timedelta(seconds = -time.timezone) cmlenz@95: if time.daylight: cmlenz@95: DSTOFFSET = timedelta(seconds = -time.altzone) cmlenz@95: else: cmlenz@95: DSTOFFSET = STDOFFSET cmlenz@95: cmlenz@95: DSTDIFF = DSTOFFSET - STDOFFSET cmlenz@95: cmlenz@106: cmlenz@95: class LocalTimezone(tzinfo): cmlenz@95: cmlenz@95: def utcoffset(self, dt): cmlenz@95: if self._isdst(dt): cmlenz@95: return DSTOFFSET cmlenz@95: else: cmlenz@95: return STDOFFSET cmlenz@95: cmlenz@95: def dst(self, dt): cmlenz@95: if self._isdst(dt): cmlenz@95: return DSTDIFF cmlenz@95: else: cmlenz@95: return ZERO cmlenz@95: cmlenz@95: def tzname(self, dt): cmlenz@95: return time.tzname[self._isdst(dt)] cmlenz@95: cmlenz@95: def _isdst(self, dt): cmlenz@95: tt = (dt.year, dt.month, dt.day, cmlenz@95: dt.hour, dt.minute, dt.second, cmlenz@95: dt.weekday(), 0, -1) cmlenz@95: stamp = time.mktime(tt) cmlenz@95: tt = time.localtime(stamp) cmlenz@95: return tt.tm_isdst > 0 cmlenz@95: cmlenz@106: cmlenz@97: LOCALTZ = LocalTimezone() cmlenz@97: """`tzinfo` object for local time-zone. cmlenz@97: cmlenz@97: :type: `tzinfo` cmlenz@97: """