~launchpad-pqm/launchpad/devel

13174.1.1 by Martin Pool
Unify implementations of save-mail-to-librarian; both use uuids for file names
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.18 by Karl Fogel
Add the copyright header block to files under lib/canonical/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
1955 by Canonical.com Patch Queue Manager
menus. [r=spiv]
4
"""Various functions and classes that are useful across different parts of
5
launchpad.
6
7
Do not simply dump stuff in here.  Think carefully as to whether it would
8
be better as a method on an existing content object or IFooSet object.
9
"""
1509 by Canonical.com Patch Queue Manager
make the bugtask listing team aware
10
11
__metaclass__ = type
12
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
13
from difflib import unified_diff
1585 by Canonical.com Patch Queue Manager
Purging canonical/auth/. ubuntulinux.org forgottenpassword's page now redirects to launchpad.
14
import re
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
15
from StringIO import StringIO
10293.1.1 by Barry Warsaw, Max Bowsher
Switch from using sha and md5 to hashlib. Also use hashlib.sha256 instead of
16
import subprocess
1513 by Canonical.com Patch Queue Manager
Merged LaunchpadPackagePoAttach spec implementation needed for Hoary Translations
17
import tarfile
1658 by Canonical.com Patch Queue Manager
Adding shortlist() to helpers.py and use it on some IPerson/IPersonSet methods. r=SteveA
18
import warnings
10293.1.1 by Barry Warsaw, Max Bowsher
Switch from using sha and md5 to hashlib. Also use hashlib.sha256 instead of
19
7876.3.19 by Francis J. Lacoste
ForbiddenAttribute error, not TypeError is raised for missing __getslice__ on security proxy object.
20
from zope.security.interfaces import ForbiddenAttribute
7876.3.2 by Francis J. Lacoste
Use shortlist from lazr.Elifecycle.
21
11532.7.5 by Curtis Hovey
Fixed test_doc harness.
22
1922 by Canonical.com Patch Queue Manager
added general-purpose text-replacement function, and simplified various functions in helpers.py [trivial]
23
def text_replaced(text, replacements, _cache={}):
24
    """Return a new string with text replaced according to the dict provided.
25
11532.7.6 by Curtis Hovey
Hushed lint.
26
    The keys of the dict are substrings to find, the values are what to
27
    replace found substrings with.
1922 by Canonical.com Patch Queue Manager
added general-purpose text-replacement function, and simplified various functions in helpers.py [trivial]
28
2972.1.11 by Carlos Perelló Marín
Applied review comments
29
    :arg text: An unicode or str to do the replacement.
30
    :arg replacements: A dictionary with the replacements that should be done
31
1922 by Canonical.com Patch Queue Manager
added general-purpose text-replacement function, and simplified various functions in helpers.py [trivial]
32
    >>> text_replaced('', {'a':'b'})
33
    ''
34
    >>> text_replaced('a', {'a':'c'})
35
    'c'
36
    >>> text_replaced('faa bar baz', {'a': 'A', 'aa': 'X'})
37
    'fX bAr bAz'
38
    >>> text_replaced('1 2 3 4', {'1': '2', '2': '1'})
39
    '2 1 3 4'
40
2972.1.11 by Carlos Perelló Marín
Applied review comments
41
    Unicode strings work too.
42
43
    >>> text_replaced(u'1 2 3 4', {u'1': u'2', u'2': u'1'})
44
    u'2 1 3 4'
45
1922 by Canonical.com Patch Queue Manager
added general-purpose text-replacement function, and simplified various functions in helpers.py [trivial]
46
    The argument _cache is used as a cache of replacements that were requested
47
    before, so we only compute regular expressions once.
48
49
    """
50
    assert replacements, "The replacements dict must not be empty."
51
    # The ordering of keys and values in the tuple will be consistent within a
52
    # single Python process.
53
    cachekey = tuple(replacements.items())
54
    if cachekey not in _cache:
55
        L = []
2972.1.11 by Carlos Perelló Marín
Applied review comments
56
        if isinstance(text, unicode):
57
            list_item = u'(%s)'
58
            join_char = u'|'
59
        else:
60
            list_item = '(%s)'
61
            join_char = '|'
