~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Robert Collins
  • Date: 2012-02-02 07:42:24 UTC
  • Revision ID: robertc@robertcollins.net-20120202074224-ujea2ocm1u1ws1en
    - Make tz calculations consistent and use UTC in the UI everywhere we show
      a precise timestamp. (Robert Collins, #594591)

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)
2
4
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
3
5
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
4
6
#
17
19
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
20
#
19
21
 
20
 
try:
21
 
    from xml.etree import ElementTree as ET
22
 
except ImportError:
23
 
    from elementtree import ElementTree as ET
24
 
 
25
22
import base64
26
 
import cgi
27
23
import datetime
28
24
import logging
29
25
import re
30
 
import sha
31
26
import struct
32
 
import sys
33
27
import threading
34
28
import time
35
 
import traceback
36
 
 
 
29
import sys
 
30
import os
 
31
import subprocess
 
32
 
 
33
try:
 
34
    from xml.etree import ElementTree as ET
 
35
except ImportError:
 
36
    from elementtree import ElementTree as ET
 
37
 
 
38
from bzrlib import urlutils
 
39
 
 
40
from simpletal.simpleTALUtils import HTMLStructureCleaner
37
41
 
38
42
log = logging.getLogger("loggerhead.controllers")
39
43
 
 
44
 
40
45
def fix_year(year):
41
46
    if year < 70:
42
47
        year += 2000
47
52
# Display of times.
48
53
 
49
54
# date_day -- just the day
50
 
# date_time -- full date with time
 
55
# date_time -- full date with time (UTC)
51
56
#
52
 
# displaydate -- for use in sentences
53
57
# approximatedate -- for use in tables
54
58
#
55
 
# displaydate and approximatedate return an elementtree <span> Element
56
 
# with the full date in a tooltip.
 
59
# approximatedate return an elementtree <span> Element
 
60
# with the full date (UTC) in a tooltip.
 
61
 
57
62
 
58
63
def date_day(value):
59
64
    return value.strftime('%Y-%m-%d')
60
65
 
61
66
 
62
67
def date_time(value):
63
 
    return value.strftime('%Y-%m-%d %T')
64
 
 
65
 
 
66
 
def _displaydate(date):
67
 
    delta = abs(datetime.datetime.now() - date)
68
 
    if delta > datetime.timedelta(1, 0, 0):
69
 
        # far in the past or future, display the date
70
 
        return 'on ' + date_day(date)
71
 
    return _approximatedate(date)
 
68
    if value is not None:
 
69
        # Note: this assumes that the value is UTC in some fashion.
 
70
        return value.strftime('%Y-%m-%d %H:%M:%S UTC')
 
71
    else:
 
72
        return 'N/A'
72
73
 
73
74
 
74
75
def _approximatedate(date):
117
118
    return _wrap_with_date_time_title(date, _approximatedate(date))
118
119
 
119
120
 
120
 
def displaydate(date):
121
 
    return _wrap_with_date_time_title(date, _displaydate(date))
122
 
 
123
 
 
124
 
class Container (object):
 
121
class Container(object):
125
122
    """
126
123
    Convert a dict into an object with attributes.
127
124
    """
 
125
 
128
126
    def __init__(self, _dict=None, **kw):
 
127
        self._properties = {}
129
128
        if _dict is not None:
130
129
            for key, value in _dict.iteritems():
131
130
                setattr(self, key, value)
135
134
    def __repr__(self):
136
135
        out = '{ '
137
136
        for key, value in self.__dict__.iteritems():
138
 
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
 
137
            if key.startswith('_') or (getattr(self.__dict__[key],
 
138
                                       '__call__', None) is not None):
139
139
                continue
140
140
            out += '%r => %r, ' % (key, value)
141
141
        out += '}'
142
142
        return out
143
143
 
144
 
 
145
 
def clean_revid(revid):
146
 
    if revid == 'missing':
147
 
        return revid
148
 
    return sha.new(revid).hexdigest()
149
 
 
150
 
 
151
 
def obfuscate(text):
152
 
    return ''.join([ '&#%d;' % ord(c) for c in text ])
 
144
    def __getattr__(self, attr):
 
145
        """Used for handling things that aren't already available."""
 
146
        if attr.startswith('_') or attr not in self._properties:
 
147
            raise AttributeError('No attribute: %s' % (attr,))
 
