~ubuntu-branches/ubuntu/precise/trac/precise

« back to all changes in this revision

Viewing changes to trac/util/__init__.py

  • Committer: Bazaar Package Importer
  • Author(s): Luis Matos
  • Date: 2008-07-13 23:46:20 UTC
  • mfrom: (1.1.13 upstream)
  • Revision ID: james.westby@ubuntu.com-20080713234620-13ynpdpkbaymfg1z
Tags: 0.11-2
* Re-added python-setup-tools to build dependences. Closes: #490320 #468705
* New upstream release Closes: 489727
* Added sugestion for other vcs support available: git bazaar mercurial 
* Added spamfilter plugin to sugests
* Moved packaging from python-support to python-central
* Added an entry to the NEWS about the cgi Closes: #490275
* Updated 10_remove_trac_suffix_from_title patch to be used in 0.11

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# -*- coding: utf-8 -*-
2
2
#
3
 
# Copyright (C) 2003-2006 Edgewall Software
4
 
# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
 
3
# Copyright (C) 2003-2008 Edgewall Software
 
4
# Copyright (C) 2003-2006 Jonas Borgström <jonas@edgewall.com>
5
5
# Copyright (C) 2006 Matthew Good <trac@matt-good.net>
6
6
# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
7
7
# All rights reserved.
25
25
import time
26
26
import tempfile
27
27
from urllib import quote, unquote, urlencode
 
28
from itertools import izip
28
29
 
29
30
# Imports for backward compatibility
30
31
from trac.core import TracError
 
32
from trac.util.compat import reversed, sorted, tee
31
33
from trac.util.html import escape, unescape, Markup, Deuglifier
32
34
from trac.util.text import CRLF, to_utf8, to_unicode, shorten_line, \
33
35
                           wrap, pretty_size
55
57
 
56
58
# -- algorithmic utilities
57
59
 
58
 
try:
59
 
    reversed = reversed
60
 
except NameError:
61
 
    def reversed(x):
62
 
        if hasattr(x, 'keys'):
63
 
            raise ValueError('mappings do not support reverse iteration')
64
 
        i = len(x)
65
 
        while i > 0:
66
 
            i -= 1
67
 
            yield x[i]
68
 
 
69
 
try:
70
 
    sorted = sorted
71
 
except NameError:
72
 
    def sorted(iterable, cmp=None, key=None, reverse=False):
73
 
        """Partial implementation of the "sorted" function from Python 2.4"""
74
 
        if key is None:
75
 
            lst = list(iterable)
76
 
        else:
77
 
            lst = [(key(val), idx, val) for idx, val in enumerate(iterable)]
78
 
        lst.sort()
79
 
        if key is None:
80
 
            if reverse:
81
 
                return lst[::-1]
82
 
            return lst
83
 
        if reverse:
84
 
            lst = reversed(lst)
85
 
        return [i[-1] for i in lst]
86
 
 
87
60
DIGITS = re.compile(r'(\d+)')
88
61
def embedded_numbers(s):
89
62
    """Comparison function for natural order sorting based on
156
129
 
157
130
# -- sys utils
158
131
 
 
132
def arity(f):
 
133
    return f.func_code.co_argcount
 
134
 
159
135
def get_last_traceback():
160
136
    import traceback
161
137
    from StringIO import StringIO
163
139
    traceback.print_exc(file=tb)
164
140
    return tb.getvalue()
165
141
 
 
142
def get_lines_from_file(filename, lineno, context=0):
 
143
    """Return `content` number of lines before and after the specified
 
144
    `lineno` from the file identified by `filename`.
 
145
    
 
146
    Returns a `(lines_before, line, lines_after)` tuple.
 
147
    """
 
148
    if os.path.isfile(filename):
 
149
        fileobj = open(filename, 'U')
 
150
        try:
 
151
            lines = fileobj.readlines()
 
152
            lbound = max(0, lineno - context)
 
153
            ubound = lineno + 1 + context
 
154
 
 
155
 
 
156
            charset = None
 
157
            rep = re.compile('coding[=:]\s*([-\w.]+)')
 
158
            for linestr in lines[0], lines[1]:
 
159
                match = rep.search(linestr)
 
160
                if match:
 
161
                    charset = match.group(1)
 
162
                    break
 
163
 
 
164
            before = [to_unicode(l.rstrip('\n'), charset)
 
165
                         for l in lines[lbound:lineno]]
 
166
            line = to_unicode(lines[lineno].rstrip('\n'), charset)
 
167
            after = [to_unicode(l.rstrip('\n'), charset) \
 
168
                         for l in lines[lineno + 1:ubound]]
 
169
 
 
170
            return before, line, after
 
171
        finally:
 
172
            fileobj.close()
 
173
    return (), None, ()
 
174
 
166
175
def safe__import__(module_name):
167
176
    """
168
177
    Safe imports: rollback after a failed import.
181
190
                del(sys.modules[modname])
182
191
        raise e
183
192
 
 
193
# -- setuptools utils
 
