~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Michael Hudson
  • Date: 2008-02-27 03:36:09 UTC
  • mto: This revision was merged to the branch mainline in revision 146.
  • Revision ID: michael.hudson@canonical.com-20080227033609-6xz80buvasyekl6a
run reindent.py over the loggerhead package

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
#
20
18
#
21
19
 
22
20
import base64
 
21
import cgi
23
22
import datetime
24
23
import logging
25
24
import re
 
25
import sha
26
26
import struct
 
27
import sys
27
28
import threading
28
29
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
 
30
import traceback
 
31
 
41
32
 
42
33
log = logging.getLogger("loggerhead.controllers")
43
34
 
44
35
 
 
36
def timespan(delta):
 
37
    if delta.days > 730:
 
38
        # good grief!
 
39
        return '%d years' % (int(delta.days // 365.25),)
 
40
    if delta.days >= 3:
 
41
        return '%d days' % delta.days
 
42
    seg = []
 
43
    if delta.days > 0:
 
44
        if delta.days == 1:
 
45
            seg.append('1 day')
 
46
        else:
 
47
            seg.append('%d days' % delta.days)
 
48
    hrs = delta.seconds // 3600
 
49
    mins = (delta.seconds % 3600) // 60
 
50
    if hrs > 0:
 
51
        if hrs == 1:
 
52
            seg.append('1 hour')
 
53
        else:
 
54
            seg.append('%d hours' % hrs)
 
55
    if delta.days == 0:
 
56
        if mins > 0:
 
57
            if mins == 1:
 
58
                seg.append('1 minute')
 
59
            else:
 
60
                seg.append('%d minutes' % mins)
 
61
        elif hrs == 0:
 
62
            seg.append('less than a minute')
 
63
    return ', '.join(seg)
 
64
 
 
65
 
 
66
def ago(timestamp):
 
67
    now = datetime.datetime.now()
 
68
    return timespan(now - timestamp) + ' ago'
 
69
 
 
70
 
45
71
def fix_year(year):
46
72
    if year < 70:
47
73
        year += 2000
49
75
        year += 1900
50
76
    return year
51
77
 
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):
 
78
 
 
79
_g_format = '%Y-%m-%d @ %H:%M'
 
80
 
 
81
def format_date(date):
 
82
    if _g_format == 'fancy':
 
83
        return fancy_format_date(date)
 
84
    return date.strftime(_g_format)
 
85
 
 
86
def fancy_format_date(date):
84
87
    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'
 
88
    if delta.days > 300:
 
89
        return date.strftime('%d %b %Y')
106
90
    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):
 
91
        return date.strftime('%d %b %H:%M')
 
92
 
 
93
def set_date_format(format):
 
94
    global _g_format
 
95
    _g_format = format
 
96
 
 
97
 
 
98
class Container (object):
134
99
    """
135
100
    Convert a dict into an object with attributes.
136
101
    """
137
 
 
138
102
    def __init__(self, _dict=None, **kw):
139
 
        self._properties = {}
140
103
        if _dict is not None:
141
104
            for key, value in _dict.iteritems():
142
105
                setattr(self, key, value)
146
109
    def __repr__(self):
147
110
        out = '{ '
148
111
        for key, value in self.__dict__.iteritems():
149
 
            if key.startswith('_') or (getattr(self.__dict__[key],
150
 
                                       '__call__', None) is not None):
 
112
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
151
113
                continue
152
114
            out += '%r => %r, ' % (key, value)
153
115
        out += '}'
154
116
        return out
155
117
 
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
 
118
 
 
119
def clean_revid(revid):
 
120
    if revid == 'missing':
 
121
        return revid
 
122
    return sha.new(revid).hexdigest()
 
123
 
 
124
 
 
125
def obfuscate(text):
 
126
    return ''.join([ '&#%d;' % ord(c) for c in text ])
173
127
 
174
128
 
175
129
def trunc(text, limit=10):
178
132
    return text[:limit] + '...'
179
133
 
180
134
 
 
135
def to_utf8(s):
 
136
    if isinstance(s, unicode):
 
137
        return s.encode('utf-8')
 
138
    return s
 
139
 
 
140
 
