~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Robey Pointer
  • Date: 2006-12-11 06:44:19 UTC
  • Revision ID: robey@lag.net-20061211064419-8ssa7mlsiflpmy0c
initial checkin

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
 
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
6
3
#
7
4
# This program is free software; you can redistribute it and/or modify
8
5
# it under the terms of the GNU General Public License as published by
19
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
17
#
21
18
 
22
 
import base64
23
 
import datetime
24
 
import logging
25
19
import re
26
 
import struct
27
 
import threading
28
 
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
41
 
 
42
 
log = logging.getLogger("loggerhead.controllers")
43
 
 
44
 
 
45
 
def fix_year(year):
46
 
    if year < 70:
47
 
        year += 2000
48
 
    if year < 100:
49
 
        year += 1900
50
 
    return year
51
 
 
52
 
# Display of times.
53
 
 
54
 
# date_day -- just the day
55
 
# date_time -- full date with time
56
 
#
57
 
# displaydate -- for use in sentences
58
 
# approximatedate -- for use in tables
59
 
#
60
 
# displaydate and approximatedate return an elementtree <span> Element
61
 
# with the full date in a tooltip.
62
 
 
63
 
 
64
 
def date_day(value):
65
 
    return value.strftime('%Y-%m-%d')
66
 
 
67
 
 
68
 
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'
73
 
 
74
 
 
75
 
def _displaydate(date):
76
 
    delta = abs(datetime.datetime.now() - date)
77
 
    if delta > datetime.timedelta(1, 0, 0):
78
 
        # far in the past or future, display the date
79
 
        return 'on ' + date_day(date)
80
 
    return _approximatedate(date)
81
 
 
82
 
 
83
 
def _approximatedate(date):
84
 
    delta = datetime.datetime.now() - date
85
 
    if abs(delta) > datetime.timedelta(1, 0, 0):
86
 
        # far in the past or future, display the date
87
 
        return date_day(date)
88
 
    future = delta < datetime.timedelta(0, 0, 0)
89
 
    delta = abs(delta)
90
 
    days = delta.days
91
 
    hours = delta.seconds / 3600
92
 
    minutes = (delta.seconds - (3600*hours)) / 60
93
 
    seconds = delta.seconds % 60
94
 
    result = ''
95
 
    if future:
96
 
        result += 'in '
97
 
    if days != 0:
98
 
        amount = days
99
 
        unit = 'day'
100
 
    elif hours != 0:
101
 
        amount = hours
102
 
        unit = 'hour'
103
 
    elif minutes != 0:
104
 
        amount = minutes
105
 
        unit = 'minute'
106
 
    else:
107
 
        amount = seconds
108
 
        unit = 'second'
109
 
    if amount != 1:
110
 
        unit += 's'
111
 
    result += '%s %s' % (amount, unit)
112
 
    if not future:
113
 
        result += ' ago'
114
 
        return result
115
 
 
116
 
 
117
 
def _wrap_with_date_time_title(date, formatted_date):
118
 
    elem = ET.Element("span")
119
 
    elem.text = formatted_date
120
 
    elem.set("title", date_time(date))
121
 
    return elem
122
 
 
123
 
 
124
 
def approximatedate(date):
125
 
    #FIXME: Returns an object instead of a string
126
 
    return _wrap_with_date_time_title(date, _approximatedate(date))
127
 
 
128
 
 
129
 
def displaydate(date):
130
 
    return _wrap_with_date_time_title(date, _displaydate(date))
131
 
 
132
 
 
133
 
class Container(object):
 
20
import sha
 
21
 
 
22
 
 
23
def timespan(delta):
 
24
    if delta.days >= 3:
 
25
        return '%d days' % delta.days
 
26
    seg = []
 
27
    if delta.days > 0:
 
28
        if delta.days == 1:
 
29
            seg.append('1 day')
 
30
        else:
 
31
            seg.append('%d days' % delta.days)
 
32
    hrs = delta.seconds // 3600
 
33
    mins = (delta.seconds % 3600) // 60
 
34
    if hrs > 0:
 
35
        if hrs == 1:
 
36
            seg.append('1 hour')
 
37
        else:
 
38
            seg.append('%d hours' % hrs)
 
39
    if delta.days == 0:
 
40
        if mins > 0:
 
41
            if mins == 1:
 
42
                seg.append('1 minute')
 