1922 by Canonical.com Patch Queue Manager
added general-purpose text-replacement function, and simplified various functions in helpers.py [trivial]
62
        for find, replace in sorted(replacements.items(),
63
                                    key=lambda (key, value): len(key),
64
                                    reverse=True):
2972.1.11 by Carlos Perelló Marín
Applied review comments
65
            L.append(list_item % re.escape(find))
1922 by Canonical.com Patch Queue Manager
added general-purpose text-replacement function, and simplified various functions in helpers.py [trivial]
66
        # Make a copy of the replacements dict, as it is mutable, but we're
67
        # keeping a cached reference to it.
68
        replacements_copy = dict(replacements)
11532.7.6 by Curtis Hovey
Hushed lint.
69
1922 by Canonical.com Patch Queue Manager
added general-purpose text-replacement function, and simplified various functions in helpers.py [trivial]
70
        def matchobj_replacer(matchobj):
71
            return replacements_copy[matchobj.group()]
11532.7.6 by Curtis Hovey
Hushed lint.
72
2972.1.11 by Carlos Perelló Marín
Applied review comments
73
        regexsub = re.compile(join_char.join(L)).sub
11532.7.6 by Curtis Hovey
Hushed lint.
74
1922 by Canonical.com Patch Queue Manager
added general-purpose text-replacement function, and simplified various functions in helpers.py [trivial]
75
        def replacer(s):
76
            return regexsub(matchobj_replacer, s)
11532.7.6 by Curtis Hovey
Hushed lint.
77
1922 by Canonical.com Patch Queue Manager
added general-purpose text-replacement function, and simplified various functions in helpers.py [trivial]
78
        _cache[cachekey] = replacer
79
    return _cache[cachekey](text)
80
2900.2.15 by Matthew Paul Thomas
remove remaining non-Ascii characters from pagetests using backslashreplace()
81
82
def backslashreplace(str):
83
    """Return a copy of the string, with non-ASCII characters rendered as
84
    xNN or uNNNN. Used to test data containing typographical quotes etc.
85
    """
86
    return str.decode('UTF-8').encode('ASCII', 'backslashreplace')
87
88
1513 by Canonical.com Patch Queue Manager
Merged LaunchpadPackagePoAttach spec implementation needed for Hoary Translations
89
def string_to_tarfile(s):
90
    """Convert a binary string containing a tar file into a tar file obj."""
91
92
    return tarfile.open('', 'r', StringIO(s))
93
94
6374.15.13 by Barry Warsaw
mergeRF
95
def simple_popen2(command, input, env=None, in_bufsize=1024, out_bufsize=128):
1635 by Canonical.com Patch Queue Manager
RosettaOptimization!
96
    """Run a command, give it input on its standard input, and capture its
97
    standard output.
98
99
    Returns the data from standard output.
100
101
    This function is needed to avoid certain deadlock situations. For example,
102
    if you popen2() a command, write its standard input, then read its
103
    standard output, this can deadlock due to the parent process blocking on
104
    writing to the child, while the child process is simultaneously blocking
1716.5.60 by kiko
Fix LIKE/ILIKE queries that were incorrectly using quote() instead of quote_like. Also fix uses of urljoin that should have been urlappends. Delintifies in places
105
    on writing to its parent. This function avoids that problem by using
106
    subprocess.Popen.communicate().
1635 by Canonical.com Patch Queue Manager
RosettaOptimization!
107
    """
