~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Michael Hudson
  • Date: 2007-10-29 16:19:30 UTC
  • mto: This revision was merged to the branch mainline in revision 141.
  • Revision ID: michael.hudson@canonical.com-20071029161930-oxqrd4rd8j1oz3hx
add do nothing check target

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#
2
 
# Copyright (C) 2008  Canonical Ltd.
3
 
#                     (Authored by Martin Albisetti <argentina@gmail.com)
4
2
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
5
3
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
6
4
#
19
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
18
#
21
19
 
22
 
try:
23
 
    from xml.etree import ElementTree as ET
24
 
except ImportError:
25
 
    from elementtree import ElementTree as ET
26
 
 
27
 
from simpletal.simpleTALUtils import HTMLStructureCleaner
28
 
 
29
20
import base64
30
21
import cgi
31
22
import datetime
32
23
import logging
33
24
import re
 
25
import sha
34
26
import struct
 
27
import sys
35
28
import threading
36
29
import time
37
 
import sys
38
 
import os
39
 
import subprocess
 
30
import traceback
 
31
 
 
32
import turbogears
 
33
 
40
34
 
41
35
log = logging.getLogger("loggerhead.controllers")
42
36
 
43
37
 
 
38
def timespan(delta):
 
39
    if delta.days > 730:
 
40
        # good grief!
 