181
141
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
182
142
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
183
143
 
184
 
 
185
144
def hide_email(email):
186
145
    """
187
146
    try to obsure any email address in a bazaar committer's name.
201
160
        return '%s at %s' % (username, domains[-2])
202
161
    return '%s at %s' % (username, domains[0])
203
162
 
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
 
163
 
 
164
def triple_factors(min_value=1):
 
165
    factors = (1, 3)
 
166
    index = 0
 
167
    n = 1
 
168
    while True:
 
169
        if n >= min_value:
 
170
            yield n * factors[index]
 
171
        index += 1
 
172
        if index >= len(factors):
 
173
            index = 0
 
174
            n *= 10
 
175
 
 
176
 
 
177
def scan_range(pos, max, pagesize=1):
 
178
    """
 
179
    given a position in a maximum range, return a list of negative and positive
 
180
    jump factors for an hgweb-style triple-factor geometric scan.
 
181
 
 
182
    for example, with pos=20 and max=500, the range would be:
 
183
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
 
184
 
 
185
    i admit this is a very strange way of jumping through revisions.  i didn't
 
186
    invent it. :)
 
187
    """
 
188
    out = []
 
189
    for n in triple_factors(pagesize + 1):
 
190
        if n > max:
 
191
            return out
 
192
        if pos + n < max:
 
193
            out.append(n)
 
194
        if pos - n >= 0:
 
195
            out.insert(0, -n)
 
196
 
212
197
 
213
198
# only do this if unicode turns out to be a problem
214
199
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
215
200
 
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
201
# FIXME: get rid of this method; use fixed_width() and avoid XML().
249
 
 
250
202
def html_clean(s):
251
203
    """
252
204
    clean up a string for html display.  expand any tabs, encode any html
253
205
    entities, and replace spaces with '&nbsp;'.  this is primarily for use
254
206
    in displaying monospace text.
255
207
    """
256
 
    s = html_escape(s.expandtabs())
 
208
    s = cgi.escape(s.expandtabs())
257
209
    s = s.replace(' ', '&nbsp;')
258
210
    return s
259
211
 
260
212
 
 
213
 
261
214
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
262
215
 
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
216
def fixed_width(s):
287
217
    """
288
218
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
298
228
            s = s.decode('utf-8')
299
229
        except UnicodeDecodeError:
300
230
            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/>')
 
231
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
305
232
 
306
233
 
307
234
def fake_permissions(kind, executable):
313
240
    return '-rw-r--r--'
314
241
 
315
242
 
 
243
def if_present(format, value):
 
244
    """
 
245
    format a value using a format string, if the value exists and is not None.
 
246
    """
 
247
    if value is None:
 
248
        return ''
 
249
    return format % value
 
250
 
 
251
 
316
252
def b64(s):
317
253
    s = base64.encodestring(s).replace('\n', '')
318
254
    while (len(s) > 0) and (s[-1] == '='):
340
276
P95_MEG = int(0.9 * MEG)
341
277
P95_GIG = int(0.9 * GIG)
342
278
 
343
 
 
344
279
def human_size(size, min_divisor=0):
345
280
    size = int(size)
346
281
    if (size == 0) and (min_divisor == 0):
375
310
    return out
376
311
 