148
        val = self._properties[attr](self, attr)
 
149
        setattr(self, attr, val)
 
150
        return val
 
151
 
 
152
    def _set_property(self, attr, prop_func):
 
153
        """Set a function that will be called when an attribute is desired.
 
154
 
 
155
        We will cache the return value, so the function call should be
 
156
        idempotent. We will pass 'self' and the 'attr' name when triggered.
 
157
        """
 
158
        if attr.startswith('_'):
 
159
            raise ValueError("Cannot create properties that start with _")
 
160
        self._properties[attr] = prop_func
153
161
 
154
162
 
155
163
def trunc(text, limit=10):
158
166
    return text[:limit] + '...'
159
167
 
160
168
 
161
 
def to_utf8(s):
162
 
    if isinstance(s, unicode):
163
 
        return s.encode('utf-8')
164
 
    return s
165
 
 
166
 
 
167
169
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
168
170
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
169
171
 
 
172
 
170
173
def hide_email(email):
171
174
    """
172
175
    try to obsure any email address in a bazaar committer's name.
186
189
        return '%s at %s' % (username, domains[-2])
187
190
    return '%s at %s' % (username, domains[0])
188
191
 
189
 
 
190
 
def triple_factors(min_value=1):
191
 
    factors = (1, 3)
192
 
    index = 0
193
 
    n = 1
194
 
    while True:
195
 
        if n >= min_value:
196
 
            yield n * factors[index]
197
 
        index += 1
198
 
        if index >= len(factors):
199
 
            index = 0
200
 
            n *= 10
201
 
 
202
 
 
203
 
def scan_range(pos, max, pagesize=1):
204
 
    """
205
 
    given a position in a maximum range, return a list of negative and positive
206
 
    jump factors for an hgweb-style triple-factor geometric scan.
207
 
 
208
 
    for example, with pos=20 and max=500, the range would be:
209
 
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
210
 
 
211
 
    i admit this is a very strange way of jumping through revisions.  i didn't
212
 
    invent it. :)
213
 
    """
214
 
    out = []
215
 
    for n in triple_factors(pagesize + 1):
216
 
        if n > max:
217
 
            return out
218
 
        if pos + n < max:
219
 
            out.append(n)
220
 
        if pos - n >= 0:
221
 
            out.insert(0, -n)
222
 
 
 
192
def hide_emails(emails):
 
193
    """
 
194
    try to obscure any email address in a list of bazaar committers' names.
 
195
    """
 
196
    result = []
 
197
    for email in emails:
 
198
        result.append(hide_email(email))
 
199
    return result
223
200
 
224
201
# only do this if unicode turns out to be a problem
225
202
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
226
203
 
 
204
# Can't be a dict; &amp; needs to be done first.
 
205
html_entity_subs = [
 
206
    ("&", "&amp;"),
 
207
    ('"', "&quot;"),
 
208
    ("'", "&#39;"), # &apos; is defined in XML, but not HTML.
 
209
    (">", "&gt;"),
 
210
    ("<", "&lt;"),
 
211
    ]
 
212
 
 
213
 
 
214
def html_escape(s):
 
215
    """Transform dangerous (X)HTML characters into entities.
 
216
 
 
217
    Like cgi.escape, except also escaping \" and '. This makes it safe to use
 
218
    in both attribute and element content.
 
219
 
 
220
    If you want to safely fill a format string with escaped values, use
 
221
    html_format instead
 
222
    """
 
223
    for char, repl in html_entity_subs:
 
224
        s = s.replace(char, repl)
 
225
    return s
 
226
 
 
227
 
 
228
def html_format(template, *args):
 
229
    """Safely format an HTML template string, escaping the arguments.
 
230
 
 
231
    The template string must not be user-controlled; it will not be escaped.
 
232
    """
 
233
    return template % tuple(html_escape(arg) for arg in args)
 
234
 
 
235
 
227
236
# FIXME: get rid of this method; use fixed_width() and avoid XML().
 
237
 
228
238
def html_clean(s):
229
239
    """
230
240
    clean up a string for html display.  expand any tabs, encode any html
231
241
    entities, and replace spaces with '&nbsp;'.  this is primarily for use
232
242
    in displaying monospace text.
