~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: John Arbash Meinel
  • Date: 2011-03-03 10:58:57 UTC
  • mfrom: (428.2.1 loggerhead)
  • Revision ID: john@arbash-meinel.com-20110303105857-2i83wei3fu4phkef
Use different favicons depending on whether you are browsing a branch, or browsing near a branch.

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
20
#
21
21
 
22
 
try:
23
 
    from xml.etree import ElementTree as ET
24
 
except ImportError:
25
 
    from elementtree import ElementTree as ET
26
 
 
27
22
import base64
28
23
import cgi
29
24
import datetime
32
27
import struct
33
28
import threading
34
29
import time
35
 
import types
 
30
import sys
 
31
import os
 
32
import subprocess
 
33
 
 
34
try:
 
35
    from xml.etree import ElementTree as ET
 
36
except ImportError:
 
37
    from elementtree import ElementTree as ET
 
38
 
 
39
from bzrlib import urlutils
 
40
 
 
41
from simpletal.simpleTALUtils import HTMLStructureCleaner
36
42
 
37
43
log = logging.getLogger("loggerhead.controllers")
38
44
 
 
45
 
39
46
def fix_year(year):
40
47
    if year < 70:
41
48
        year += 2000
54
61
# displaydate and approximatedate return an elementtree <span> Element
55
62
# with the full date in a tooltip.
56
63
 
 
64
 
57
65
def date_day(value):
58
66
    return value.strftime('%Y-%m-%d')
59
67
 
60
68
 
61
69
def date_time(value):
62
 
    return value.strftime('%Y-%m-%d %T')
 
70
    if value is not None:
 
71
        return value.strftime('%Y-%m-%d %H:%M:%S')
 
72
    else:
 
73
        return 'N/A'
63
74
 
64
75
 
65
76
def _displaydate(date):
120
131
    return _wrap_with_date_time_title(date, _displaydate(date))
121
132
 
122
133
 
123
 
class Container (object):
 
134
class Container(object):
124
135
    """
125
136
    Convert a dict into an object with attributes.
126
137
    """
 
138
 
127
139
    def __init__(self, _dict=None, **kw):
 
140
        self._properties = {}
128
141
        if _dict is not None:
129
142
            for key, value in _dict.iteritems():
130
143
                setattr(self, key, value)
134
147
    def __repr__(self):
135
148
        out = '{ '
136
149
        for key, value in self.__dict__.iteritems():
137
 
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
 
150
            if key.startswith('_') or (getattr(self.__dict__[key],
 
151
                                       '__call__', None) is not None):
138
152
                continue
139
153
            out += '%r => %r, ' % (key, value)
140
154
        out += '}'
141
155
        return out
142
156
 
 
157
    def __getattr__(self, attr):
 
158
        """Used for handling things that aren't already available."""
 
159
        if attr.startswith('_') or attr not in self._properties:
 
160
            raise AttributeError('No attribute: %s' % (attr,))
 
161
        val = self._properties[attr](self, attr)
 
162
        setattr(self, attr, val)
 
163
        return val
 
164
 
 
165
    def _set_property(self, attr, prop_func):
 
166
        """Set a function that will be called when an attribute is desired.
 
167
 
 
168
        We will cache the return value, so the function call should be
 
169
        idempotent. We will pass 'self' and the 'attr' name when triggered.
 
170
        """
 
171
        if attr.startswith('_'):
 
172
            raise ValueError("Cannot create properties that start with _")
 
173
        self._properties[attr] = prop_func
 
174
 
143
175
 
144
176
def trunc(text, limit=10):
145
177
    if len(text) <= limit:
150
182
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
151
183
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
152
184
 
 
185
 
153
186
def hide_email(email):
154
187
    """
155
188
    try to obsure any email address in a bazaar committer's name.
169
202
        return '%s at %s' % (username, domains[-2])
170
203
    return '%s at %s' % (username, domains[0])
171
204
 
 
205
def hide_emails(emails):
 
206
    """
 
207
    try to obscure any email address in a list of bazaar committers' names.
 
208
    """
 
209
    result = []
 
210
    for email in emails:
 
211
        result.append(hide_email(email))
 
212
    return result
172
213
 
173
214
# only do this if unicode turns out to be a problem
174
215
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
175
216
 
176
217
# FIXME: get rid of this method; use fixed_width() and avoid XML().
 
218
 
 
219
 
177
220
def html_clean(s):
178
221
    """
179
222
    clean up a string for html display.  expand any tabs, encode any html
184
227
    s = s.replace(' ', '&nbsp;')
185
228
    return s
186
229
 
 
230
 
187
231
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
188
232
 
 
233
 
189
234
def fill_div(s):
190
235
    """
191
236
    CSS is stupid. In some cases we need to replace an empty value with
193
238
 
194
239
    return: the same value recieved if not empty, and a '&nbsp;' if it is.
195
240
    """