194
 
 
195
def get_module_path(module):
 
196
    # Determine the plugin that this component belongs to
 
197
    path = module.__file__
 
198
    module_name = module.__name__
 
199
    if path.endswith('.pyc') or path.endswith('.pyo'):
 
200
        path = path[:-1]
 
201
    if os.path.basename(path) == '__init__.py':
 
202
        path = os.path.dirname(path)
 
203
    base_path = os.path.splitext(path)[0]
 
204
    while base_path.replace(os.sep, '.').endswith(module_name):
 
205
        base_path = os.path.dirname(base_path)
 
206
        module_name = '.'.join(module_name.split('.')[:-1])
 
207
        if not module_name:
 
208
            break
 
209
    return base_path
 
210
 
 
211
def get_pkginfo(dist):
 
212
    """Get a dictionary containing package information for a package
 
213
 
 
214
    `dist` can be either a Distribution instance or, as a shortcut,
 
215
    directly the module instance, if one can safely infer a Distribution
 
216
    instance from it.
 
217
    
 
218
    Always returns a dictionary but it will be empty if no Distribution
 
219
    instance can be created for the given module.
 
220
    """
 
221
    import types
 
222
    if isinstance(dist, types.ModuleType):
 
223
        try:
 
224
            from pkg_resources import find_distributions
 
225
            module = dist
 
226
            module_path = get_module_path(module)
 
227
            for dist in find_distributions(module_path, only=True):
 
228
                if os.path.isfile(module_path) or \
 
229
                       dist.key == module.__name__.lower():
 
230
                    break
 
231
            else:
 
232
                return {}
 
233
        except ImportError:
 
234
            return {}
 
235
    import email
 
236
    attrs = ('author', 'author-email', 'license', 'home-page', 'summary',
 
237
             'description', 'version')
 
238
    info = {}
 
239
    def normalize(attr):
 
240
        return attr.lower().replace('-', '_')
 
241
    try:
 
242
        pkginfo = email.message_from_string(dist.get_metadata('PKG-INFO'))
 
243
        for attr in [key for key in attrs if key in pkginfo]:
 
244
            info[normalize(attr)] = pkginfo[attr]
 
245
    except IOError, e:
 
246
        err = 'Failed to read PKG-INFO file for %s: %s' % (dist, e)
 
247
        for attr in attrs:
 
248
            info[normalize(attr)] = err
 
249
    except email.Errors.MessageError, e:
 
250
        err = 'Failed to parse PKG-INFO file for %s: %s' % (dist, e)
 
251
        for attr in attrs:
 
252
            info[normalize(attr)] = err
 
253
    return info
184
254
 
185
255
# -- crypto utils
186
256
 
187
257
def hex_entropy(bytes=32):
188
 
    import md5
 
258
    import sha
189
259
    import random
190
 
    return md5.md5(str(random.random())).hexdigest()[:bytes]
 
260
    return sha.new(str(random.random())).hexdigest()[:bytes]
191
261
 
192
262
 
193
263
# Original license for md5crypt:
257
327
        rearranged += itoa64[v & 0x3f]; v >>= 6
258
328
 
259
329
    return magic + salt + '$' + rearranged
 
330
 
 
331
 
 
332
# -- misc. utils
 
333
 
 
334
class Ranges(object):
 
335
    """
 
336
    Holds information about ranges parsed from a string
 
337
    
 
338
    >>> x = Ranges("1,2,9-15")
 
339
    >>> 1 in x
 
340
    True
 
341
    >>> 5 in x
 
342
    False
 
343
    >>> 10 in x
 
344
    True
 
345
    >>> 16 in x
 
346
    False
 
347
    >>> [i for i in range(20) if i in x]
 
348
    [1, 2, 9, 10, 11, 12, 13, 14, 15]
 
349
    
 
350
    Also supports iteration, which makes that last example a bit simpler:
 
351
    
 
352
    >>> list(x)
 
353
    [1, 2, 9, 10, 11, 12, 13, 14, 15]
 
354
    
 
355
    Note that it automatically reduces the list and short-circuits when the
 
356
    desired ranges are a relatively small portion of the entire set:
 
357
    
 
358
    >>> x = Ranges("99")
 
359
    >>> 1 in x # really fast
 
360
    False
 
361
    >>> x = Ranges("1, 2, 1-2, 2") # reduces this to 1-2
 
362
    >>> x.pairs
 
363
    [(1, 2)]
 
364
    >>> x = Ranges("1-9,2-4") # handle ranges that completely overlap
 
365
    >>> list(x)
 
366
    [1, 2, 3, 4, 5, 6, 7, 8, 9]
 
367
 
 
368
    The members 'a' and 'b' refer to the min and max value of the range, and
 
369
    are None if the range is empty:
 
370
    
 
371
    >>> x.a
 
372
    1
 
373
    >>> x.b
 
374
    9
 
375
    >>> e = Ranges()
 
376
    >>> e.a, e.b
 
377
    (None, None)
 
378
 
 
379
    Empty ranges are ok, and ranges can be constructed in pieces, if you
 
380
    so choose:
 
381
    
 
382
    >>> x = Ranges()
 
383
    >>> x.appendrange("1, 2, 3")
 
384
    >>> x.appendrange("5-9")
 
385
    >>> x.appendrange("2-3") # reduce'd away
 
386
    >>> list(x)
 
387
    [1, 2, 3, 5, 6, 7, 8, 9]
 
388
 
 
389
    ''Code contributed by Tim Hatch''
 
390
    """
 