233
243
    """
234
 
    s = cgi.escape(s.expandtabs())
 
244
    s = html_escape(s.expandtabs())
235
245
    s = s.replace(' ', '&nbsp;')
236
246
    return s
237
247
 
238
248
 
239
 
 
240
249
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
241
250
 
 
251
 
 
252
def fill_div(s):
 
253
    """
 
254
    CSS is stupid. In some cases we need to replace an empty value with
 
255
    a non breaking space (&nbsp;). There has to be a better way of doing this.
 
256
 
 
257
    return: the same value recieved if not empty, and a '&nbsp;' if it is.
 
258
    """
 
259
    if s is None:
 
260
        return '&nbsp;'
 
261
    elif isinstance(s, int):
 
262
        return s
 
263
    elif not s.strip():
 
264
        return '&nbsp;'
 
265
    else:
 
266
        try:
 
267
            s = s.decode('utf-8')
 
268
        except UnicodeDecodeError:
 
269
            s = s.decode('iso-8859-15')
 
270
        return s
 
271
 
 
272
HSC = HTMLStructureCleaner()
 
273
 
242
274
def fixed_width(s):
243
275
    """
244
276
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
254
286
            s = s.decode('utf-8')
255
287
        except UnicodeDecodeError:
256
288
            s = s.decode('iso-8859-15')
257
 
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
 
289
 
 
290
    s = html_escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
 
291
 
 
292
    return HSC.clean(s).replace('\n', '<br/>')
258
293
 
259
294
 
260
295
def fake_permissions(kind, executable):
266
301
    return '-rw-r--r--'
267
302
 
268
303
 
269
 
def if_present(format, value):
270
 
    """
271
 
    format a value using a format string, if the value exists and is not None.
272
 
    """
273
 
    if value is None:
274
 
        return ''
275
 
    return format % value
276
 
 
277
 
 
278
304
def b64(s):
279
305
    s = base64.encodestring(s).replace('\n', '')
280
306
    while (len(s) > 0) and (s[-1] == '='):
302
328
P95_MEG = int(0.9 * MEG)
303
329
P95_GIG = int(0.9 * GIG)
304
330
 
 
331
 
305
332
def human_size(size, min_divisor=0):
306
333
    size = int(size)
307
334
    if (size == 0) and (min_divisor == 0):
336
363
    return out
337
364
 
338
365
 
 
366
def local_path_from_url(url):
 
367
    """Convert Bazaar URL to local path, ignoring readonly+ prefix"""
 
368
    readonly_prefix = 'readonly+'
 
369
    if url.startswith(readonly_prefix):
 
370
        url = url[len(readonly_prefix):]
 
371
    return urlutils.local_path_from_url(url)
 
372
 
 
373
 
339
374
def fill_in_navigation(navigation):
340
375
    """
341
376
    given a navigation block (used by the template for the page header), fill
347
382
        navigation.position = 0
348
383
    navigation.count = len(navigation.revid_list)
349
384
    navigation.page_position = navigation.position // navigation.pagesize + 1
350
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
 
385
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
 
386
 - 1)) // navigation.pagesize
351
387
 
352
388
    def get_offset(offset):
353
 
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
 
389
        if (navigation.position + offset < 0) or (
 
390
           navigation.position + offset > navigation.count - 1):
354
391
            return None
355
392
        return navigation.revid_list[navigation.position + offset]
356
393
 
 
394
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
357
395
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
358
396
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
359
 
    prev_page_revno = navigation.branch.history.get_revno(
 
397
    prev_page_revno = navigation.history.get_revno(
360
398
            navigation.prev_page_revid)
361
 
    next_page_revno = navigation.branch.history.get_revno(
 
399
    next_page_revno = navigation.history.get_revno(
362
400
            navigation.next_page_revid)
363
 
    start_revno = navigation.branch._history.get_revno(navigation.start_revid)
 
401
    start_revno = navigation.history.get_revno(navigation.start_revid)
364
402
 
365
 
    params = { 'filter_file_id': navigation.filter_file_id }
 
403
    params = {'filter_file_id': navigation.filter_file_id}
366
404
    if getattr(navigation, 'query', None) is not None:
367
405
        params['q'] = navigation.query
368
406
 
377
415
            [navigation.scan_url, next_page_revno], **params)
378
416
 
379
417
 
380
 
def log_exception(log):
381
 
    for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
382
 
        log.debug(line)
 
418
def directory_breadcrumbs(path, is_root, view):
 
419
    """
 