43
            else:
 
44
                seg.append('%d minutes' % mins)
 
45
        elif hrs == 0:
 
46
            seg.append('less than a minute')
 
47
    return ', '.join(seg)
 
48
 
 
49
 
 
50
class Container (object):
134
51
    """
135
52
    Convert a dict into an object with attributes.
136
53
    """
137
 
 
138
54
    def __init__(self, _dict=None, **kw):
139
 
        self._properties = {}
140
55
        if _dict is not None:
141
56
            for key, value in _dict.iteritems():
142
57
                setattr(self, key, value)
143
58
        for key, value in kw.iteritems():
144
59
            setattr(self, key, value)
145
60
 
146
 
    def __repr__(self):
147
 
        out = '{ '
148
 
        for key, value in self.__dict__.iteritems():
149
 
            if key.startswith('_') or (getattr(self.__dict__[key],
150
 
                                       '__call__', None) is not None):
151
 
                continue
152
 
            out += '%r => %r, ' % (key, value)
153
 
        out += '}'
154
 
        return out
155
 
 
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
173
 
 
174
 
 
175
 
def trunc(text, limit=10):
176
 
    if len(text) <= limit:
177
 
        return text
178
 
    return text[:limit] + '...'
 
61
 
 
62
def clean_revid(revid):
 
63
    if revid == 'missing':
 
64
        return revid
 
65
    return sha.new(revid).hexdigest()
 
66
 
 
67
 
 
68
def obfuscate(text):
 
69
    return ''.join([ '&#%d;' % ord(c) for c in text ])
179
70
 
180
71
 
181
72
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
182
73
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
183
74
 
184
 
 
185
75
def hide_email(email):
186
76
    """
187
77
    try to obsure any email address in a bazaar committer's name.
201
91
        return '%s at %s' % (username, domains[-2])
202
92
    return '%s at %s' % (username, domains[0])
203
93
 
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
212
 
 
213
 
# only do this if unicode turns out to be a problem
214
 
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
215
 
 
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
 
# FIXME: get rid of this method; use fixed_width() and avoid XML().
249
 
 
250
 
def html_clean(s):
251
 
    """
252
 
    clean up a string for html display.  expand any tabs, encode any html
253
 
    entities, and replace spaces with '&nbsp;'.  this is primarily for use
254
 
    in displaying monospace text.
255
 
    """
256
 
    s = html_escape(s.expandtabs())
257
 
    s = s.replace(' ', '&nbsp;')
258
 
    return s
259
 
 
260
 
 
261
 
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
262
 
 
263
 
 
264
 
def fill_div(s):
265
 
    """
266
 
    CSS is stupid. In some cases we need to replace an empty value with
267
 
    a non breaking space (&nbsp;). There has to be a better way of doing this.
268
 
 
269
 
    return: the same value recieved if not empty, and a '&nbsp;' if it is.
270
 
    """
271
 
    if s is None:
272
 
        return '&nbsp;'
273
 
    elif isinstance(s, int):
274
 
        return s
275
 
    elif not s.strip():
276
 
        return '&nbsp;'
277
 
    else:
278
 
        try:
279
 
            s = s.decode('utf-8')
280
 
        except UnicodeDecodeError:
281
 
            s = s.decode('iso-8859-15')
282
 
        return s
283
 
 
284
 
HSC = HTMLStructureCleaner()
285
 
 
286
 
def fixed_width(s):
287
 
    """
288
 
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
289
 
    chop up the string.
290
 
    """
291
 
    if not isinstance(s, unicode):
292
 
        # this kinda sucks.  file contents are just binary data, and no
293
 
        # encoding metadata is stored, so we need to guess.  this is probably
294
 
        # okay for most code, but for people using things like KOI-8, this
295
 
        # will display gibberish.  we have no way of detecting the correct
296
 
        # encoding to use.
297
 
        try:
298
 
            s = s.decode('utf-8')
299
 
        except UnicodeDecodeError:
300
 
            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/>')
305
 
 
306
 
 
307
 
def fake_permissions(kind, executable):
308
 
    # fake up unix-style permissions given only a "kind" and executable bit
309
 
    if kind == 'directory':
310
 
        return 'drwxr-xr-x'
311
 
    if executable:
312
 
        return '-rwxr-xr-x'
313
 
    return '-rw-r--r--'
314
 
 
315
 
 
316
 
def b64(s):
317
 
    s = base64.encodestring(s).replace('\n', '')
318
 
    while (len(s) > 0) and (s[-1] == '='):
319
 
        s = s[:-1]
320
 
    return s
321
 
 
322
 
 
323
 
def uniq(uniqs, s):
324
 
    """