41
        return '%d years' % (int(delta.days // 365.25),)
 
42
    if delta.days >= 3:
 
43
        return '%d days' % delta.days
 
44
    seg = []
 
45
    if delta.days > 0:
 
46
        if delta.days == 1:
 
47
            seg.append('1 day')
 
48
        else:
 
49
            seg.append('%d days' % delta.days)
 
50
    hrs = delta.seconds // 3600
 
51
    mins = (delta.seconds % 3600) // 60
 
52
    if hrs > 0:
 
53
        if hrs == 1:
 
54
            seg.append('1 hour')
 
55
        else:
 
56
            seg.append('%d hours' % hrs)
 
57
    if delta.days == 0:
 
58
        if mins > 0:
 
59
            if mins == 1:
 
60
                seg.append('1 minute')
 
61
            else:
 
62
                seg.append('%d minutes' % mins)
 
63
        elif hrs == 0:
 
64
            seg.append('less than a minute')
 
65
    return ', '.join(seg)
 
66
 
 
67
 
 
68
def ago(timestamp):
 
69
    now = datetime.datetime.now()
 
70
    return timespan(now - timestamp) + ' ago'
 
71
 
 
72
 
44
73
def fix_year(year):
45
74
    if year < 70:
46
75
        year += 2000
48
77
        year += 1900
49
78
    return year
50
79
 
51
 
# Display of times.
52
 
 
53
 
# date_day -- just the day
54
 
# date_time -- full date with time
55
 
#
56
 
# displaydate -- for use in sentences
57
 
# approximatedate -- for use in tables
58
 
#
59
 
# displaydate and approximatedate return an elementtree <span> Element
60
 
# with the full date in a tooltip.
61
 
 
62
 
 
63
 
def date_day(value):
64
 
    return value.strftime('%Y-%m-%d')
65
 
 
66
 
 
67
 
def date_time(value):
68
 
    if value is not None:
69
 
        return value.strftime('%Y-%m-%d %T')
70
 
    else:
71
 
        return 'N/A'
72
 
 
73
 
 
74
 
def _displaydate(date):
75
 
    delta = abs(datetime.datetime.now() - date)
76
 
    if delta > datetime.timedelta(1, 0, 0):
77
 
        # far in the past or future, display the date
78
 
        return 'on ' + date_day(date)
79
 
    return _approximatedate(date)
80
 
 
81
 
 
82
 
def _approximatedate(date):
83
 
    delta = datetime.datetime.now() - date
84
 
    if abs(delta) > datetime.timedelta(1, 0, 0):
85
 
        # far in the past or future, display the date
86
 
        return date_day(date)
87
 
    future = delta < datetime.timedelta(0, 0, 0)
88
 
    delta = abs(delta)
89
 
    days = delta.days
90
 
    hours = delta.seconds / 3600
91
 
    minutes = (delta.seconds - (3600*hours)) / 60
92
 
    seconds = delta.seconds % 60
93
 
    result = ''
94
 
    if future:
95
 
        result += 'in '
96
 
    if days != 0:
97
 
        amount = days
98
 
        unit = 'day'
99
 
    elif hours != 0:
100
 
        amount = hours
101
 
        unit = 'hour'
102
 
    elif minutes != 0:
103
 
        amount = minutes
104
 
        unit = 'minute'
105
 
    else:
106
 
        amount = seconds
107
 
        unit = 'second'
108
 
    if amount != 1:
109
 
        unit += 's'
110
 
    result += '%s %s' % (amount, unit)
111
 
    if not future:
112
 
        result += ' ago'
113
 
        return result
114
 
 
115
 
 
116
 
def _wrap_with_date_time_title(date, formatted_date):
117
 
    elem = ET.Element("span")
118
 
    elem.text = formatted_date
119
 
    elem.set("title", date_time(date))
120
 
    return elem
121
 
 
122
 
 
123
 
def approximatedate(date):
124
 
    #FIXME: Returns an object instead of a string
125
 
    return _wrap_with_date_time_title(date, _approximatedate(date))
126
 
 
127
 
 
128
 
def displaydate(date):
129
 
    return _wrap_with_date_time_title(date, _displaydate(date))
130
 
 
131
80
 
132
81
class Container (object):
133
82
    """
134
83
    Convert a dict into an object with attributes.
135
84
    """
136
 
 
137
85
    def __init__(self, _dict=None, **kw):
138
86
        if _dict is not None:
139
87
            for key, value in _dict.iteritems():
140
88
                setattr(self, key, value)
141
89
        for key, value in kw.iteritems():
142
90
            setattr(self, key, value)
143
 
 
 
91
    
144
92
    def __repr__(self):
145
93
        out = '{ '
146
94
        for key, value in self.__dict__.iteritems():
147
 
            if key.startswith('_') or (getattr(self.__dict__[key],
148
 
                                       '__call__', None) is not None):
 
95
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
149
96
                continue
150
97
            out += '%r => %r, ' % (key, value)
151
98
        out += '}'
152
99
        return out
153
100
 
154
101
 
 
102
def clean_revid(revid):
 
103
    if revid == 'missing':
 
104
        return revid
 
105
    return sha.new(revid).hexdigest()
 
106
 
 
107
 
 
108
def obfuscate(text):
 
109
    return ''.join([ '&#%d;' % ord(c) for c in text ])
 
110
 
 
111
 
155
112
def trunc(text, limit=10):
156
113
    if len(text) <= limit:
157
114
        return text
158
115
    return text[:limit] + '...'
159
116
 
160
117
 
 
118
def to_utf8(s):
 
119
    if isinstance(s, unicode):
 
120
        return s.encode('utf-8')
 
121
    return s
 
122
 
 
123
 
161
124
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
162
125
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
163
126
 
164
 
 
165
127
def hide_email(email):
166
128
    """
167
129
    try to obsure any email address in a bazaar committer's name.
181
143
        return '%s at %s' % (username, domains[-2])
182
144
    return '%s at %s' % (username, domains[0])
183
145
 
184
 
def hide_emails(emails):
185
 
    """
186
 
    try to obscure any email address in a list of bazaar committers' names.
187
 
    """
188
 
    result = []
189
 
    for email in emails:
190
 
        result.append(hide_email(email))
191
 
    return result
 
146
    
 
147
def triple_factors(min_value=1):
 
148
    factors = (1, 3)
 
149
    index = 0
 
150
    n = 1
 
151
    while True:
 
152
        if n >= min_value:
 
153
            yield n * factors[index]
 
154
        index += 1
 
155
        if index >= len(factors):
 
156
            index = 0
 
157
            n *= 10
 
158
 
 
159
 
 
160
def scan_range(pos, max, pagesize=1):
 
161
    """
 
162
    given a position in a maximum range, return a list of negative and positive
 
163
    jump factors for an hgweb-style triple-factor geometric scan.
 
164
    
 
165
    for example, with pos=20 and max=500, the range would be:
 
166
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
 
167
    
 
168
    i admit this is a very strange way of jumping through revisions.  i didn't
 
169
    invent it. :)
 
170
    """
 
171
    out = []
 
172
    for n in triple_factors(pagesize + 1):
 
173
        if n > max:
 
174
            return out
 
175
        if pos + n < max:
 
176
            out.append(n)
 
177
        if pos - n >= 0:
 
178
            out.insert(0, -n)
 
179
 
192
180
 
193
181
# only do this if unicode turns out to be a problem
194
182
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
195
183
 
196
184
# FIXME: get rid of this method; use fixed_width() and avoid XML().
197
 
 
198
 
 
199
185
def html_clean(s):
200
186
    """
201
187
    clean up a string for html display.  expand any tabs, encode any html
207
193
    return s
208
194
 
209
195
 
 
196
 
210
197
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
211
198
 
212
 
 
213
 
def fill_div(s):
214
 
    """
215
 
    CSS is stupid. In some cases we need to replace an empty value with
216
 
    a non breaking space (&nbsp;). There has to be a better way of doing this.
217
 
 
218
 
    return: the same value recieved if not empty, and a '&nbsp;' if it is.
219
 
    """
220
 
 
221
 
 
222
 
    if s is None:
223
 
        return '&nbsp;'
224
 
    elif isinstance(s, int):
225
 
        return s
226
 
    elif not s.strip():
227
 
        return '&nbsp;'
228
 
    else:
229
 
        try:
230
 
            s = s.decode('utf-8')
231
 
        except UnicodeDecodeError:
232
 
            s = s.decode('iso-8859-15')
233
 
        return s
234
 
 
235
 
HSC = HTMLStructureCleaner()
236
 
 
237
199
def fixed_width(s):
238
200
    """
239
201
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
249
211
            s = s.decode('utf-8')
250
212
        except UnicodeDecodeError:
251
213
            s = s.decode('iso-8859-15')
252
 
 
253
 
    s = cgi.escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
254
 
 
255
 
    return HSC.clean(s).replace('\n', '<br/>')
 
214
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
256
215
 
257
216
 
258
217
def fake_permissions(kind, executable):
264
223
    return '-rw-r--r--'
265
224
 
266
225
 
 
226
def if_present(format, value):
 
227
    """
 
228
    format a value using a format string, if the value exists and is not None.
 
229
    """
 
230
    if value is None:
 
231
        return ''
 
232
    return format % value
 
233
 
 
234
 
267
235
def b64(s):
268
236
    s = base64.encodestring(s).replace('\n', '')
269
237
    while (len(s) > 0) and (s[-1] == '='):
291
259
P95_MEG = int(0.9 * MEG)
292
260
P95_GIG = int(0.9 * GIG)
293
261
 
294
 
 
295
262
def human_size(size, min_divisor=0):
296
263
    size = int(size)
297
264
    if (size == 0) and (min_divisor == 0):
305
272
        divisor = MEG
306
273
    else:
307
274
        divisor = KILO
308
 
 
 
275
    
309
276
    dot = size % divisor
310
277
    base = size - dot
311
278
    dot = dot * 10 // divisor
313
280
    if dot >= 10:
314
281
        base += 1
315
282
        dot -= 10
316
 
 
 
283
    
317
284
    out = str(base)
318
285
    if (base < 100) and (dot != 0):
319
 
        out += '.%d' % (dot)
 
286
        out += '.%d' % (dot,)
320
287
    if divisor == KILO:
321
288
        out += 'K'
322
289
    elif divisor == MEG:
324
291
    elif divisor == GIG:
325
292
        out += 'G'
326
293
    return out
327
 
 
 
294
    
328
295
 
329
296
def fill_in_navigation(navigation):
330
297
    """
337
304
        navigation.position = 0
338
305
    navigation.count = len(navigation.revid_list)
339
306
    navigation.page_position = navigation.position // navigation.pagesize + 1
340
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
341
 
 - 1)) // navigation.pagesize