391
 
 
392
    RE_STR = r"""\d+(?:[-:]\d+)?(?:,\d+(?:[-:]\d+)?)*"""
 
393
    
 
394
    def __init__(self, r=None):
 
395
        self.pairs = []
 
396
        self.a = self.b = None
 
397
        self.appendrange(r)
 
398
 
 
399
    def appendrange(self, r):
 
400
        """Add a range (from a string or None) to the current one"""
 
401
        if not r:
 
402
            return
 
403
        p = self.pairs
 
404
        for x in r.split(","):
 
405
            try:
 
406
                a, b = map(int, x.split('-', 1))
 
407
            except ValueError:
 
408
                a, b = int(x), int(x)
 
409
            if b >= a:
 
410
                p.append((a, b))
 
411
        self._reduce()
 
412
 
 
413
    def _reduce(self):
 
414
        """Come up with the minimal representation of the ranges"""
 
415
        p = self.pairs
 
416
        p.sort()
 
417
        i = 0
 
418
        while i + 1 < len(p):
 
419
            if p[i+1][0]-1 <= p[i][1]: # this item overlaps with the next
 
420
                # make the first include the second
 
421
                p[i] = (p[i][0], max(p[i][1], p[i+1][1])) 
 
422
                del p[i+1] # delete the second, after adjusting my endpoint
 
423
            else:
 
424
                i += 1
 
425
        if p:
 
426
            self.a = p[0][0] # min value
 
427
            self.b = p[-1][1] # max value
 
428
        else:
 
429
            self.a = self.b = None        
 
430
 
 
431
    def __iter__(self):
 
432
        """
 
433
        This is another way I came up with to do it.  Is it faster?
 
434
        
 
435
        from itertools import chain
 
436
        return chain(*[xrange(a, b+1) for a, b in self.pairs])
 
437
        """
 
438
        for a, b in self.pairs:
 
439
            for i in range(a, b+1):
 
440
                yield i
 
441
 
 
442
    def __contains__(self, x):
 
443
        """
 
444
        >>> 55 in Ranges()
 
445
        False
 
446
        """
 
447
        # short-circuit if outside the possible range
 
448
        if self.a is not None and self.a <= x <= self.b:
 
449
            for a, b in self.pairs:
 
450
                if a <= x <= b:
 
451
                    return True
 
452
                if b > x: # short-circuit if we've gone too far
 
453
                    break
 
454
        return False
 
455
 
 
456
    def __str__(self):
 
457
        """Provide a compact string representation of the range.
 
458
        
 
459
        >>> (str(Ranges("1,2,3,5")), str(Ranges()), str(Ranges('2')))
 
460
        ('1-3,5', '', '2')
 
461
        >>> str(Ranges('99-1')) # only nondecreasing ranges allowed
 
462
        ''
 
463
        """
 
464
        r = []
 
465
        for a, b in self.pairs:
 
466
            if a == b:
 
467
                r.append(str(a))
 
468
            else:
 
469
                r.append("%d-%d" % (a, b))
 
470
        return ",".join(r)
 
471
 
 
472
    def __len__(self):
 
473
        """The length of the entire span, ignoring holes.
 
474
        
 
475
        >>> (len(Ranges('99')), len(Ranges('1-2')), len(Ranges('')))
 
476
        (1, 2, 0)
 
477
        """
 
478
        if self.a is not None and self.b is not None:
 
479
            return self.b - self.a + 1
 
480
        else:
 
481
            return 0
 
482
 
 
483
def content_disposition(type, filename=None):
 
484
    """Generate a properly escaped Content-Disposition header"""
 
485
    if isinstance(filename, unicode):
 
486
        filename = filename.encode('utf-8')
 
487
    return type + '; filename=' + quote(filename, safe='')
 
488
 
 
489
def pairwise(iterable):
 
490
    """s -> (s0,s1), (s1,s2), (s2, s3), ...
 
491
 
 
492
    :deprecated: since 0.11 (if this really needs to be used, rewrite it
 
493
                             without izip)
 
494
    """
 
495
    a, b = tee(iterable)
 
496
    try:
 
497
        b.next()
 
498
    except StopIteration:
 
499
        pass
 
500
    return izip(a, b)
 
501
 
 
502
def partition(iterable, order=None):
 
503
    result = {}
 
504
    if order is not None:
 
505
        for key in order:
 
506
            result[key] = []
 
507
    for item, category in iterable:
 
508
        result.setdefault(category, []).append(item)
 
509
    if order is None:
 
510
        return result
 
511
    return [result[key] for key in order]