196
 
    
197
 
 
198
241
    if s is None:
199
242
        return '&nbsp;'
200
243
    elif isinstance(s, int):
208
251
            s = s.decode('iso-8859-15')
209
252
        return s
210
253
 
 
254
HSC = HTMLStructureCleaner()
211
255
 
212
256
def fixed_width(s):
213
257
    """
224
268
            s = s.decode('utf-8')
225
269
        except UnicodeDecodeError:
226
270
            s = s.decode('iso-8859-15')
227
 
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
 
271
 
 
272
    s = cgi.escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
 
273
 
 
274
    return HSC.clean(s).replace('\n', '<br/>')
228
275
 
229
276
 
230
277
def fake_permissions(kind, executable):
263
310
P95_MEG = int(0.9 * MEG)
264
311
P95_GIG = int(0.9 * GIG)
265
312
 
 
313
 
266
314
def human_size(size, min_divisor=0):
267
315
    size = int(size)
268
316
    if (size == 0) and (min_divisor == 0):
297
345
    return out
298
346
 
299
347
 
 
348
def local_path_from_url(url):
 
349
    """Convert Bazaar URL to local path, ignoring readonly+ prefix"""
 
350
    readonly_prefix = 'readonly+'
 
351
    if url.startswith(readonly_prefix):
 
352
        url = url[len(readonly_prefix):]
 
353
    return urlutils.local_path_from_url(url)
 
354
 
 
355
 
300
356
def fill_in_navigation(navigation):
301
357
    """
302
358
    given a navigation block (used by the template for the page header), fill
308
364
        navigation.position = 0
309
365
    navigation.count = len(navigation.revid_list)
310
366
    navigation.page_position = navigation.position // navigation.pagesize + 1
311
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
 
367
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
 
368
 - 1)) // navigation.pagesize
312
369
 
313
370
    def get_offset(offset):
314
 
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
 
371
        if (navigation.position + offset < 0) or (
 
372
           navigation.position + offset > navigation.count - 1):
315
373
            return None
316
374
        return navigation.revid_list[navigation.position + offset]
317
375
 
324
382
            navigation.next_page_revid)
325
383
    start_revno = navigation.history.get_revno(navigation.start_revid)
326
384
 
327
 
    params = { 'filter_file_id': navigation.filter_file_id }
 
385
    params = {'filter_file_id': navigation.filter_file_id}
328
386
    if getattr(navigation, 'query', None) is not None:
329
387
        params['q'] = navigation.query
330
388
 
339
397
            [navigation.scan_url, next_page_revno], **params)
340
398
 
341
399
 
 
400
def directory_breadcrumbs(path, is_root, view):
 
401
    """
 
402
    Generate breadcrumb information from the directory path given
 
403
 
 
404
    The path given should be a path up to any branch that is currently being
 
405
    served
 
406
 
 
407
    Arguments:
 
408
    path -- The path to convert into breadcrumbs
 
409
    is_root -- Whether or not loggerhead is serving a branch at its root
 
410
    view -- The type of view we are showing (files, changes etc)
 
411
    """
 
412
    # Is our root directory itself a branch?
 
413
    if is_root:
 
414
        breadcrumbs = [{
 
415
            'dir_name': path,
 
416
            'path': '',
 
417
            'suffix': view,
 
418
        }]
 
419
    else:
 
420
        # Create breadcrumb trail for the path leading up to the branch
 
421
        breadcrumbs = [{
 
422
            'dir_name': "(root)",
 
423
            'path': '',
 
424
            'suffix': '',
 
425
        }]
 
426
        if path != '/':
 
427
            dir_parts = path.strip('/').split('/')
 
428
            for index, dir_name in enumerate(dir_parts):
 
429
                breadcrumbs.append({
 
430
                    'dir_name': dir_name,
 
431
                    'path': '/'.join(dir_parts[:index + 1]),
 
432
                    'suffix': '',
 
433
                })
 
434
            # If we are not in the directory view, the last crumb is a branch,
 
435
            # so we need to specify a view
 
436
            if view != 'directory':
 
437
                breadcrumbs[-1]['suffix'] = '/' + view
 
438
    return breadcrumbs
 
439
 
 
440
 
 
441
def branch_breadcrumbs(path, inv, view):
 
442
    """
 
443
    Generate breadcrumb information from the branch path given
 
444
 
 
445
    The path given should be a path that exists within a branch
 
446
 
 
447
    Arguments:
 
448
    path -- The path to convert into breadcrumbs
 
449
    inv -- Inventory to get file information from
 
450
    view -- The type of view we are showing (files, changes etc)
 
451
    """
 
452
    dir_parts = path.strip('/').split('/')
 
453
    inner_breadcrumbs = []
 
454
    for index, dir_name in enumerate(dir_parts):
 
455
        inner_breadcrumbs.append({
 
456
            'dir_name': dir_name,
 
457
            'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
 
458
            'suffix': '/' + view,
 
459
        })
 