420
    Generate breadcrumb information from the directory path given
 
421
 
 
422
    The path given should be a path up to any branch that is currently being
 
423
    served
 
424
 
 
425
    Arguments:
 
426
    path -- The path to convert into breadcrumbs
 
427
    is_root -- Whether or not loggerhead is serving a branch at its root
 
428
    view -- The type of view we are showing (files, changes etc)
 
429
    """
 
430
    # Is our root directory itself a branch?
 
431
    if is_root:
 
432
        breadcrumbs = [{
 
433
            'dir_name': path,
 
434
            'path': '',
 
435
            'suffix': view,
 
436
        }]
 
437
    else:
 
438
        # Create breadcrumb trail for the path leading up to the branch
 
439
        breadcrumbs = [{
 
440
            'dir_name': "(root)",
 
441
            'path': '',
 
442
            'suffix': '',
 
443
        }]
 
444
        if path != '/':
 
445
            dir_parts = path.strip('/').split('/')
 
446
            for index, dir_name in enumerate(dir_parts):
 
447
                breadcrumbs.append({
 
448
                    'dir_name': dir_name,
 
449
                    'path': '/'.join(dir_parts[:index + 1]),
 
450
                    'suffix': '',
 
451
                })
 
452
            # If we are not in the directory view, the last crumb is a branch,
 
453
            # so we need to specify a view
 
454
            if view != 'directory':
 
455
                breadcrumbs[-1]['suffix'] = '/' + view
 
456
    return breadcrumbs
 
457
 
 
458
 
 
459
def branch_breadcrumbs(path, inv, view):
 
460
    """
 
461
    Generate breadcrumb information from the branch path given
 
462
 
 
463
    The path given should be a path that exists within a branch
 
464
 
 
465
    Arguments:
 
466
    path -- The path to convert into breadcrumbs
 
467
    inv -- Inventory to get file information from
 
468
    view -- The type of view we are showing (files, changes etc)
 
469
    """
 
470
    dir_parts = path.strip('/').split('/')
 
471
    inner_breadcrumbs = []
 
472
    for index, dir_name in enumerate(dir_parts):
 
473
        inner_breadcrumbs.append({
 
474
            'dir_name': dir_name,
 
475
            'path': '/'.join(dir_parts[:index + 1]),
 
476
            'suffix': '/' + view,
 
477
        })
 
478
    return inner_breadcrumbs
383
479
 
384
480
 
385
481
def decorator(unbound):
 
482
 
386
483
    def new_decorator(f):
387
484
        g = unbound(f)
388
485
        g.__name__ = f.__name__
395
492
    return new_decorator
396
493
 
397
494
 
398
 
# common threading-lock decorator
399
 
def with_lock(lockname, debug_name=None):
400
 
    if debug_name is None:
401
 
        debug_name = lockname
402
 
    @decorator
403
 
    def _decorator(unbound):
404
 
        def locked(self, *args, **kw):
405
 
            getattr(self, lockname).acquire()
406
 
            try:
407
 
                return unbound(self, *args, **kw)
408
 
            finally:
409
 
                getattr(self, lockname).release()
410
 
        return locked
411
 
    return _decorator
412
 
 
413
 
 
414
 
@decorator
415
 
def strip_whitespace(f):
416
 
    def _f(*a, **kw):
417
 
        out = f(*a, **kw)
418
 
        orig_len = len(out)
419
 
        out = re.sub(r'\n\s+', '\n', out)
420
 
        out = re.sub(r'[ \t]+', ' ', out)
421
 
        out = re.sub(r'\s+\n', '\n', out)
422
 
        new_len = len(out)
423
 
        log.debug('Saved %sB (%d%%) by stripping whitespace.',
424
 
                  human_size(orig_len - new_len),
425
 
                  round(100.0 - float(new_len) * 100.0 / float(orig_len)))
426
 
        return out
427
 
    return _f
428
 
 
429
495
 
430
496
@decorator
431
497
def lsprof(f):
 
498
 
432
499
    def _f(*a, **kw):
433
500
        from loggerhead.lsprof import profile
434
501
        import cPickle
435
502
        z = time.time()
436
503
        ret, stats = profile(f, *a, **kw)
437
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
 
504
        log.debug('Finished profiled %s in %d msec.' % (f.__name__,
 
505
            int((time.time() - z) * 1000)))
438
506
        stats.sort()
439
507
        stats.freeze()
440
508
        now = time.time()
441
509
        msec = int(now * 1000) % 1000
442
 
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
 
510
        timestr = time.strftime('%Y%m%d%H%M%S',
 
511
                                time.localtime(now)) + ('%03d' % (msec,))
443
512
        filename = f.__name__ + '-' + timestr + '.lsprof'
444
513
        cPickle.dump(stats, open(filename, 'w'), 2)
445
514
        return ret
474
543
#         for re-ordering an existing page by different sort
475
544
 
476
545
t_context = threading.local()
477
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
478
 
          'compare_revid', 'sort')
 
546
_valid = (
 
547
    'start_revid', 'filter_file_id', 'q', 'remember', 'compare_revid', 'sort')
479
548
 
480
549
 
481
550
def set_context(map):
501
570
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
502
571
    map.update(overrides)
503
572
    return map
 
573
 
 
574
 
 
575
class Reloader(object):
 
576
    """
 