325
 
    turn a potentially long string into a unique smaller string.
326
 
    """
327
 
    if s in uniqs:
328
 
        return uniqs[s]
329
 
    uniqs[type(None)] = next = uniqs.get(type(None), 0) + 1
330
 
    x = struct.pack('>I', next)
331
 
    while (len(x) > 1) and (x[0] == '\x00'):
332
 
        x = x[1:]
333
 
    uniqs[s] = b64(x)
334
 
    return uniqs[s]
335
 
 
336
 
 
337
 
KILO = 1024
338
 
MEG = 1024 * KILO
339
 
GIG = 1024 * MEG
340
 
P95_MEG = int(0.9 * MEG)
341
 
P95_GIG = int(0.9 * GIG)
342
 
 
343
 
 
344
 
def human_size(size, min_divisor=0):
345
 
    size = int(size)
346
 
    if (size == 0) and (min_divisor == 0):
347
 
        return '0'
348
 
    if (size < 512) and (min_divisor == 0):
349
 
        return str(size)
350
 
 
351
 
    if (size >= P95_GIG) or (min_divisor >= GIG):
352
 
        divisor = GIG
353
 
    elif (size >= P95_MEG) or (min_divisor >= MEG):
354
 
        divisor = MEG
355
 
    else:
356
 
        divisor = KILO
357
 
 
358
 
    dot = size % divisor
359
 
    base = size - dot
360
 
    dot = dot * 10 // divisor
361
 
    base //= divisor
362
 
    if dot >= 10:
363
 
        base += 1
364
 
        dot -= 10
365
 
 
366
 
    out = str(base)
367
 
    if (base < 100) and (dot != 0):
368
 
        out += '.%d' % (dot,)
369
 
    if divisor == KILO:
370
 
        out += 'K'
371
 
    elif divisor == MEG:
372
 
        out += 'M'
373
 
    elif divisor == GIG:
374
 
        out += 'G'
375
 
    return out
376
 
 
377
 
 
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
 
def fill_in_navigation(navigation):
387
 
    """
388
 
    given a navigation block (used by the template for the page header), fill
389
 
    in useful calculated values.
