~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: j.c.sackett
  • Date: 2011-11-23 20:08:17 UTC
  • mfrom: (459.1.2 add-lock-icon)
  • Revision ID: jonathan.sackett@canonical.com-20111123200817-b1wad8vgo9aa7siw
Updated css to include the privacy icon to match the LP privacy notification style.

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