577
    This class wraps all paste.reloader logic. All methods are @classmethod.
 
578
    """
 
579
 
 
580
    _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
 
581
 
 
582
    @classmethod
 
583
    def _turn_sigterm_into_systemexit(cls):
 
584
        """
 
585
        Attempts to turn a SIGTERM exception into a SystemExit exception.
 
586
        """
 
587
        try:
 
588
            import signal
 
589
        except ImportError:
 
590
            return
 
591
 
 
592
        def handle_term(signo, frame):
 
593
            raise SystemExit
 
594
        signal.signal(signal.SIGTERM, handle_term)
 
595
 
 
596
    @classmethod
 
597
    def is_installed(cls):
 
598
        return os.environ.get(cls._reloader_environ_key)
 
599
 
 
600
    @classmethod
 
601
    def install(cls):
 
602
        from paste import reloader
 
603
        reloader.install(int(1))
 
604
 
 
605
    @classmethod
 
606
    def restart_with_reloader(cls):
 
607
        """Based on restart_with_monitor from paste.script.serve."""
 
608
        print 'Starting subprocess with file monitor'
 
609
        while True:
 
610
            args = [sys.executable] + sys.argv
 
611
            new_environ = os.environ.copy()
 
612
            new_environ[cls._reloader_environ_key] = 'true'
 
613
            proc = None
 
614
            try:
 
615
                try:
 
616
                    cls._turn_sigterm_into_systemexit()
 
617
                    proc = subprocess.Popen(args, env=new_environ)
 
618
                    exit_code = proc.wait()
 
619
                    proc = None
 
620
                except KeyboardInterrupt:
 
621
                    print '^C caught in monitor process'
 
622
                    return 1
 
623
            finally:
 
624
                if (proc is not None
 
625
                    and getattr(os, 'kill', None) is not None):
 
626
                    import signal
 
627
                    try:
 
628
                        os.kill(proc.pid, signal.SIGTERM)
 
629
                    except (OSError, IOError):
 
630
                        pass
 
631
 
 
632
            # Reloader always exits with code 3; but if we are
 
633
            # a monitor, any exit code will restart
 
634
            if exit_code != 3:
 
635
                return exit_code
 
636
            print '-'*20, 'Restarting', '-'*20
 
637
 
 
638
 
 
639
def convert_file_errors(application):
 
640
    """WSGI wrapper to convert some file errors to Paste exceptions"""
 
641
    def new_application(environ, start_response):
 
642
        try:
 
643
            return application(environ, start_response)
 
644
        except (IOError, OSError), e:
 
645
            import errno
 
646
            from paste import httpexceptions
 
647
            if e.errno == errno.ENOENT:
 
648
                raise httpexceptions.HTTPNotFound()
 
649
            elif e.errno == errno.EACCES:
 
650
                raise httpexceptions.HTTPForbidden()
 
651
            else:
 
652
                raise
 
653
    return new_application
 
654
 
 
655
 
 
656
def convert_to_json_ready(obj):
 
657
    if isinstance(obj, Container):
 
658
        d = obj.__dict__.copy()
 
659
        del d['_properties']
 
660
        return d
 
661
    elif isinstance(obj, datetime.datetime):
 
662
        return tuple(obj.utctimetuple())
 
663
    raise TypeError(repr(obj) + " is not JSON serializable")