/loggerhead/trunk

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/loggerhead/trunk

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Ubuntu One Auto Copilot
  • Author(s): Jelmer Vernooij
  • Date: 2023-02-02 11:08:17 UTC
  • mfrom: (545.2.1 lp:loggerhead)
  • Revision ID: otto-copilot@canonical.com-20230202110817-001db22jiwyhfrk3
Fix spelling errors in code

Merged from https://code.launchpad.net/~jelmer/loggerhead/codespell/+merge/436735

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