You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
290 lines
8.0 KiB
Python
290 lines
8.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
babel.util
|
|
~~~~~~~~~~
|
|
|
|
Various utility classes and functions.
|
|
|
|
:copyright: (c) 2013 by the Babel Team.
|
|
:license: BSD, see LICENSE for more details.
|
|
"""
|
|
|
|
import codecs
|
|
from datetime import timedelta, tzinfo
|
|
import os
|
|
import re
|
|
import textwrap
|
|
from babel._compat import izip, imap
|
|
|
|
missing = object()
|
|
|
|
|
|
def distinct(iterable):
|
|
"""Yield all items in an iterable collection that are distinct.
|
|
|
|
Unlike when using sets for a similar effect, the original ordering of the
|
|
items in the collection is preserved by this function.
|
|
|
|
>>> print list(distinct([1, 2, 1, 3, 4, 4]))
|
|
[1, 2, 3, 4]
|
|
>>> print list(distinct('foobar'))
|
|
['f', 'o', 'b', 'a', 'r']
|
|
|
|
:param iterable: the iterable collection providing the data
|
|
"""
|
|
seen = set()
|
|
for item in iter(iterable):
|
|
if item not in seen:
|
|
yield item
|
|
seen.add(item)
|
|
|
|
# Regexp to match python magic encoding line
|
|
PYTHON_MAGIC_COMMENT_re = re.compile(
|
|
br'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)', re.VERBOSE)
|
|
def parse_encoding(fp):
|
|
"""Deduce the encoding of a source file from magic comment.
|
|
|
|
It does this in the same way as the `Python interpreter`__
|
|
|
|
.. __: http://docs.python.org/ref/encodings.html
|
|
|
|
The ``fp`` argument should be a seekable file object.
|
|
|
|
(From Jeff Dairiki)
|
|
"""
|
|
pos = fp.tell()
|
|
fp.seek(0)
|
|
try:
|
|
line1 = fp.readline()
|
|
has_bom = line1.startswith(codecs.BOM_UTF8)
|
|
if has_bom:
|
|
line1 = line1[len(codecs.BOM_UTF8):]
|
|
|
|
m = PYTHON_MAGIC_COMMENT_re.match(line1)
|
|
if not m:
|
|
try:
|
|
import parser
|
|
parser.suite(line1.decode('latin-1'))
|
|
except (ImportError, SyntaxError):
|
|
# Either it's a real syntax error, in which case the source is
|
|
# not valid python source, or line2 is a continuation of line1,
|
|
# in which case we don't want to scan line2 for a magic
|
|
# comment.
|
|
pass
|
|
else:
|
|
line2 = fp.readline()
|
|
m = PYTHON_MAGIC_COMMENT_re.match(line2)
|
|
|
|
if has_bom:
|
|
if m:
|
|
raise SyntaxError(
|
|
"python refuses to compile code with both a UTF8 "
|
|
"byte-order-mark and a magic encoding comment")
|
|
return 'utf_8'
|
|
elif m:
|
|
return m.group(1).decode('latin-1')
|
|
else:
|
|
return None
|
|
finally:
|
|
fp.seek(pos)
|
|
|
|
def pathmatch(pattern, filename):
|
|
"""Extended pathname pattern matching.
|
|
|
|
This function is similar to what is provided by the ``fnmatch`` module in
|
|
the Python standard library, but:
|
|
|
|
* can match complete (relative or absolute) path names, and not just file
|
|
names, and
|
|
* also supports a convenience pattern ("**") to match files at any
|
|
directory level.
|
|
|
|
Examples:
|
|
|
|
>>> pathmatch('**.py', 'bar.py')
|
|
True
|
|
>>> pathmatch('**.py', 'foo/bar/baz.py')
|
|
True
|
|
>>> pathmatch('**.py', 'templates/index.html')
|
|
False
|
|
|
|
>>> pathmatch('**/templates/*.html', 'templates/index.html')
|
|
True
|
|
>>> pathmatch('**/templates/*.html', 'templates/foo/bar.html')
|
|
False
|
|
|
|
:param pattern: the glob pattern
|
|
:param filename: the path name of the file to match against
|
|
"""
|
|
symbols = {
|
|
'?': '[^/]',
|
|
'?/': '[^/]/',
|
|
'*': '[^/]+',
|
|
'*/': '[^/]+/',
|
|
'**/': '(?:.+/)*?',
|
|
'**': '(?:.+/)*?[^/]+',
|
|
}
|
|
buf = []
|
|
for idx, part in enumerate(re.split('([?*]+/?)', pattern)):
|
|
if idx % 2:
|
|
buf.append(symbols[part])
|
|
elif part:
|
|
buf.append(re.escape(part))
|
|
match = re.match(''.join(buf) + '$', filename.replace(os.sep, '/'))
|
|
return match is not None
|
|
|
|
|
|
class TextWrapper(textwrap.TextWrapper):
|
|
wordsep_re = re.compile(
|
|
r'(\s+|' # any whitespace
|
|
r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))' # em-dash
|
|
)
|
|
|
|
|
|
def wraptext(text, width=70, initial_indent='', subsequent_indent=''):
|
|
"""Simple wrapper around the ``textwrap.wrap`` function in the standard
|
|
library. This version does not wrap lines on hyphens in words.
|
|
|
|
:param text: the text to wrap
|
|
:param width: the maximum line width
|
|
:param initial_indent: string that will be prepended to the first line of
|
|
wrapped output
|
|
:param subsequent_indent: string that will be prepended to all lines save
|
|
the first of wrapped output
|
|
"""
|
|
wrapper = TextWrapper(width=width, initial_indent=initial_indent,
|
|
subsequent_indent=subsequent_indent,
|
|
break_long_words=False)
|
|
return wrapper.wrap(text)
|
|
|
|
|
|
class odict(dict):
|
|
"""Ordered dict implementation.
|
|
|
|
:see: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747
|
|
"""
|
|
def __init__(self, data=None):
|
|
dict.__init__(self, data or {})
|
|
self._keys = list(dict.keys(self))
|
|
|
|
def __delitem__(self, key):
|
|
dict.__delitem__(self, key)
|
|
self._keys.remove(key)
|
|
|
|
def __setitem__(self, key, item):
|
|
dict.__setitem__(self, key, item)
|
|
if key not in self._keys:
|
|
self._keys.append(key)
|
|
|
|
def __iter__(self):
|
|
return iter(self._keys)
|
|
iterkeys = __iter__
|
|
|
|
def clear(self):
|
|
dict.clear(self)
|
|
self._keys = []
|
|
|
|
def copy(self):
|
|
d = odict()
|
|
d.update(self)
|
|
return d
|
|
|
|
def items(self):
|
|
return zip(self._keys, self.values())
|
|
|
|
def iteritems(self):
|
|
return izip(self._keys, self.itervalues())
|
|
|
|
def keys(self):
|
|
return self._keys[:]
|
|
|
|
def pop(self, key, default=missing):
|
|
if default is missing:
|
|
return dict.pop(self, key)
|
|
elif key not in self:
|
|
return default
|
|
self._keys.remove(key)
|
|
return dict.pop(self, key, default)
|
|
|
|
def popitem(self, key):
|
|
self._keys.remove(key)
|
|
return dict.popitem(key)
|
|
|
|
def setdefault(self, key, failobj = None):
|
|
dict.setdefault(self, key, failobj)
|
|
if key not in self._keys:
|
|
self._keys.append(key)
|
|
|
|
def update(self, dict):
|
|
for (key, val) in dict.items():
|
|
self[key] = val
|
|
|
|
def values(self):
|
|
return map(self.get, self._keys)
|
|
|
|
def itervalues(self):
|
|
return imap(self.get, self._keys)
|
|
|
|
|
|
try:
|
|
relpath = os.path.relpath
|
|
except AttributeError:
|
|
def relpath(path, start='.'):
|
|
"""Compute the relative path to one path from another.
|
|
|
|
>>> relpath('foo/bar.txt', '').replace(os.sep, '/')
|
|
'foo/bar.txt'
|
|
>>> relpath('foo/bar.txt', 'foo').replace(os.sep, '/')
|
|
'bar.txt'
|
|
>>> relpath('foo/bar.txt', 'baz').replace(os.sep, '/')
|
|
'../foo/bar.txt'
|
|
"""
|
|
start_list = os.path.abspath(start).split(os.sep)
|
|
path_list = os.path.abspath(path).split(os.sep)
|
|
|
|
# Work out how much of the filepath is shared by start and path.
|
|
i = len(os.path.commonprefix([start_list, path_list]))
|
|
|
|
rel_list = [os.path.pardir] * (len(start_list) - i) + path_list[i:]
|
|
return os.path.join(*rel_list)
|
|
|
|
|
|
class FixedOffsetTimezone(tzinfo):
|
|
"""Fixed offset in minutes east from UTC."""
|
|
|
|
def __init__(self, offset, name=None):
|
|
self._offset = timedelta(minutes=offset)
|
|
if name is None:
|
|
name = 'Etc/GMT+%d' % offset
|
|
self.zone = name
|
|
|
|
def __str__(self):
|
|
return self.zone
|
|
|
|
def __repr__(self):
|
|
return '<FixedOffset "%s" %s>' % (self.zone, self._offset)
|
|
|
|
def utcoffset(self, dt):
|
|
return self._offset
|
|
|
|
def tzname(self, dt):
|
|
return self.zone
|
|
|
|
def dst(self, dt):
|
|
return ZERO
|
|
|
|
|
|
import pytz as _pytz
|
|
from babel import localtime
|
|
|
|
# Export the localtime functionality here because that's
|
|
# where it was in the past.
|
|
UTC = _pytz.utc
|
|
LOCALTZ = localtime.LOCALTZ
|
|
get_localzone = localtime.get_localzone
|
|
|
|
STDOFFSET = localtime.STDOFFSET
|
|
DSTOFFSET = localtime.DSTOFFSET
|
|
DSTDIFF = localtime.DSTDIFF
|
|
ZERO = localtime.ZERO
|