342
 
 
 
307
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
 
308
    
343
309
    def get_offset(offset):
344
 
        if (navigation.position + offset < 0) or (
345
 
           navigation.position + offset > navigation.count - 1):
 
310
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
346
311
            return None
347
312
        return navigation.revid_list[navigation.position + offset]
348
 
 
349
 
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
 
313
    
350
314
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
351
315
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
352
 
    prev_page_revno = navigation.history.get_revno(
353
 
            navigation.prev_page_revid)
354
 
    next_page_revno = navigation.history.get_revno(
355
 
            navigation.next_page_revid)
356
 
    start_revno = navigation.history.get_revno(navigation.start_revid)
357
 
 
358
 
    params = {'filter_file_id': navigation.filter_file_id}
 
316
    
 
317
    params = { 'file_id': navigation.file_id }
359
318
    if getattr(navigation, 'query', None) is not None:
360
319
        params['q'] = navigation.query
361
 
 
362
 
    if getattr(navigation, 'start_revid', None) is not None:
363
 
        params['start_revid'] = start_revno
364
 
 
 
320
    else:
 
321
        params['start_revid'] = navigation.start_revid
 
322
        
365
323
    if navigation.prev_page_revid:
366
 
        navigation.prev_page_url = navigation.branch.context_url(
367
 
            [navigation.scan_url, prev_page_revno], **params)
 
