/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>
3
5
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
4
6
#
14
16
#
15
17
# You should have received a copy of the GNU General Public License
16
18
# along with this program; if not, write to the Free Software
17
 
# 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
18
20
#
19
21
 
20
 
try:
21
 
    from xml.etree import ElementTree as ET
22
 
except ImportError:
23
 
    from elementtree import ElementTree as ET
 
22
from __future__ import print_function
24
23
 
25
24
import base64
26
 
import cgi
27
25
import datetime
28
26
import logging
 
27
import os
29
28
import re
30
29
import struct
 
30
import subprocess
 
31
import sys
31
32
import threading
32
33
import time
 
34
from xml.etree import ElementTree as ET
33
35
 
 
36
import bleach
 
37
from breezy import urlutils
34
38
 
35
39
log = logging.getLogger("loggerhead.controllers")
36
40
 
 
41
 
37
42
def fix_year(year):
38
43
    if year < 70:
39
44
        year += 2000
44
49
# Display of times.
45
50
 
46
51
# date_day -- just the day
47
 
# date_time -- full date with time
 
52
# date_time -- full date with time (UTC)
48
53
#
49
 
# displaydate -- for use in sentences
50
54
# approximatedate -- for use in tables
51
55
#
52
 
# displaydate and approximatedate return an elementtree <span> Element
53
 
# with the full date in a tooltip.
 
56
# approximatedate return an elementtree <span> Element
 
57
# with the full date (UTC) in a tooltip.
 
58
 
54
59
 
55
60
def date_day(value):
56
61
    return value.strftime('%Y-%m-%d')
57
62
 
58
63
 
59
64
def date_time(value):
60
 
    return value.strftime('%Y-%m-%d %T')
61
 
 
62
 
 
63
 
def _displaydate(date):
64
 
    delta = abs(datetime.datetime.now() - date)
65
 
    if delta > datetime.timedelta(1, 0, 0):
66
 
        # far in the past or future, display the date
67
 
        return 'on ' + date_day(date)
68
 
    return _approximatedate(date)
 
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'
69
70
 
70
71
 
71
72
def _approximatedate(date):
72
 
    delta = datetime.datetime.now() - date
73
 
    if abs(delta) > datetime.timedelta(1, 0, 0):
74
 
        # far in the past or future, display the date
75
 
        return date_day(date)
 
73
    if date is None:
 
74
        return 'Never'
 
75
    delta = datetime.datetime.utcnow() - date
76
76
    future = delta < datetime.timedelta(0, 0, 0)
77
77
    delta = abs(delta)
 
78
    years = delta.days // 365
 
79
    months = delta.days // 30 # This is approximate.
78
80
    days = delta.days
79
 
    hours = delta.seconds / 3600
 
81
    hours = delta.seconds // 3600
80
82
    minutes = (delta.seconds - (3600*hours)) / 60
81
83
    seconds = delta.seconds % 60
82
84
    result = ''
83
85
    if future:
84
86
        result += 'in '
85
 
    if days != 0:
 
87
    if years != 0:
 
88
        amount = years
 
89
        unit = 'year'
 
90
    elif months != 0:
 
91
        amount = months
 
92
        unit = 'month'
 
93
    elif days != 0:
86
94
        amount = days
87
95
        unit = 'day'
88
96
    elif hours != 0:
96
104
        unit = 'second'
97
105
    if amount != 1:
98
106
        unit += 's'
99
 
    result += '%s %s' % (amount, unit)
 
107
    result += '%s %s' % (int(amount), unit)
100
108
    if not future:
101
109
        result += ' ago'
102
 
        return result
 
110
    return result
103
111
 
104
112
 
105
113
def _wrap_with_date_time_title(date, formatted_date):
114
122
    return _wrap_with_date_time_title(date, _approximatedate(date))
115
123
 
116
124
 
117
 
def displaydate(date):
118
 
    return _wrap_with_date_time_title(date, _displaydate(date))
119
 
 
120
 
 
121
 
class Container (object):
 
125
class Container(object):
122
126
    """
123
127
    Convert a dict into an object with attributes.
124
128
    """
 
129
 
125
130
    def __init__(self, _dict=None, **kw):
 
131
        self._properties = {}
