~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Martin Albisetti
  • Date: 2008-06-20 18:04:33 UTC
  • mto: (157.1.3 loggerhead)
  • mto: This revision was merged to the branch mainline in revision 187.
  • Revision ID: argentina@gmail.com-20080620180433-y5cw7g6lsk5h28x3
Adapt to WSGI

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