324
        navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
368
325
    if navigation.next_page_revid:
369
 
        navigation.next_page_url = navigation.branch.context_url(
370
 
            [navigation.scan_url, next_page_revno], **params)
371
 
 
372
 
 
373
 
def directory_breadcrumbs(path, is_root, view):
374
 
    """
375
 
    Generate breadcrumb information from the directory path given
376
 
 
377
 
    The path given should be a path up to any branch that is currently being
378
 
    served
379
 
 
380
 
    Arguments:
381
 
    path -- The path to convert into breadcrumbs
382
 
    is_root -- Whether or not loggerhead is serving a branch at its root
383
 
    view -- The type of view we are showing (files, changes etc)
384
 
    """
385
 
    # Is our root directory itself a branch?
386
 
    if is_root:
387
 
        if view == 'directory':
388
 
            directory = 'files'
389
 
        breadcrumbs = [{
390
 
            'dir_name': path,
391
 
            'path': '',
392
 
            'suffix': view,
393
 
        }]
394
 
    else:
395
 
        # Create breadcrumb trail for the path leading up to the branch
396
 
        breadcrumbs = [{
397
 
            'dir_name': "(root)",
398
 
            'path': '',
399
 
            'suffix': '',
400
 
        }]
401
 
        if path != '/':
402
 
            dir_parts = path.strip('/').split('/')
403
 
            for index, dir_name in enumerate(dir_parts):
404
 
                breadcrumbs.append({
405
 
                    'dir_name': dir_name,
406
 
                    'path': '/'.join(dir_parts[:index + 1]),
407
 
                    'suffix': '',
408
 
                })
409
 
            # If we are not in the directory view, the last crumb is a branch,
410
 
            # so we need to specify a view
411
 
            if view != 'directory':
412
 
                breadcrumbs[-1]['suffix'] = '/' + view
413
 
    return breadcrumbs
414
 
 
415
 
 
416
 
def branch_breadcrumbs(path, inv, view):
417
 
    """
418
 
    Generate breadcrumb information from the branch path given
419
 
 
420
 
    The path given should be a path that exists within a branch
421
 
 
422
 
    Arguments:
423
 
    path -- The path to convert into breadcrumbs
424
 
    inv -- Inventory to get file information from
425
 
    view -- The type of view we are showing (files, changes etc)
426
 
    """
427
 
    dir_parts = path.strip('/').split('/')
428
 
    inner_breadcrumbs = []
429
 
    for index, dir_name in enumerate(dir_parts):
430
 
        inner_breadcrumbs.append({
431
 
            'dir_name': dir_name,
432
 
            'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
433
 
            'suffix': '/' + view,
434
 
        })
435
 
    return inner_breadcrumbs
 
326
        navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **get_context(**params))
 
327
 
 
328
 
 
329
def log_exception(log):
 
330
    for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
 
331
        log.debug(line)
436
332
 
437
333
 
438
334
def decorator(unbound):
439
 
 
440
335
    def new_decorator(f):
441
336
        g = unbound(f)
442
337
        g.__name__ = f.__name__
449
344
    return new_decorator
450
345
 
451
346
 
 
347
# common threading-lock decorator
 
348
def with_lock(lockname, debug_name=None):
 
349
    if debug_name is None:
 
350
        debug_name = lockname
 
351
    @decorator
 
352
    def _decorator(unbound):
 
353
        def locked(self, *args, **kw):
 
354
            getattr(self, lockname).acquire()
 
355
            try:
 
356
                return unbound(self, *args, **kw)
 
357
            finally:
 
358
                getattr(self, lockname).release()
 
359
        return locked
 
360
    return _decorator
 
361
 
 
362
 
 
363
@decorator
 
364
def strip_whitespace(f):
 
365
    def _f(*a, **kw):
 
366
        out = f(*a, **kw)
 
367
        orig_len = len(out)
 
368
        out = re.sub(r'\n\s+', '\n', out)
 
369
        out = re.sub(r'[ \t]+', ' ', out)
 
370
        out = re.sub(r'\s+\n', '\n', out)
 
371
        new_len = len(out)
 
372
        log.debug('Saved %sB (%d%%) by stripping whitespace.',
 
373
                  human_size(orig_len - new_len),
 
374
                  round(100.0 - float(new_len) * 100.0 / float(orig_len)))
 
375
        return out
 
376
    return _f
 
377
 
452
378
 
453
379
@decorator
454
380
def lsprof(f):
455
 
 
456
381
    def _f(*a, **kw):