126
132
        if _dict is not None:
127
 
            for key, value in _dict.iteritems():
 
133
            for key, value in _dict.items():
128
134
                setattr(self, key, value)
129
 
        for key, value in kw.iteritems():
 
135
        for key, value in kw.items():
130
136
            setattr(self, key, value)
131
137
 
132
138
    def __repr__(self):
133
139
        out = '{ '
134
 
        for key, value in self.__dict__.iteritems():
135
 
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
 
140
        for key, value in self.__dict__.items():
 
141
            if key.startswith('_') or (getattr(self.__dict__[key],
 
142
                                       '__call__', None) is not None):
136
143
                continue
137
144
            out += '%r => %r, ' % (key, value)
138
145
        out += '}'
139
146
        return out
140
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
 
141
166
 
142
167
def trunc(text, limit=10):
143
168
    if len(text) <= limit:
148
173
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
149
174
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
150
175
 
 
176
 
151
177
def hide_email(email):
152
178
    """
153
 
    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.
154
180
    """
155
181
    m = STANDARD_PATTERN.search(email)
156
182
    if m is not None:
167
193
        return '%s at %s' % (username, domains[-2])
168
194
    return '%s at %s' % (username, domains[0])
169
195
 
 
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
170
204
 
171
205
# only do this if unicode turns out to be a problem
172
206
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
173
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
 
174
240
# FIXME: get rid of this method; use fixed_width() and avoid XML().
 
241
 
175
242
def html_clean(s):
176
243
    """
177
244
    clean up a string for html display.  expand any tabs, encode any html
178
245
    entities, and replace spaces with '&nbsp;'.  this is primarily for use
179
246
    in displaying monospace text.
180
247
    """
181
 
    s = cgi.escape(s.expandtabs())
 
248
    s = html_escape(s.expandtabs())
182
249
    s = s.replace(' ', '&nbsp;')
183
250
    return s
184
251
 
185
252
 
186
 
 
187
253
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
188
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
 
189
281
def fixed_width(s):
190
282
    """
191
283
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
192
284
    chop up the string.
193
285
    """
194
 
    if not isinstance(s, unicode):
 
286
    if not isinstance(s, str):
195
287
        # this kinda sucks.  file contents are just binary data, and no
196
288
        # encoding metadata is stored, so we need to guess.  this is probably
197
289
        # okay for most code, but for people using things like KOI-8, this
201
293
            s = s.decode('utf-8')
202
294
        except UnicodeDecodeError:
203
295
            s = s.decode('iso-8859-15')
204
 
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
 
296
 
 
297
    s = html_escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
 
298
 
 
299
    return bleach.clean(s).replace('\n', '<br/>')
205
300
 
206
301
 
207
302
def fake_permissions(kind, executable):
240
335
P95_MEG = int(0.9 * MEG)
241
336
P95_GIG = int(0.9 * GIG)
242
337
 
 
338
 
243
339
def human_size(size, min_divisor=0):
244
340
    size = int(size)
245
341
    if (size == 0) and (min_divisor == 0):
246
 
        return '0'
247
 
    if (size < 512) and (min_divisor == 0):
248
 
        return str(size)
 
342
        return 'Empty'
 
343
    if (size < 1024) and (min_divisor == 0):
 
344
        return str(size) + ' bytes'
249
345
 
250
346
    if (size >= P95_GIG) or (min_divisor >= GIG):
251
347
        divisor = GIG
266
362
    if (base < 100) and (dot != 0):
267
363
        out += '.%d' % (dot,)
268
364
    if divisor == KILO:
269
 
        out += 'K'
 
365
        out += ' KB'
270
366
    elif divisor == MEG:
271
 
        out += 'M'
 
367
        out += ' MB'
272
368
    elif divisor == GIG:
273
 
        out += 'G'
 
369
        out += ' GB'
274
370
    return out
275
371
 
276
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
 