108
1681.1.177 by Stuart Bishop
Make popen2 helper use subprocess module for great justice
109
    p = subprocess.Popen(
6374.15.13 by Barry Warsaw
mergeRF
110
            command, env=env, stdin=subprocess.PIPE,
11532.7.6 by Curtis Hovey
Hushed lint.
111
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1681.1.177 by Stuart Bishop
Make popen2 helper use subprocess module for great justice
112
    (output, nothing) = p.communicate(input)
1635 by Canonical.com Patch Queue Manager
RosettaOptimization!
113
    return output
114
2938.2.10 by Brad Bollenbach
response to code review
115
7876.3.9 by Francis J. Lacoste
Merged back fixes to shortlist.
116
class ShortListTooBigError(Exception):
117
    """This error is raised when the shortlist hardlimit is reached"""
118
119
120
def shortlist(sequence, longest_expected=15, hardlimit=None):
121
    """Return a listified version of sequence.
122
123
    If <sequence> has more than <longest_expected> items, a warning is issued.
124
125
    >>> shortlist([1, 2])
126
    [1, 2]
127
7876.3.14 by Francis J. Lacoste
Review comments.
128
    >>> shortlist([1, 2, 3], 2) #doctest: +NORMALIZE_WHITESPACE
7876.3.9 by Francis J. Lacoste
Merged back fixes to shortlist.
129
    Traceback (most recent call last):
130
    ...
7876.3.14 by Francis J. Lacoste
Review comments.
131
    UserWarning: shortlist() should not be used here. It's meant to listify
132
    sequences with no more than 2 items.  There were 3 items.
7876.3.9 by Francis J. Lacoste
Merged back fixes to shortlist.
133
134
    >>> shortlist([1, 2, 3, 4], hardlimit=2)
135
    Traceback (most recent call last):
136
    ...
137
    ShortListTooBigError: Hard limit of 2 exceeded.
138
7876.3.14 by Francis J. Lacoste
Review comments.
139
    >>> shortlist(
140
    ...     [1, 2, 3, 4], 2, hardlimit=4) #doctest: +NORMALIZE_WHITESPACE
141
    Traceback (most recent call last):
142
    ...
143
    UserWarning: shortlist() should not be used here. It's meant to listify
144
    sequences with no more than 2 items.  There were 4 items.
145
146
    It works on iterable also which don't support the extended slice protocol.
147
148
    >>> xrange(5)[:1] #doctest: +ELLIPSIS
149
    Traceback (most recent call last):
150
    ...
151
    TypeError: ...
152
153
    >>> shortlist(xrange(10), 5, hardlimit=8) #doctest: +ELLIPSIS
7876.3.9 by Francis J. Lacoste
Merged back fixes to shortlist.
154
    Traceback (most recent call last):
155
    ...
156
    ShortListTooBigError: ...
157
158
    """
159
    if hardlimit is not None:
160
        last = hardlimit + 1
161
    else:
7943.1.1 by Francis J. Lacoste
Don't crop results when no hardlimit is requested.
162
        last = None
7876.3.14 by Francis J. Lacoste
Review comments.
163
    try:
7876.3.9 by Francis J. Lacoste
Merged back fixes to shortlist.
164
        results = list(sequence[:last])
7876.3.19 by Francis J. Lacoste
ForbiddenAttribute error, not TypeError is raised for missing __getslice__ on security proxy object.
165
    except (TypeError, ForbiddenAttribute):
7876.3.9 by Francis J. Lacoste
Merged back fixes to shortlist.
166
        results = []
167
        for idx, item in enumerate(sequence):
168
            if hardlimit and idx > hardlimit:
169
                break
170
            results.append(item)
171
172
    size = len(results)
173
    if hardlimit and size > hardlimit:
174
        raise ShortListTooBigError(
175
           'Hard limit of %d exceeded.' % hardlimit)
176
    elif size > longest_expected:
177
        warnings.warn(
178
            "shortlist() should not be used here. It's meant to listify"
179
            " sequences with no more than %d items.  There were %s items."
180
            % (longest_expected, size), stacklevel=2)
181
    return results
182
183
1646 by Canonical.com Patch Queue Manager
Rosetta source code follows now the Launchpad standard layout rs=SteveA
184
def is_tar_filename(filename):
185
    '''
186
    Check whether a filename looks like a filename that belongs to a tar file,
187
    possibly one compressed somehow.
188
    '''
189
190
    return (filename.endswith('.tar') or
191
            filename.endswith('.tar.gz') or
2570.1.7 by Carlos Perello Marin
Lots of fixes + new code + tests updates
192
            filename.endswith('.tgz') or
1646 by Canonical.com Patch Queue Manager
Rosetta source code follows now the Launchpad standard layout rs=SteveA
193
            filename.endswith('.tar.bz2'))
194
1715 by Canonical.com Patch Queue Manager
Moved the translation form from potemplate context into pofile context. r=SteveA
195
1702 by Canonical.com Patch Queue Manager
PO attach functional test, plus miscellaneous fixes (r=Steve)
196
def test_diff(lines_a, lines_b):
197
    """Generate a string indicating the difference between expected and actual
198
    values in a test.
199
    """
200
201
    return '\n'.join(list(unified_diff(
202
        a=lines_a,
203
        b=lines_b,
204
        fromfile='expected',
205
        tofile='actual',
206
        lineterm='',
207
        )))
208
2938.2.10 by Brad Bollenbach
response to code review
209
1881 by Canonical.com Patch Queue Manager
Comments for 'gpghandler' and 'zeca' configuration sections
210
def filenameToContentType(fname):
2499 by Canonical.com Patch Queue Manager
[r=kiko] make malone admin aware,
211
    """ Return the a ContentType-like entry for arbitrary filenames
1881 by Canonical.com Patch Queue Manager
Comments for 'gpghandler' and 'zeca' configuration sections
212
213
    deb files
214
215
    >>> filenameToContentType('test.deb')
216
    'application/x-debian-package'
217
218
    text files
219
220
    >>> filenameToContentType('test.txt')
221
    'text/plain'
222
223
    Not recognized format
2499 by Canonical.com Patch Queue Manager
[r=kiko] make malone admin aware,
224
1881 by Canonical.com Patch Queue Manager
Comments for 'gpghandler' and 'zeca' configuration sections
225
    >>> filenameToContentType('test.tgz')
226
    'application/octet-stream'
227
    """
11532.7.6 by Curtis Hovey
Hushed lint.
228
    ftmap = {".dsc": "text/plain",
229
             ".changes": "text/plain",
230
             ".deb": "application/x-debian-package",
231
             ".udeb": "application/x-debian-package",
232
             ".txt": "text/plain",
233
             # For the build master logs
234
             ".txt.gz": "text/plain",
1881 by Canonical.com Patch Queue Manager
Comments for 'gpghandler' and 'zeca' configuration sections
235
             }
236
    for ending in ftmap:
237
        if fname.endswith(ending):
238
            return ftmap[ending]
239
    return "application/octet-stream"
240
1872 by Canonical.com Patch Queue Manager
First cut of the email interface! Refactorings to SQLObjectFooEvents. First cut of banzai deeath scene. Fix bug 931. Add bug url declarations. And some more... r=stevea
241
2372 by Canonical.com Patch Queue Manager
ShipItNG: Allow people to place new orders and shipit admins to search for existing requests and approve, cancel or change them. It's also possible to create new standard shipit requests, which are the options presented to users when they place new orders. r=kiko
242
def intOrZero(value):
2851.2.4 by Guilherme Salgado
Some small fixes Steve suggested.
243
    """Return int(value) or 0 if the conversion fails.
2972.1.11 by Carlos Perelló Marín
Applied review comments
244
2851.2.4 by Guilherme Salgado
Some small fixes Steve suggested.
245
    >>> intOrZero('1.23')
246
    0
247
    >>> intOrZero('1.ab')
248
    0
249
    >>> intOrZero('2')
250
    2
251
    >>> intOrZero(None)
252
    0
253
    >>> intOrZero(1)
254
    1
2851.2.6 by Guilherme Salgado
Some extra tests for intOrZero() and positiveIntOrZero(), as kiko suggested.
255
    >>> intOrZero(-9)
256
    -9
2851.2.4 by Guilherme Salgado
Some small fixes Steve suggested.
257
    """
2372 by Canonical.com Patch Queue Manager
ShipItNG: Allow people to place new orders and shipit admins to search for existing requests and approve, cancel or change them. It's also possible to create new standard shipit requests, which are the options presented to users when they place new orders. r=kiko
258
    try:
259
        return int(value)
2851.2.1 by Guilherme Salgado
ShipIt Reports, take 1
260
    except (ValueError, TypeError):
2372 by Canonical.com Patch Queue Manager
ShipItNG: Allow people to place new orders and shipit admins to search for existing requests and approve, cancel or change them. It's also possible to create new standard shipit requests, which are the options presented to users when they place new orders. r=kiko
261
        return 0
262
2938.2.10 by Brad Bollenbach
response to code review
263
4199.1.5 by Jonathan Lange
Apply statik's review comments
264
def truncate_text(text, max_length):
4199.1.3 by Jonathan Lange
truncate_text helper function and use constants in BranchView, rather than
265
    """Return a version of string no longer than max_length characters.
266
267
    Tries not to cut off the text mid-word.
268
    """
4199.1.5 by Jonathan Lange
Apply statik's review comments
269
    words = re.compile(r'\s*\S+').findall(text, 0, max_length + 1)
4199.1.3 by Jonathan Lange
truncate_text helper function and use constants in BranchView, rather than
270
    truncated = words[0]
271
    for word in words[1:]:
272
        if len(truncated) + len(word) > max_length:
273
            break
274
        truncated += word
275
    return truncated[:max_length]
5796.13.8 by Gavin Panella
New helper function, english_list.
276
277
5796.13.15 by Gavin Panella
Use the term 'conjunction', and improve docs of english_list.
278
def english_list(items, conjunction='and'):
5796.13.8 by Gavin Panella
New helper function, english_list.
279
    """Return all the items concatenated into a English-style string.
280
6484.9.19 by Gavin Panella
Correct chapter for Strunk & White.
281
    Follows the advice given in The Elements of Style, chapter I,
5796.13.15 by Gavin Panella
Use the term 'conjunction', and improve docs of english_list.
282
    section 2:
5796.13.8 by Gavin Panella
New helper function, english_list.
283
5796.13.15 by Gavin Panella
Use the term 'conjunction', and improve docs of english_list.
284
    "In a series of three or more terms with a single conjunction, use
285
     a comma after each term except the last."
6607.7.5 by Julian Edwards
allenap comments
286
287
    Beware that this is US English and is wrong for non-US.
5796.13.8 by Gavin Panella
New helper function, english_list.
288
    """
289
    items = list(items)
290
    if len(items) <= 2:
5796.13.15 by Gavin Panella
Use the term 'conjunction', and improve docs of english_list.
291
        return (' %s ' % conjunction).join(items)
5796.13.8 by Gavin Panella
New helper function, english_list.
292
    else:
5796.13.15 by Gavin Panella
Use the term 'conjunction', and improve docs of english_list.
293
        items[-1] = '%s %s' % (conjunction, items[-1])
5796.13.8 by Gavin Panella
New helper function, english_list.
294
        return ', '.join(items)
7675.166.315 by Stuart Bishop
Be more careful about casting to Unicode
295
296
297
def ensure_unicode(string):
7675.166.318 by Stuart Bishop
ensure_unicode should handle None
298
    r"""Return input as unicode. None is passed through unharmed.
7675.166.315 by Stuart Bishop
Be more careful about casting to Unicode
299
300
    Do not use this method. This method exists only to help migration
301
    of legacy code where str objects were being passed into contexts
302
    where unicode objects are required. All invokations of
303
    ensure_unicode() should eventually be removed.
304
305
    This differs from the builtin unicode() function, as a TypeError
306
    exception will be raised if the parameter is not a basestring or if
307
    a raw string is not ASCII.
308
309
    >>> ensure_unicode(u'hello')
310
    u'hello'
311
312
    >>> ensure_unicode('hello')
313
    u'hello'
314
315
    >>> ensure_unicode(u'A'.encode('utf-16')) # Not ASCII
316
    Traceback (most recent call last):
317
    ...
318
    TypeError: '\xff\xfeA\x00' is not US-ASCII
319
320
    >>> ensure_unicode(42)
321
    Traceback (most recent call last):
322
    ...
323
    TypeError: 42 is not a basestring (<type 'int'>)
7675.166.318 by Stuart Bishop
ensure_unicode should handle None
324
325
    >>> ensure_unicode(None) is None
326
    True
7675.166.315 by Stuart Bishop
Be more careful about casting to Unicode
327
    """
7675.166.318 by Stuart Bishop
ensure_unicode should handle None
328
    if string is None:
329
        return None
330
    elif isinstance(string, unicode):
7675.166.315 by Stuart Bishop
Be more careful about casting to Unicode
331
        return string
332
    elif isinstance(string, basestring):
333
        try:
334
            return string.decode('US-ASCII')
335
        except UnicodeDecodeError:
336
            raise TypeError("%s is not US-ASCII" % repr(string))
337
    else:
338
        raise TypeError(
339
            "%r is not a basestring (%r)" % (string, type(string)))