457
382
        from loggerhead.lsprof import profile
458
383
        import cPickle
459
384
        z = time.time()
460
385
        ret, stats = profile(f, *a, **kw)
461
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__,
462
 
            int((time.time() - z) * 1000)))
 
386
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
463
387
        stats.sort()
464
388
        stats.freeze()
465
389
        now = time.time()
466
390
        msec = int(now * 1000) % 1000
467
 
        timestr = time.strftime('%Y%m%d%H%M%S',
468
 
                                time.localtime(now)) + ('%03d' % msec)
 
391
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
469
392
        filename = f.__name__ + '-' + timestr + '.lsprof'
470
393
        cPickle.dump(stats, open(filename, 'w'), 2)
471
394
        return ret
479
402
#         current location along the navigation path (while browsing)
480
403
#     - starting revid (start_revid)
481
404
#         the current beginning of navigation (navigation continues back to
482
 
#         the original revision) -- this defines an 'alternate mainline'
483
 
#         when the user navigates into a branch.
 
405
#         the original revision) -- this may not be along the primary revision
 
406
#         path since the user may have navigated into a branch
484
407
#     - file_id
485
 
#         the file being looked at
486
 
#     - filter_file_id
487
408
#         if navigating the revisions that touched a file
488
409
#     - q (query)
489
410
#         if navigating the revisions that matched a search query
500
421
#         for re-ordering an existing page by different sort
501
422
 
502
423
t_context = threading.local()
503
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
504
 
          'compare_revid', 'sort')
 
424
_valid = ('start_revid', 'file_id', 'q', 'remember', 'compare_revid', 'sort')
505
425
 
506
426
 
507
427
def set_context(map):
510
430
 
511
431
def get_context(**overrides):
512
432
    """
513
 
    Soon to be deprecated.
514
 
 
515
 
 
516
433
    return a context map that may be overriden by specific values passed in,
517
434
    but only contains keys from the list of valid context keys.
518
 
 
 
435
    
519
436
    if 'clear' is set, only the 'remember' context value will be added, and
520
437
    all other context will be omitted.
521
438
    """
527
444
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
528
445
    map.update(overrides)
529
446
    return map
530
 
 
531
 
 
532
 
class Reloader(object):
533
 
    """
534
 
    This class wraps all paste.reloader logic. All methods are @classmethod.
535
 
    """
536
 
 
537
 
    _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
538
 
 
539
 
    @classmethod
540
 
    def _turn_sigterm_into_systemexit(self):
541
 
        """
542
 
        Attempts to turn a SIGTERM exception into a SystemExit exception.
543
 
        """
544
 
        try:
545
 
            import signal
546
 
        except ImportError:
547
 
            return
548
 
 
549
 
        def handle_term(signo, frame):
550
 
            raise SystemExit
551
 
        signal.signal(signal.SIGTERM, handle_term)
552
 
 
553
 
    @classmethod
554
 
    def is_installed(self):
555
 
        return os.environ.get(self._reloader_environ_key)
556
 
 
557
 
    @classmethod
558
 
    def install(self):
559
 
        from paste import reloader
560
 
        reloader.install(int(1))
561
 
 
562
 
    @classmethod
563
 
    def restart_with_reloader(self):
564
 
        """Based on restart_with_monitor from paste.script.serve."""
565
 
        print 'Starting subprocess with file monitor'
566
 
        while 1:
567
 
            args = [sys.executable] + sys.argv
568
 
            new_environ = os.environ.copy()
569
 
            new_environ[self._reloader_environ_key] = 'true'
570
 
            proc = None
571
 
            try:
572
 
                try:
573
 
                    self._turn_sigterm_into_systemexit()
574
 
                    proc = subprocess.Popen(args, env=new_environ)
575
 
                    exit_code = proc.wait()
576
 
                    proc = None
577
 
                except KeyboardInterrupt:
578
 
                    print '^C caught in monitor process'
579
 
                    return 1
580
 
            finally:
581
 
                if (proc is not None
582
 
                    and hasattr(os, 'kill')):
583
 
                    import signal
584
 
                    try:
585
 
                        os.kill(proc.pid, signal.SIGTERM)
586
 
                    except (OSError, IOError):
587
 
                        pass
588
 
 
589
 
            # Reloader always exits with code 3; but if we are
590
 
            # a monitor, any exit code will restart
591
 
            if exit_code != 3:
592
 
                return exit_code
593
 
            print '-'*20, 'Restarting', '-'*20