277
381
def fill_in_navigation(navigation):
278
382
    """
279
383
    given a navigation block (used by the template for the page header), fill
285
389
        navigation.position = 0
286
390
    navigation.count = len(navigation.revid_list)
287
391
    navigation.page_position = navigation.position // navigation.pagesize + 1
288
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
 
392
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
 
393
 - 1)) // navigation.pagesize
289
394
 
290
395
    def get_offset(offset):
291
 
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
 
396
        if (navigation.position + offset < 0) or (
 
397
           navigation.position + offset > navigation.count - 1):
292
398
            return None
293
399
        return navigation.revid_list[navigation.position + offset]
294
400
 
 
401
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
295
402
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
296
403
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
297
404
    prev_page_revno = navigation.history.get_revno(
300
407
            navigation.next_page_revid)
301
408
    start_revno = navigation.history.get_revno(navigation.start_revid)
302
409
 
303
 
    params = { 'filter_file_id': navigation.filter_file_id }
 
410
    params = {'filter_path': navigation.filter_path}
304
411
    if getattr(navigation, 'query', None) is not None:
305
412
        params['q'] = navigation.query
306
413
 
315
422
            [navigation.scan_url, next_page_revno], **params)
316
423
 
317
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
 
318
488
def decorator(unbound):
 
489
 
319
490
    def new_decorator(f):
320
491
        g = unbound(f)
321
492
        g.__name__ = f.__name__
328
499
    return new_decorator
329
500
 
330
501
 
331
 
# common threading-lock decorator
332
 
def with_lock(lockname, debug_name=None):
333
 
    if debug_name is None:
334
 
        debug_name = lockname
335
 
    @decorator
336
 
    def _decorator(unbound):
337
 
        def locked(self, *args, **kw):
338
 
            getattr(self, lockname).acquire()
339
 
            try:
340
 
                return unbound(self, *args, **kw)
341
 
            finally:
342
 
                getattr(self, lockname).release()
343
 
        return locked
344
 
    return _decorator
345
 
 
346
502
 
347
503
@decorator
348
504
def lsprof(f):
 
505
 
349
506
    def _f(*a, **kw):
350
 
        from loggerhead.lsprof import profile
351
507
        import cPickle
 
508
 
 
509
        from .loggerhead.lsprof import profile
352
510
        z = time.time()
353
511
        ret, stats = profile(f, *a, **kw)
354
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
 
512
        log.debug('Finished profiled %s in %d msec.' % (f.__name__,
 
513
            int((time.time() - z) * 1000)))
355
514
        stats.sort()
356
515
        stats.freeze()
357
516
        now = time.time()
358
517
        msec = int(now * 1000) % 1000
359
 
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
 
518
        timestr = time.strftime('%Y%m%d%H%M%S',
 
519
                                time.localtime(now)) + ('%03d' % (msec,))
360
520
        filename = f.__name__ + '-' + timestr + '.lsprof'
361
521
        cPickle.dump(stats, open(filename, 'w'), 2)
362
522
        return ret
372
532
#         the current beginning of navigation (navigation continues back to
373
533
#         the original revision) -- this defines an 'alternate mainline'
374
534
#         when the user navigates into a branch.
375
 
#     - file_id
376
 
#         the file being looked at
377
 
#     - filter_file_id
 
535
#     - filter_path
378
536
#         if navigating the revisions that touched a file
379
537
#     - q (query)
380
538
#         if navigating the revisions that matched a search query
391
549
#         for re-ordering an existing page by different sort
392
550
 
393
551
t_context = threading.local()
394
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
395
 
          'compare_revid', 'sort')
 
552
_valid = (
 
553
    'start_revid', 'filter_path', 'q', 'remember', 'compare_revid', 'sort')
396
554
 
397
555
 
398
556
def set_context(map):
399
 
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
 
557
    t_context.map = dict((k, v) for (k, v) in map.items() if k in _valid)
400
558
 
401
559
 
402
560
def get_context(**overrides):
404
562
    Soon to be deprecated.
405
563
 
406
564
 
407
 
    return a context map that may be overriden by specific values passed in,
 
565
    return a context map that may be overridden by specific values passed in,
408
566
    but only contains keys from the list of valid context keys.
409
567
 
410
568
    if 'clear' is set, only the 'remember' context value will be added, and
415
573
        map['remember'] = t_context.map.get('remember', None)
416
574
    else:
417
575
        map.update(t_context.map)
418
 
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
 
576
    overrides = dict((k, v) for (k, v) in overrides.items() if k in _valid)
419
577
    map.update(overrides)
420
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")