460
    return inner_breadcrumbs
 
461
 
 
462
 
342
463
def decorator(unbound):
 
464
 
343
465
    def new_decorator(f):
344
466
        g = unbound(f)
345
467
        g.__name__ = f.__name__
352
474
    return new_decorator
353
475
 
354
476
 
355
 
# common threading-lock decorator
356
 
def with_lock(lockname, debug_name=None):
357
 
    if debug_name is None:
358
 
        debug_name = lockname
359
 
    @decorator
360
 
    def _decorator(unbound):
361
 
        def locked(self, *args, **kw):
362
 
            getattr(self, lockname).acquire()
363
 
            try:
364
 
                return unbound(self, *args, **kw)
365
 
            finally:
366
 
                getattr(self, lockname).release()
367
 
        return locked
368
 
    return _decorator
369
 
 
370
477
 
371
478
@decorator
372
479
def lsprof(f):
 
480
 
373
481
    def _f(*a, **kw):
374
482
        from loggerhead.lsprof import profile
375
483
        import cPickle
376
484
        z = time.time()
377
485
        ret, stats = profile(f, *a, **kw)
378
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
 
486
        log.debug('Finished profiled %s in %d msec.' % (f.__name__,
 
487
            int((time.time() - z) * 1000)))
379
488
        stats.sort()
380
489
        stats.freeze()
381
490
        now = time.time()
382
491
        msec = int(now * 1000) % 1000
383
 
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
 
492
        timestr = time.strftime('%Y%m%d%H%M%S',
 
493
                                time.localtime(now)) + ('%03d' % (msec,))
384
494
        filename = f.__name__ + '-' + timestr + '.lsprof'
385
495
        cPickle.dump(stats, open(filename, 'w'), 2)
386
496
        return ret
442
552
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
443
553
    map.update(overrides)
444
554
    return map
 
555
 
 
556
 
 
557
class Reloader(object):
 
558
    """
 
559
    This class wraps all paste.reloader logic. All methods are @classmethod.
 
560
    """
 
561
 
 
562
    _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
 
563
 
 
564
    @classmethod
 
565
    def _turn_sigterm_into_systemexit(cls):
 
566
        """
 
567
        Attempts to turn a SIGTERM exception into a SystemExit exception.
 
568
        """
 
569
        try:
 
570
            import signal
 
571
        except ImportError:
 
572
            return
 
573
 
 
574
        def handle_term(signo, frame):
 
575
            raise SystemExit
 
576
        signal.signal(signal.SIGTERM, handle_term)
 
577
 
 
578
    @classmethod
 
579
    def is_installed(cls):
 
580
        return os.environ.get(cls._reloader_environ_key)
 
581
 
 
582
    @classmethod
 
583
    def install(cls):
 
584
        from paste import reloader
 
585
        reloader.install(int(1))
 
586
 
 
587
    @classmethod
 
588
    def restart_with_reloader(cls):
 
589
        """Based on restart_with_monitor from paste.script.serve."""
 
590
        print 'Starting subprocess with file monitor'
 
591
        while True:
 
592
            args = [sys.executable] + sys.argv
 
593
            new_environ = os.environ.copy()
 
594
            new_environ[cls._reloader_environ_key] = 'true'
 
595
            proc = None
 
596
            try:
 
597
                try:
 
598
                    cls._turn_sigterm_into_systemexit()
 
599
                    proc = subprocess.Popen(args, env=new_environ)
 
600
                    exit_code = proc.wait()
 
601
                    proc = None
 
602
                except KeyboardInterrupt:
 
603
                    print '^C caught in monitor process'
 
604
                    return 1
 
605
            finally:
 
606
                if (proc is not None
 
607
                    and getattr(os, 'kill', None) is not None):
 
608
                    import signal
 
609
                    try:
 
610
                        os.kill(proc.pid, signal.SIGTERM)
 
611
                    except (OSError, IOError):
 
612
                        pass
 
613
 
 
614
            # Reloader always exits with code 3; but if we are
 
615
            # a monitor, any exit code will restart
 
616
            if exit_code != 3:
 
617
                return exit_code
 
618
            print '-'*20, 'Restarting', '-'*20
 
619
 
 
620
 
 
621
def convert_file_errors(application):
 
622
    """WSGI wrapper to convert some file errors to Paste exceptions"""
 
623
    def new_application(environ, start_response):
 
624
        try:
 
625
            return application(environ, start_response)
 
626
        except (IOError, OSError), e:
 
627
            import errno
 
628
            from paste import httpexceptions
 
629
            if e.errno == errno.ENOENT:
 
630
                raise httpexceptions.HTTPNotFound()
 
631
            elif e.errno == errno.EACCES:
 
632
                raise httpexceptions.HTTPForbidden()
 
633
            else:
 
634
                raise
 
635
    return new_application