~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

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

Show diffs side-by-side

added added

removed removed

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