Mercurial > babel > old > mirror
annotate 0.9.x/babel/util.py @ 510:4c473bedd528 stable
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
author | fschwarz |
---|---|
date | Fri, 04 Mar 2011 14:16:15 +0000 |
parents | cd2dec0823c9 |
children |
rev | line source |
---|---|
263 | 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 | |
14 """Various utility classes and functions.""" | |
15 | |
16 import codecs | |
17 from datetime import timedelta, tzinfo | |
18 import os | |
19 import re | |
20 try: | |
509
cd2dec0823c9
Python 2.3 compatibility: backporting r456 and r457 to 0.9 branch (see #233)
fschwarz
parents:
371
diff
changeset
|
21 set = set |
263 | 22 except NameError: |
23 from sets import Set as set | |
318 | 24 import textwrap |
263 | 25 import time |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
26 from itertools import izip, imap |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
27 missing = object() |
263 | 28 |
318 | 29 __all__ = ['distinct', 'pathmatch', 'relpath', 'wraptext', 'odict', 'UTC', |
30 'LOCALTZ'] | |
263 | 31 __docformat__ = 'restructuredtext en' |
32 | |
371 | 33 |
263 | 34 def distinct(iterable): |
35 """Yield all items in an iterable collection that are distinct. | |
36 | |
37 Unlike when using sets for a similar effect, the original ordering of the | |
38 items in the collection is preserved by this function. | |
39 | |
40 >>> print list(distinct([1, 2, 1, 3, 4, 4])) | |
41 [1, 2, 3, 4] | |
42 >>> print list(distinct('foobar')) | |
43 ['f', 'o', 'b', 'a', 'r'] | |
44 | |
45 :param iterable: the iterable collection providing the data | |
46 :return: the distinct items in the collection | |
47 :rtype: ``iterator`` | |
48 """ | |
49 seen = set() | |
50 for item in iter(iterable): | |
51 if item not in seen: | |
52 yield item | |
53 seen.add(item) | |
54 | |
55 # Regexp to match python magic encoding line | |
56 PYTHON_MAGIC_COMMENT_re = re.compile( | |
57 r'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)', re.VERBOSE) | |
58 def parse_encoding(fp): | |
59 """Deduce the encoding of a source file from magic comment. | |
60 | |
61 It does this in the same way as the `Python interpreter`__ | |
62 | |
63 .. __: http://docs.python.org/ref/encodings.html | |
64 | |
65 The ``fp`` argument should be a seekable file object. | |
66 | |
67 (From Jeff Dairiki) | |
68 """ | |
69 pos = fp.tell() | |
70 fp.seek(0) | |
71 try: | |
72 line1 = fp.readline() | |
73 has_bom = line1.startswith(codecs.BOM_UTF8) | |
74 if has_bom: | |
75 line1 = line1[len(codecs.BOM_UTF8):] | |
76 | |
77 m = PYTHON_MAGIC_COMMENT_re.match(line1) | |
78 if not m: | |
79 try: | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
80 import parser |
263 | 81 parser.suite(line1) |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
82 except (ImportError, SyntaxError): |
263 | 83 # Either it's a real syntax error, in which case the source is |
84 # not valid python source, or line2 is a continuation of line1, | |
85 # in which case we don't want to scan line2 for a magic | |
86 # comment. | |
87 pass | |
88 else: | |
89 line2 = fp.readline() | |
90 m = PYTHON_MAGIC_COMMENT_re.match(line2) | |
91 | |
92 if has_bom: | |
93 if m: | |
94 raise SyntaxError( | |
95 "python refuses to compile code with both a UTF8 " | |
96 "byte-order-mark and a magic encoding comment") | |
97 return 'utf_8' | |
98 elif m: | |
99 return m.group(1) | |
100 else: | |
101 return None | |
102 finally: | |
103 fp.seek(pos) | |
104 | |
105 def pathmatch(pattern, filename): | |
106 """Extended pathname pattern matching. | |
107 | |
108 This function is similar to what is provided by the ``fnmatch`` module in | |
109 the Python standard library, but: | |
110 | |
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 | |
116 Examples: | |
117 | |
118 >>> pathmatch('**.py', 'bar.py') | |
119 True | |
120 >>> pathmatch('**.py', 'foo/bar/baz.py') | |
121 True | |
122 >>> pathmatch('**.py', 'templates/index.html') | |
123 False | |
124 | |
125 >>> pathmatch('**/templates/*.html', 'templates/index.html') | |
126 True | |
127 >>> pathmatch('**/templates/*.html', 'templates/foo/bar.html') | |
128 False | |
129 | |
130 :param pattern: the glob pattern | |
131 :param filename: the path name of the file to match against | |
132 :return: `True` if the path name matches the pattern, `False` otherwise | |
133 :rtype: `bool` | |
134 """ | |
135 symbols = { | |
136 '?': '[^/]', | |
137 '?/': '[^/]/', | |
138 '*': '[^/]+', | |
139 '*/': '[^/]+/', | |
140 '**/': '(?:.+/)*?', | |
141 '**': '(?:.+/)*?[^/]+', | |
142 } | |
143 buf = [] | |
144 for idx, part in enumerate(re.split('([?*]+/?)', pattern)): | |
145 if idx % 2: | |
146 buf.append(symbols[part]) | |
147 elif part: | |
148 buf.append(re.escape(part)) | |
149 match = re.match(''.join(buf) + '$', filename.replace(os.sep, '/')) | |
150 return match is not None | |
151 | |
152 | |
318 | 153 class TextWrapper(textwrap.TextWrapper): |
154 wordsep_re = re.compile( | |
155 r'(\s+|' # any whitespace | |
156 r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))' # em-dash | |
157 ) | |
158 | |
159 | |
160 def wraptext(text, width=70, initial_indent='', subsequent_indent=''): | |
161 """Simple wrapper around the ``textwrap.wrap`` function in the standard | |
162 library. This version does not wrap lines on hyphens in words. | |
163 | |
164 :param text: the text to wrap | |
165 :param width: the maximum line width | |
166 :param initial_indent: string that will be prepended to the first line of | |
167 wrapped output | |
168 :param subsequent_indent: string that will be prepended to all lines save | |
169 the first of wrapped output | |
170 :return: a list of lines | |
171 :rtype: `list` | |
172 """ | |
173 wrapper = TextWrapper(width=width, initial_indent=initial_indent, | |
174 subsequent_indent=subsequent_indent, | |
175 break_long_words=False) | |
176 return wrapper.wrap(text) | |
177 | |
178 | |
263 | 179 class odict(dict): |
180 """Ordered dict implementation. | |
181 | |
182 :see: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747 | |
183 """ | |
184 def __init__(self, data=None): | |
185 dict.__init__(self, data or {}) | |
186 self._keys = dict.keys(self) | |
187 | |
188 def __delitem__(self, key): | |
189 dict.__delitem__(self, key) | |
190 self._keys.remove(key) | |
191 | |
192 def __setitem__(self, key, item): | |
193 dict.__setitem__(self, key, item) | |
194 if key not in self._keys: | |
195 self._keys.append(key) | |
196 | |
197 def __iter__(self): | |
198 return iter(self._keys) | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
199 iterkeys = __iter__ |
263 | 200 |
201 def clear(self): | |
202 dict.clear(self) | |
203 self._keys = [] | |
204 | |
205 def copy(self): | |
206 d = odict() | |
207 d.update(self) | |
208 return d | |
209 | |
210 def items(self): | |
211 return zip(self._keys, self.values()) | |
212 | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
213 def iteritems(self): |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
214 return izip(self._keys, self.itervalues()) |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
215 |
263 | 216 def keys(self): |
217 return self._keys[:] | |
218 | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
219 def pop(self, key, default=missing): |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
220 if default is missing: |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
221 return dict.pop(self, key) |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
222 elif key not in self: |
263 | 223 return default |
224 self._keys.remove(key) | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
225 return dict.pop(self, key, default) |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
226 |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
227 def popitem(self, key): |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
228 self._keys.remove(key) |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
229 return dict.popitem(key) |
263 | 230 |
231 def setdefault(self, key, failobj = None): | |
232 dict.setdefault(self, key, failobj) | |
233 if key not in self._keys: | |
234 self._keys.append(key) | |
235 | |
236 def update(self, dict): | |
237 for (key, val) in dict.items(): | |
238 self[key] = val | |
239 | |
240 def values(self): | |
241 return map(self.get, self._keys) | |
242 | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
243 def itervalues(self): |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
244 return imap(self.get, self._keys) |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
318
diff
changeset
|
245 |
263 | 246 |
247 try: | |
248 relpath = os.path.relpath | |
249 except AttributeError: | |
250 def relpath(path, start='.'): | |
251 """Compute the relative path to one path from another. | |
252 | |
253 >>> relpath('foo/bar.txt', '').replace(os.sep, '/') | |
254 'foo/bar.txt' | |
255 >>> relpath('foo/bar.txt', 'foo').replace(os.sep, '/') | |
256 'bar.txt' | |
257 >>> relpath('foo/bar.txt', 'baz').replace(os.sep, '/') | |
258 '../foo/bar.txt' | |
259 | |
260 :return: the relative path | |
261 :rtype: `basestring` | |
262 """ | |
263 start_list = os.path.abspath(start).split(os.sep) | |
264 path_list = os.path.abspath(path).split(os.sep) | |
265 | |
266 # Work out how much of the filepath is shared by start and path. | |
267 i = len(os.path.commonprefix([start_list, path_list])) | |
268 | |
269 rel_list = [os.path.pardir] * (len(start_list) - i) + path_list[i:] | |
270 return os.path.join(*rel_list) | |
271 | |
510
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
272 try: |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
273 from operator import attrgetter, itemgetter |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
274 except ImportError: |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
275 def itemgetter(name): |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
276 def _getitem(obj): |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
277 return obj[name] |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
278 return _getitem |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
279 |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
280 try: |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
281 ''.rsplit |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
282 def rsplit(a_string, sep=None, maxsplit=None): |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
283 return a_string.rsplit(sep, maxsplit) |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
284 except AttributeError: |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
285 def rsplit(a_string, sep=None, maxsplit=None): |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
286 parts = a_string.split(sep) |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
287 if maxsplit is None or len(parts) <= maxsplit: |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
288 return parts |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
289 maxsplit_index = len(parts) - maxsplit |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
290 non_splitted_part = sep.join(parts[:maxsplit_index]) |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
291 splitted = parts[maxsplit_index:] |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
292 return [non_splitted_part] + splitted |
4c473bedd528
Fix Python 2.3 compatibility for 0.9 branch (closes #233)
fschwarz
parents:
509
diff
changeset
|
293 |
263 | 294 ZERO = timedelta(0) |
295 | |
296 | |
297 class FixedOffsetTimezone(tzinfo): | |
298 """Fixed offset in minutes east from UTC.""" | |
299 | |
300 def __init__(self, offset, name=None): | |
301 self._offset = timedelta(minutes=offset) | |
302 if name is None: | |
303 name = 'Etc/GMT+%d' % offset | |
304 self.zone = name | |
305 | |
306 def __str__(self): | |
307 return self.zone | |
308 | |
309 def __repr__(self): | |
310 return '<FixedOffset "%s" %s>' % (self.zone, self._offset) | |
311 | |
312 def utcoffset(self, dt): | |
313 return self._offset | |
314 | |
315 def tzname(self, dt): | |
316 return self.zone | |
317 | |
318 def dst(self, dt): | |
319 return ZERO | |
320 | |
321 | |
322 try: | |
323 from pytz import UTC | |
324 except ImportError: | |
325 UTC = FixedOffsetTimezone(0, 'UTC') | |
326 """`tzinfo` object for UTC (Universal Time). | |
327 | |
328 :type: `tzinfo` | |
329 """ | |
330 | |
331 STDOFFSET = timedelta(seconds = -time.timezone) | |
332 if time.daylight: | |
333 DSTOFFSET = timedelta(seconds = -time.altzone) | |
334 else: | |
335 DSTOFFSET = STDOFFSET | |
336 | |
337 DSTDIFF = DSTOFFSET - STDOFFSET | |
338 | |
339 | |
340 class LocalTimezone(tzinfo): | |
341 | |
342 def utcoffset(self, dt): | |
343 if self._isdst(dt): | |
344 return DSTOFFSET | |
345 else: | |
346 return STDOFFSET | |
347 | |
348 def dst(self, dt): | |
349 if self._isdst(dt): | |
350 return DSTDIFF | |
351 else: | |
352 return ZERO | |
353 | |
354 def tzname(self, dt): | |
355 return time.tzname[self._isdst(dt)] | |
356 | |
357 def _isdst(self, dt): | |
358 tt = (dt.year, dt.month, dt.day, | |
359 dt.hour, dt.minute, dt.second, | |
360 dt.weekday(), 0, -1) | |
361 stamp = time.mktime(tt) | |
362 tt = time.localtime(stamp) | |
363 return tt.tm_isdst > 0 | |
364 | |
365 | |
366 LOCALTZ = LocalTimezone() | |
367 """`tzinfo` object for local time-zone. | |
368 | |
369 :type: `tzinfo` | |
370 """ |