390
 
    """
391
 
    if navigation.revid in navigation.revid_list: # XXX is this always true?
392
 
        navigation.position = navigation.revid_list.index(navigation.revid)
393
 
    else:
394
 
        navigation.position = 0
395
 
    navigation.count = len(navigation.revid_list)
396
 
    navigation.page_position = navigation.position // navigation.pagesize + 1
397
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
398
 
 - 1)) // navigation.pagesize
399
 
 
400
 
    def get_offset(offset):
401
 
        if (navigation.position + offset < 0) or (
402
 
           navigation.position + offset > navigation.count - 1):
403
 
            return None
404
 
        return navigation.revid_list[navigation.position + offset]
405
 
 
406
 
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
407
 
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
408
 
    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}
416
 
    if getattr(navigation, 'query', None) is not None:
417
 
        params['q'] = navigation.query
418
 
 
419
 
    if getattr(navigation, 'start_revid', None) is not None:
420
 
        params['start_revid'] = start_revno
421
 
 
422
 
    if navigation.prev_page_revid:
423
 
        navigation.prev_page_url = navigation.branch.context_url(
424
 
            [navigation.scan_url, prev_page_revno], **params)
425
 
    if navigation.next_page_revid:
426
 
        navigation.next_page_url = navigation.branch.context_url(
427
 
            [navigation.scan_url, next_page_revno], **params)
428
 
 
429
 
 
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
491
 
 
492
 
 
493
 
def decorator(unbound):
494
 
 
495
 
    def new_decorator(f):
496
 
        g = unbound(f)
497
 
        g.__name__ = f.__name__
498
 
        g.__doc__ = f.__doc__
499
 
        g.__dict__.update(f.__dict__)
500
 
        return g
501
 
    new_decorator.__name__ = unbound.__name__
502
 
    new_decorator.__doc__ = unbound.__doc__
503
 
    new_decorator.__dict__.update(unbound.__dict__)
504
 
    return new_decorator
505
 
 
506
 
 
507
 
 
508
 
@decorator
509
 
def lsprof(f):
510
 
 
511
 
    def _f(*a, **kw):
512
 
        from loggerhead.lsprof import profile
513
 
        import cPickle
514
 
        z = time.time()
515
 
        ret, stats = profile(f, *a, **kw)
516
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__,
517
 
            int((time.time() - z) * 1000)))
518
 
        stats.sort()
519
 
        stats.freeze()
520
 
        now = time.time()
521
 
        msec = int(now * 1000) % 1000
522
 
        timestr = time.strftime('%Y%m%d%H%M%S',
523
 
                                time.localtime(now)) + ('%03d' % (msec,))
524
 
        filename = f.__name__ + '-' + timestr + '.lsprof'
525
 
        cPickle.dump(stats, open(filename, 'w'), 2)
526
 
        return ret
527
 
    return _f
528
 
 
529
 
 
530
 
# just thinking out loud here...
531
 
#
532
 
# so, when browsing around, there are 5 pieces of context, most optional:
533
 
#     - current revid
534
 
#         current location along the navigation path (while browsing)
535
 
#     - starting revid (start_revid)
536
 
#         the current beginning of navigation (navigation continues back to
537
 
#         the original revision) -- this defines an 'alternate mainline'
538
 
#         when the user navigates into a branch.
539
 
#     - file_id
540
 
#         the file being looked at
541
 
#     - filter_file_id
542
 
#         if navigating the revisions that touched a file
543
 
#     - q (query)
544
 
#         if navigating the revisions that matched a search query
545
 
#     - remember
546
 
#         a previous revision to remember for future comparisons
547
 
#
548
 
# current revid is given on the url path.  the rest are optional components
549
 
# in the url params.
550
 
#
551
 
# other transient things can be set:
552
 
#     - compare_revid
553
 
#         to compare one revision to another, on /revision only
554
 
#     - sort
555
 
#         for re-ordering an existing page by different sort
556
 
 
557
 
t_context = threading.local()
558
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
559
 
          'compare_revid', 'sort')
560
 
 
561
 
 
562
 
def set_context(map):
563
 
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
564
 
 
565
 
 
566
 
def get_context(**overrides):
567
 
    """
568
 
    Soon to be deprecated.
569
 
 
570
 
 
571
 
    return a context map that may be overriden by specific values passed in,
572
 
    but only contains keys from the list of valid context keys.
573
 
 
574
 
    if 'clear' is set, only the 'remember' context value will be added, and
575
 
    all other context will be omitted.
576
 
    """
577
 
    map = dict()
578
 
    if overrides.get('clear', False):
579
 
        map['remember'] = t_context.map.get('remember', None)
580
 
    else:
581
 
        map.update(t_context.map)
582
 
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
583
 
    map.update(overrides)
584
 
    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
666
 
 
667
 
 
668
 
def convert_to_json_ready(obj):
669
 
    if isinstance(obj, Container):
670
 
        d = obj.__dict__.copy()
671
 
        del d['_properties']
672
 
        return d
673
 
    elif isinstance(obj, datetime.datetime):
674
 
        return tuple(obj.utctimetuple())
675
 
    raise TypeError(repr(obj) + " is not JSON serializable")
 
94
    
 
95
def triple_factors():
 
96
    factors = (1, 3)
 
97
    index = 0
 
98
    n = 1
 
99
    while True:
 
100
        yield n * factors[index]
 
101
        index += 1
 
102
        if index >= len(factors):
 
103
            index = 0
 
104
            n *= 10
 
105
 
 
106
 
 
107
def scan_range(pos, max):
 
108
    """
 
109
    given a position in a maximum range, return a list of negative and positive
 
110
    jump factors for an hgweb-style triple-factor geometric scan.
 
111
    
 
112
    for example, with pos=20 and max=500, the range would be:
 
113
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
 
114
    
 
115
    i admit this is a very strange way of jumping through revisions.  i didn't
 
116
    invent it. :)
 
117
    """
 
118
    out = []
 
119
    for n in triple_factors():
 
120
        if n > max:
 
121
            return out
 
122
        if pos + n < max:
 
123
            out.append(n)
 
124
        if pos - n >= 0:
 
125
            out.insert(0, -n)
 
126