377
312
 
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
313
def fill_in_navigation(navigation):
387
314
    """
388
315
    given a navigation block (used by the template for the page header), fill
394
321
        navigation.position = 0
395
322
    navigation.count = len(navigation.revid_list)
396
323
    navigation.page_position = navigation.position // navigation.pagesize + 1
397
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
398
 
 - 1)) // navigation.pagesize
 
324
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
399
325
 
400
326
    def get_offset(offset):
401
 
        if (navigation.position + offset < 0) or (
402
 
           navigation.position + offset > navigation.count - 1):
 
327
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
403
328
            return None
404
329
        return navigation.revid_list[navigation.position + offset]
405
330
 
406
 
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
407
331
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
408
332
    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
333
 
415
 
    params = {'filter_file_id': navigation.filter_file_id}
 
334
    params = { 'file_id': navigation.file_id }
416
335
    if getattr(navigation, 'query', None) is not None:
417
336
        params['q'] = navigation.query
418
 
 
419
 
    if getattr(navigation, 'start_revid', None) is not None:
420
 
        params['start_revid'] = start_revno
 
337
    else:
 
338
        params['start_revid'] = navigation.start_revid
421
339
 
422
340
    if navigation.prev_page_revid:
423
 
        navigation.prev_page_url = navigation.branch.context_url(
424
 
            [navigation.scan_url, prev_page_revno], **params)
 
341
        navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
425
342
    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
 
343
        navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **get_context(**params))
 
344
 
 
345
 
 
346
def log_exception(log):
 
347
    for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
 
348
        log.debug(line)
491
349
 
492
350
 
493
351
def decorator(unbound):
494
 
 
495
352
    def new_decorator(f):
496
353
        g = unbound(f)
497
354
        g.__name__ = f.__name__
504
361
    return new_decorator
505
362
 
506
363
 
 
364
# common threading-lock decorator
 
365
def with_lock(lockname, debug_name=None):
 
366
    if debug_name is None:
 
367
        debug_name = lockname
 
368
    @decorator
 
369
    def _decorator(unbound):
 
370
        def locked(self, *args, **kw):
 
371
            getattr(self, lockname).acquire()
 
372
            try:
 
373
                return unbound(self, *args, **kw)
 
374
            finally:
 
375
                getattr(self, lockname).release()
 
376
        return locked
 
377
    return _decorator
 
378
 
 
379
 
 
380
@decorator
 
381
def strip_whitespace(f):
 
382
    def _f(*a, **kw):
 
383
        out = f(*a, **kw)
 
384
        orig_len = len(out)
 
385
        out = re.sub(r'\n\s+', '\n', out)
 
386
        out = re.sub(r'[ \t]+', ' ', out)
 
387
        out = re.sub(r'\s+\n', '\n', out)
 
388
        new_len = len(out)
 
389
        log.debug('Saved %sB (%d%%) by stripping whitespace.',
 
390
                  human_size(orig_len - new_len),
 
391
                  round(100.0 - float(new_len) * 100.0 / float(orig_len)))
 
392
        return out
 
393
    return _f
 
394
 
507
395
 
508
396
@decorator
509
397
def lsprof(f):
510
 
 
511
398
    def _f(*a, **kw):
512
399
        from loggerhead.lsprof import profile
513
400
        import cPickle
514
401
        z = time.time()
515
402
        ret, stats = profile(f, *a, **kw)
516
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__,
517
 
            int((time.time() - z) * 1000)))
 
403
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
518
404
        stats.sort()
519
405
        stats.freeze()
520
406
        now = time.time()
521
407
        msec = int(now * 1000) % 1000
522
 
        timestr = time.strftime('%Y%m%d%H%M%S',
523
 
                                time.localtime(now)) + ('%03d' % (msec,))
 
408
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
524
409
        filename = f.__name__ + '-' + timestr + '.lsprof'
525
410
        cPickle.dump(stats, open(filename, 'w'), 2)
526
411
        return ret
534
419
#         current location along the navigation path (while browsing)
535
420
#     - starting revid (start_revid)
536
421
#         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.
 
422
#         the original revision) -- this may not be along the primary revision
 
423
#         path since the user may have navigated into a branch
539
424
#     - file_id
540
 
#         the file being looked at
541
 
#     - filter_file_id
542
425
#         if navigating the revisions that touched a file
543
426
#     - q (query)
544
427
#         if navigating the revisions that matched a search query
555
438
#         for re-ordering an existing page by different sort
556
439
 
557
440
t_context = threading.local()
558
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
559
 
          'compare_revid', 'sort')
 
441
_valid = ('start_revid', 'file_id', 'q', 'remember', 'compare_revid', 'sort')
560
442
 
561
443
 
562
444
def set_context(map):
565
447
 
566
448
def get_context(**overrides):
567
449
    """
568
 
    Soon to be deprecated.
569
 
 
570
 
 
571
450
    return a context map that may be overriden by specific values passed in,
572
451
    but only contains keys from the list of valid context keys.
573
452
 
582
461
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
583
462
    map.update(overrides)
584
463
    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