##// END OF EJS Templates
stringutil: rename local email/names variables to their plural forms...
Connor Sheehan -
r37262:54b896f1 default
parent child Browse files
Show More
@@ -1,469 +1,469 b''
1 # stringutil.py - utility for generic string formatting, parsing, etc.
1 # stringutil.py - utility for generic string formatting, parsing, etc.
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import codecs
12 import codecs
13 import re as remod
13 import re as remod
14 import textwrap
14 import textwrap
15
15
16 from ..i18n import _
16 from ..i18n import _
17 from ..thirdparty import attr
17 from ..thirdparty import attr
18
18
19 from .. import (
19 from .. import (
20 encoding,
20 encoding,
21 error,
21 error,
22 pycompat,
22 pycompat,
23 )
23 )
24
24
25 _DATA_ESCAPE_MAP = {pycompat.bytechr(i): br'\x%02x' % i for i in range(256)}
25 _DATA_ESCAPE_MAP = {pycompat.bytechr(i): br'\x%02x' % i for i in range(256)}
26 _DATA_ESCAPE_MAP.update({
26 _DATA_ESCAPE_MAP.update({
27 b'\\': b'\\\\',
27 b'\\': b'\\\\',
28 b'\r': br'\r',
28 b'\r': br'\r',
29 b'\n': br'\n',
29 b'\n': br'\n',
30 })
30 })
31 _DATA_ESCAPE_RE = remod.compile(br'[\x00-\x08\x0a-\x1f\\\x7f-\xff]')
31 _DATA_ESCAPE_RE = remod.compile(br'[\x00-\x08\x0a-\x1f\\\x7f-\xff]')
32
32
33 def escapedata(s):
33 def escapedata(s):
34 if isinstance(s, bytearray):
34 if isinstance(s, bytearray):
35 s = bytes(s)
35 s = bytes(s)
36
36
37 return _DATA_ESCAPE_RE.sub(lambda m: _DATA_ESCAPE_MAP[m.group(0)], s)
37 return _DATA_ESCAPE_RE.sub(lambda m: _DATA_ESCAPE_MAP[m.group(0)], s)
38
38
39 def binary(s):
39 def binary(s):
40 """return true if a string is binary data"""
40 """return true if a string is binary data"""
41 return bool(s and '\0' in s)
41 return bool(s and '\0' in s)
42
42
43 def stringmatcher(pattern, casesensitive=True):
43 def stringmatcher(pattern, casesensitive=True):
44 """
44 """
45 accepts a string, possibly starting with 're:' or 'literal:' prefix.
45 accepts a string, possibly starting with 're:' or 'literal:' prefix.
46 returns the matcher name, pattern, and matcher function.
46 returns the matcher name, pattern, and matcher function.
47 missing or unknown prefixes are treated as literal matches.
47 missing or unknown prefixes are treated as literal matches.
48
48
49 helper for tests:
49 helper for tests:
50 >>> def test(pattern, *tests):
50 >>> def test(pattern, *tests):
51 ... kind, pattern, matcher = stringmatcher(pattern)
51 ... kind, pattern, matcher = stringmatcher(pattern)
52 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
52 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
53 >>> def itest(pattern, *tests):
53 >>> def itest(pattern, *tests):
54 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
54 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
55 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
55 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
56
56
57 exact matching (no prefix):
57 exact matching (no prefix):
58 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
58 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
59 ('literal', 'abcdefg', [False, False, True])
59 ('literal', 'abcdefg', [False, False, True])
60
60
61 regex matching ('re:' prefix)
61 regex matching ('re:' prefix)
62 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
62 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
63 ('re', 'a.+b', [False, False, True])
63 ('re', 'a.+b', [False, False, True])
64
64
65 force exact matches ('literal:' prefix)
65 force exact matches ('literal:' prefix)
66 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
66 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
67 ('literal', 're:foobar', [False, True])
67 ('literal', 're:foobar', [False, True])
68
68
69 unknown prefixes are ignored and treated as literals
69 unknown prefixes are ignored and treated as literals
70 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
70 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
71 ('literal', 'foo:bar', [False, False, True])
71 ('literal', 'foo:bar', [False, False, True])
72
72
73 case insensitive regex matches
73 case insensitive regex matches
74 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
74 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
75 ('re', 'A.+b', [False, False, True])
75 ('re', 'A.+b', [False, False, True])
76
76
77 case insensitive literal matches
77 case insensitive literal matches
78 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
78 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
79 ('literal', 'ABCDEFG', [False, False, True])
79 ('literal', 'ABCDEFG', [False, False, True])
80 """
80 """
81 if pattern.startswith('re:'):
81 if pattern.startswith('re:'):
82 pattern = pattern[3:]
82 pattern = pattern[3:]
83 try:
83 try:
84 flags = 0
84 flags = 0
85 if not casesensitive:
85 if not casesensitive:
86 flags = remod.I
86 flags = remod.I
87 regex = remod.compile(pattern, flags)
87 regex = remod.compile(pattern, flags)
88 except remod.error as e:
88 except remod.error as e:
89 raise error.ParseError(_('invalid regular expression: %s')
89 raise error.ParseError(_('invalid regular expression: %s')
90 % e)
90 % e)
91 return 're', pattern, regex.search
91 return 're', pattern, regex.search
92 elif pattern.startswith('literal:'):
92 elif pattern.startswith('literal:'):
93 pattern = pattern[8:]
93 pattern = pattern[8:]
94
94
95 match = pattern.__eq__
95 match = pattern.__eq__
96
96
97 if not casesensitive:
97 if not casesensitive:
98 ipat = encoding.lower(pattern)
98 ipat = encoding.lower(pattern)
99 match = lambda s: ipat == encoding.lower(s)
99 match = lambda s: ipat == encoding.lower(s)
100 return 'literal', pattern, match
100 return 'literal', pattern, match
101
101
102 def shortuser(user):
102 def shortuser(user):
103 """Return a short representation of a user name or email address."""
103 """Return a short representation of a user name or email address."""
104 f = user.find('@')
104 f = user.find('@')
105 if f >= 0:
105 if f >= 0:
106 user = user[:f]
106 user = user[:f]
107 f = user.find('<')
107 f = user.find('<')
108 if f >= 0:
108 if f >= 0:
109 user = user[f + 1:]
109 user = user[f + 1:]
110 f = user.find(' ')
110 f = user.find(' ')
111 if f >= 0:
111 if f >= 0:
112 user = user[:f]
112 user = user[:f]
113 f = user.find('.')
113 f = user.find('.')
114 if f >= 0:
114 if f >= 0:
115 user = user[:f]
115 user = user[:f]
116 return user
116 return user
117
117
118 def emailuser(user):
118 def emailuser(user):
119 """Return the user portion of an email address."""
119 """Return the user portion of an email address."""
120 f = user.find('@')
120 f = user.find('@')
121 if f >= 0:
121 if f >= 0:
122 user = user[:f]
122 user = user[:f]
123 f = user.find('<')
123 f = user.find('<')
124 if f >= 0:
124 if f >= 0:
125 user = user[f + 1:]
125 user = user[f + 1:]
126 return user
126 return user
127
127
128 def email(author):
128 def email(author):
129 '''get email of author.'''
129 '''get email of author.'''
130 r = author.find('>')
130 r = author.find('>')
131 if r == -1:
131 if r == -1:
132 r = None
132 r = None
133 return author[author.find('<') + 1:r]
133 return author[author.find('<') + 1:r]
134
134
135 def person(author):
135 def person(author):
136 """Returns the name before an email address,
136 """Returns the name before an email address,
137 interpreting it as per RFC 5322
137 interpreting it as per RFC 5322
138
138
139 >>> person(b'foo@bar')
139 >>> person(b'foo@bar')
140 'foo'
140 'foo'
141 >>> person(b'Foo Bar <foo@bar>')
141 >>> person(b'Foo Bar <foo@bar>')
142 'Foo Bar'
142 'Foo Bar'
143 >>> person(b'"Foo Bar" <foo@bar>')
143 >>> person(b'"Foo Bar" <foo@bar>')
144 'Foo Bar'
144 'Foo Bar'
145 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
145 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
146 'Foo "buz" Bar'
146 'Foo "buz" Bar'
147 >>> # The following are invalid, but do exist in real-life
147 >>> # The following are invalid, but do exist in real-life
148 ...
148 ...
149 >>> person(b'Foo "buz" Bar <foo@bar>')
149 >>> person(b'Foo "buz" Bar <foo@bar>')
150 'Foo "buz" Bar'
150 'Foo "buz" Bar'
151 >>> person(b'"Foo Bar <foo@bar>')
151 >>> person(b'"Foo Bar <foo@bar>')
152 'Foo Bar'
152 'Foo Bar'
153 """
153 """
154 if '@' not in author:
154 if '@' not in author:
155 return author
155 return author
156 f = author.find('<')
156 f = author.find('<')
157 if f != -1:
157 if f != -1:
158 return author[:f].strip(' "').replace('\\"', '"')
158 return author[:f].strip(' "').replace('\\"', '"')
159 f = author.find('@')
159 f = author.find('@')
160 return author[:f].replace('.', ' ')
160 return author[:f].replace('.', ' ')
161
161
162 @attr.s(hash=True)
162 @attr.s(hash=True)
163 class mailmapping(object):
163 class mailmapping(object):
164 '''Represents a username/email key or value in
164 '''Represents a username/email key or value in
165 a mailmap file'''
165 a mailmap file'''
166 email = attr.ib()
166 email = attr.ib()
167 name = attr.ib(default=None)
167 name = attr.ib(default=None)
168
168
169 def parsemailmap(mailmapcontent):
169 def parsemailmap(mailmapcontent):
170 """Parses data in the .mailmap format
170 """Parses data in the .mailmap format
171
171
172 >>> mmdata = b"\\n".join([
172 >>> mmdata = b"\\n".join([
173 ... b'# Comment',
173 ... b'# Comment',
174 ... b'Name <commit1@email.xx>',
174 ... b'Name <commit1@email.xx>',
175 ... b'<name@email.xx> <commit2@email.xx>',
175 ... b'<name@email.xx> <commit2@email.xx>',
176 ... b'Name <proper@email.xx> <commit3@email.xx>',
176 ... b'Name <proper@email.xx> <commit3@email.xx>',
177 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
177 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
178 ... ])
178 ... ])
179 >>> mm = parsemailmap(mmdata)
179 >>> mm = parsemailmap(mmdata)
180 >>> for key in sorted(mm.keys()):
180 >>> for key in sorted(mm.keys()):
181 ... print(key)
181 ... print(key)
182 mailmapping(email='commit1@email.xx', name=None)
182 mailmapping(email='commit1@email.xx', name=None)
183 mailmapping(email='commit2@email.xx', name=None)
183 mailmapping(email='commit2@email.xx', name=None)
184 mailmapping(email='commit3@email.xx', name=None)
184 mailmapping(email='commit3@email.xx', name=None)
185 mailmapping(email='commit4@email.xx', name='Commit')
185 mailmapping(email='commit4@email.xx', name='Commit')
186 >>> for val in sorted(mm.values()):
186 >>> for val in sorted(mm.values()):
187 ... print(val)
187 ... print(val)
188 mailmapping(email='commit1@email.xx', name='Name')
188 mailmapping(email='commit1@email.xx', name='Name')
189 mailmapping(email='name@email.xx', name=None)
189 mailmapping(email='name@email.xx', name=None)
190 mailmapping(email='proper@email.xx', name='Name')
190 mailmapping(email='proper@email.xx', name='Name')
191 mailmapping(email='proper@email.xx', name='Name')
191 mailmapping(email='proper@email.xx', name='Name')
192 """
192 """
193 mailmap = {}
193 mailmap = {}
194
194
195 if mailmapcontent is None:
195 if mailmapcontent is None:
196 return mailmap
196 return mailmap
197
197
198 for line in mailmapcontent.splitlines():
198 for line in mailmapcontent.splitlines():
199
199
200 # Don't bother checking the line if it is a comment or
200 # Don't bother checking the line if it is a comment or
201 # is an improperly formed author field
201 # is an improperly formed author field
202 if line.lstrip().startswith('#') or any(c not in line for c in '<>@'):
202 if line.lstrip().startswith('#') or any(c not in line for c in '<>@'):
203 continue
203 continue
204
204
205 # name, email hold the parsed emails and names for each line
205 # names, emails hold the parsed emails and names for each line
206 # name_builder holds the words in a persons name
206 # name_builder holds the words in a persons name
207 name, email = [], []
207 names, emails = [], []
208 namebuilder = []
208 namebuilder = []
209
209
210 for element in line.split():
210 for element in line.split():
211 if element.startswith('#'):
211 if element.startswith('#'):
212 # If we reach a comment in the mailmap file, move on
212 # If we reach a comment in the mailmap file, move on
213 break
213 break
214
214
215 elif element.startswith('<') and element.endswith('>'):
215 elif element.startswith('<') and element.endswith('>'):
216 # We have found an email.
216 # We have found an email.
217 # Parse it, and finalize any names from earlier
217 # Parse it, and finalize any names from earlier
218 email.append(element[1:-1]) # Slice off the "<>"
218 emails.append(element[1:-1]) # Slice off the "<>"
219
219
220 if namebuilder:
220 if namebuilder:
221 name.append(' '.join(namebuilder))
221 names.append(' '.join(namebuilder))
222 namebuilder = []
222 namebuilder = []
223
223
224 # Break if we have found a second email, any other
224 # Break if we have found a second email, any other
225 # data does not fit the spec for .mailmap
225 # data does not fit the spec for .mailmap
226 if len(email) > 1:
226 if len(emails) > 1:
227 break
227 break
228
228
229 else:
229 else:
230 # We have found another word in the committers name
230 # We have found another word in the committers name
231 namebuilder.append(element)
231 namebuilder.append(element)
232
232
233 mailmapkey = mailmapping(
233 mailmapkey = mailmapping(
234 email=email[-1],
234 email=emails[-1],
235 name=name[-1] if len(name) == 2 else None,
235 name=names[-1] if len(names) == 2 else None,
236 )
236 )
237
237
238 mailmap[mailmapkey] = mailmapping(
238 mailmap[mailmapkey] = mailmapping(
239 email=email[0],
239 email=emails[0],
240 name=name[0] if name else None,
240 name=names[0] if names else None,
241 )
241 )
242
242
243 return mailmap
243 return mailmap
244
244
245 def mapname(mailmap, author):
245 def mapname(mailmap, author):
246 """Returns the author field according to the mailmap cache, or
246 """Returns the author field according to the mailmap cache, or
247 the original author field.
247 the original author field.
248
248
249 >>> mmdata = b"\\n".join([
249 >>> mmdata = b"\\n".join([
250 ... b'# Comment',
250 ... b'# Comment',
251 ... b'Name <commit1@email.xx>',
251 ... b'Name <commit1@email.xx>',
252 ... b'<name@email.xx> <commit2@email.xx>',
252 ... b'<name@email.xx> <commit2@email.xx>',
253 ... b'Name <proper@email.xx> <commit3@email.xx>',
253 ... b'Name <proper@email.xx> <commit3@email.xx>',
254 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
254 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
255 ... ])
255 ... ])
256 >>> m = parsemailmap(mmdata)
256 >>> m = parsemailmap(mmdata)
257 >>> mapname(m, b'Commit <commit1@email.xx>')
257 >>> mapname(m, b'Commit <commit1@email.xx>')
258 'Name <commit1@email.xx>'
258 'Name <commit1@email.xx>'
259 >>> mapname(m, b'Name <commit2@email.xx>')
259 >>> mapname(m, b'Name <commit2@email.xx>')
260 'Name <name@email.xx>'
260 'Name <name@email.xx>'
261 >>> mapname(m, b'Commit <commit3@email.xx>')
261 >>> mapname(m, b'Commit <commit3@email.xx>')
262 'Name <proper@email.xx>'
262 'Name <proper@email.xx>'
263 >>> mapname(m, b'Commit <commit4@email.xx>')
263 >>> mapname(m, b'Commit <commit4@email.xx>')
264 'Name <proper@email.xx>'
264 'Name <proper@email.xx>'
265 >>> mapname(m, b'Unknown Name <unknown@email.com>')
265 >>> mapname(m, b'Unknown Name <unknown@email.com>')
266 'Unknown Name <unknown@email.com>'
266 'Unknown Name <unknown@email.com>'
267 """
267 """
268 # If the author field coming in isn't in the correct format,
268 # If the author field coming in isn't in the correct format,
269 # or the mailmap is empty just return the original author field
269 # or the mailmap is empty just return the original author field
270 if not isauthorwellformed(author) or not mailmap:
270 if not isauthorwellformed(author) or not mailmap:
271 return author
271 return author
272
272
273 # Turn the user name into a mailmaptup
273 # Turn the user name into a mailmaptup
274 commit = mailmapping(name=person(author), email=email(author))
274 commit = mailmapping(name=person(author), email=email(author))
275
275
276 try:
276 try:
277 # Try and use both the commit email and name as the key
277 # Try and use both the commit email and name as the key
278 proper = mailmap[commit]
278 proper = mailmap[commit]
279
279
280 except KeyError:
280 except KeyError:
281 # If the lookup fails, use just the email as the key instead
281 # If the lookup fails, use just the email as the key instead
282 # We call this commit2 as not to erase original commit fields
282 # We call this commit2 as not to erase original commit fields
283 commit2 = mailmapping(email=commit.email)
283 commit2 = mailmapping(email=commit.email)
284 proper = mailmap.get(commit2, mailmapping(None, None))
284 proper = mailmap.get(commit2, mailmapping(None, None))
285
285
286 # Return the author field with proper values filled in
286 # Return the author field with proper values filled in
287 return '%s <%s>' % (
287 return '%s <%s>' % (
288 proper.name if proper.name else commit.name,
288 proper.name if proper.name else commit.name,
289 proper.email if proper.email else commit.email,
289 proper.email if proper.email else commit.email,
290 )
290 )
291
291
292 _correctauthorformat = remod.compile(br'^[^<]+\s\<[^<>]+@[^<>]+\>$')
292 _correctauthorformat = remod.compile(br'^[^<]+\s\<[^<>]+@[^<>]+\>$')
293
293
294 def isauthorwellformed(author):
294 def isauthorwellformed(author):
295 '''Return True if the author field is well formed
295 '''Return True if the author field is well formed
296 (ie "Contributor Name <contrib@email.dom>")
296 (ie "Contributor Name <contrib@email.dom>")
297
297
298 >>> isauthorwellformed(b'Good Author <good@author.com>')
298 >>> isauthorwellformed(b'Good Author <good@author.com>')
299 True
299 True
300 >>> isauthorwellformed(b'Author <good@author.com>')
300 >>> isauthorwellformed(b'Author <good@author.com>')
301 True
301 True
302 >>> isauthorwellformed(b'Bad Author')
302 >>> isauthorwellformed(b'Bad Author')
303 False
303 False
304 >>> isauthorwellformed(b'Bad Author <author@author.com')
304 >>> isauthorwellformed(b'Bad Author <author@author.com')
305 False
305 False
306 >>> isauthorwellformed(b'Bad Author author@author.com')
306 >>> isauthorwellformed(b'Bad Author author@author.com')
307 False
307 False
308 >>> isauthorwellformed(b'<author@author.com>')
308 >>> isauthorwellformed(b'<author@author.com>')
309 False
309 False
310 >>> isauthorwellformed(b'Bad Author <author>')
310 >>> isauthorwellformed(b'Bad Author <author>')
311 False
311 False
312 '''
312 '''
313 return _correctauthorformat.match(author) is not None
313 return _correctauthorformat.match(author) is not None
314
314
315 def ellipsis(text, maxlength=400):
315 def ellipsis(text, maxlength=400):
316 """Trim string to at most maxlength (default: 400) columns in display."""
316 """Trim string to at most maxlength (default: 400) columns in display."""
317 return encoding.trim(text, maxlength, ellipsis='...')
317 return encoding.trim(text, maxlength, ellipsis='...')
318
318
319 def escapestr(s):
319 def escapestr(s):
320 # call underlying function of s.encode('string_escape') directly for
320 # call underlying function of s.encode('string_escape') directly for
321 # Python 3 compatibility
321 # Python 3 compatibility
322 return codecs.escape_encode(s)[0]
322 return codecs.escape_encode(s)[0]
323
323
324 def unescapestr(s):
324 def unescapestr(s):
325 return codecs.escape_decode(s)[0]
325 return codecs.escape_decode(s)[0]
326
326
327 def forcebytestr(obj):
327 def forcebytestr(obj):
328 """Portably format an arbitrary object (e.g. exception) into a byte
328 """Portably format an arbitrary object (e.g. exception) into a byte
329 string."""
329 string."""
330 try:
330 try:
331 return pycompat.bytestr(obj)
331 return pycompat.bytestr(obj)
332 except UnicodeEncodeError:
332 except UnicodeEncodeError:
333 # non-ascii string, may be lossy
333 # non-ascii string, may be lossy
334 return pycompat.bytestr(encoding.strtolocal(str(obj)))
334 return pycompat.bytestr(encoding.strtolocal(str(obj)))
335
335
336 def uirepr(s):
336 def uirepr(s):
337 # Avoid double backslash in Windows path repr()
337 # Avoid double backslash in Windows path repr()
338 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
338 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
339
339
340 # delay import of textwrap
340 # delay import of textwrap
341 def _MBTextWrapper(**kwargs):
341 def _MBTextWrapper(**kwargs):
342 class tw(textwrap.TextWrapper):
342 class tw(textwrap.TextWrapper):
343 """
343 """
344 Extend TextWrapper for width-awareness.
344 Extend TextWrapper for width-awareness.
345
345
346 Neither number of 'bytes' in any encoding nor 'characters' is
346 Neither number of 'bytes' in any encoding nor 'characters' is
347 appropriate to calculate terminal columns for specified string.
347 appropriate to calculate terminal columns for specified string.
348
348
349 Original TextWrapper implementation uses built-in 'len()' directly,
349 Original TextWrapper implementation uses built-in 'len()' directly,
350 so overriding is needed to use width information of each characters.
350 so overriding is needed to use width information of each characters.
351
351
352 In addition, characters classified into 'ambiguous' width are
352 In addition, characters classified into 'ambiguous' width are
353 treated as wide in East Asian area, but as narrow in other.
353 treated as wide in East Asian area, but as narrow in other.
354
354
355 This requires use decision to determine width of such characters.
355 This requires use decision to determine width of such characters.
356 """
356 """
357 def _cutdown(self, ucstr, space_left):
357 def _cutdown(self, ucstr, space_left):
358 l = 0
358 l = 0
359 colwidth = encoding.ucolwidth
359 colwidth = encoding.ucolwidth
360 for i in xrange(len(ucstr)):
360 for i in xrange(len(ucstr)):
361 l += colwidth(ucstr[i])
361 l += colwidth(ucstr[i])
362 if space_left < l:
362 if space_left < l:
363 return (ucstr[:i], ucstr[i:])
363 return (ucstr[:i], ucstr[i:])
364 return ucstr, ''
364 return ucstr, ''
365
365
366 # overriding of base class
366 # overriding of base class
367 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
367 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
368 space_left = max(width - cur_len, 1)
368 space_left = max(width - cur_len, 1)
369
369
370 if self.break_long_words:
370 if self.break_long_words:
371 cut, res = self._cutdown(reversed_chunks[-1], space_left)
371 cut, res = self._cutdown(reversed_chunks[-1], space_left)
372 cur_line.append(cut)
372 cur_line.append(cut)
373 reversed_chunks[-1] = res
373 reversed_chunks[-1] = res
374 elif not cur_line:
374 elif not cur_line:
375 cur_line.append(reversed_chunks.pop())
375 cur_line.append(reversed_chunks.pop())
376
376
377 # this overriding code is imported from TextWrapper of Python 2.6
377 # this overriding code is imported from TextWrapper of Python 2.6
378 # to calculate columns of string by 'encoding.ucolwidth()'
378 # to calculate columns of string by 'encoding.ucolwidth()'
379 def _wrap_chunks(self, chunks):
379 def _wrap_chunks(self, chunks):
380 colwidth = encoding.ucolwidth
380 colwidth = encoding.ucolwidth
381
381
382 lines = []
382 lines = []
383 if self.width <= 0:
383 if self.width <= 0:
384 raise ValueError("invalid width %r (must be > 0)" % self.width)
384 raise ValueError("invalid width %r (must be > 0)" % self.width)
385
385
386 # Arrange in reverse order so items can be efficiently popped
386 # Arrange in reverse order so items can be efficiently popped
387 # from a stack of chucks.
387 # from a stack of chucks.
388 chunks.reverse()
388 chunks.reverse()
389
389
390 while chunks:
390 while chunks:
391
391
392 # Start the list of chunks that will make up the current line.
392 # Start the list of chunks that will make up the current line.
393 # cur_len is just the length of all the chunks in cur_line.
393 # cur_len is just the length of all the chunks in cur_line.
394 cur_line = []
394 cur_line = []
395 cur_len = 0
395 cur_len = 0
396
396
397 # Figure out which static string will prefix this line.
397 # Figure out which static string will prefix this line.
398 if lines:
398 if lines:
399 indent = self.subsequent_indent
399 indent = self.subsequent_indent
400 else:
400 else:
401 indent = self.initial_indent
401 indent = self.initial_indent
402
402
403 # Maximum width for this line.
403 # Maximum width for this line.
404 width = self.width - len(indent)
404 width = self.width - len(indent)
405
405
406 # First chunk on line is whitespace -- drop it, unless this
406 # First chunk on line is whitespace -- drop it, unless this
407 # is the very beginning of the text (i.e. no lines started yet).
407 # is the very beginning of the text (i.e. no lines started yet).
408 if self.drop_whitespace and chunks[-1].strip() == r'' and lines:
408 if self.drop_whitespace and chunks[-1].strip() == r'' and lines:
409 del chunks[-1]
409 del chunks[-1]
410
410
411 while chunks:
411 while chunks:
412 l = colwidth(chunks[-1])
412 l = colwidth(chunks[-1])
413
413
414 # Can at least squeeze this chunk onto the current line.
414 # Can at least squeeze this chunk onto the current line.
415 if cur_len + l <= width:
415 if cur_len + l <= width:
416 cur_line.append(chunks.pop())
416 cur_line.append(chunks.pop())
417 cur_len += l
417 cur_len += l
418
418
419 # Nope, this line is full.
419 # Nope, this line is full.
420 else:
420 else:
421 break
421 break
422
422
423 # The current line is full, and the next chunk is too big to
423 # The current line is full, and the next chunk is too big to
424 # fit on *any* line (not just this one).
424 # fit on *any* line (not just this one).
425 if chunks and colwidth(chunks[-1]) > width:
425 if chunks and colwidth(chunks[-1]) > width:
426 self._handle_long_word(chunks, cur_line, cur_len, width)
426 self._handle_long_word(chunks, cur_line, cur_len, width)
427
427
428 # If the last chunk on this line is all whitespace, drop it.
428 # If the last chunk on this line is all whitespace, drop it.
429 if (self.drop_whitespace and
429 if (self.drop_whitespace and
430 cur_line and cur_line[-1].strip() == r''):
430 cur_line and cur_line[-1].strip() == r''):
431 del cur_line[-1]
431 del cur_line[-1]
432
432
433 # Convert current line back to a string and store it in list
433 # Convert current line back to a string and store it in list
434 # of all lines (return value).
434 # of all lines (return value).
435 if cur_line:
435 if cur_line:
436 lines.append(indent + r''.join(cur_line))
436 lines.append(indent + r''.join(cur_line))
437
437
438 return lines
438 return lines
439
439
440 global _MBTextWrapper
440 global _MBTextWrapper
441 _MBTextWrapper = tw
441 _MBTextWrapper = tw
442 return tw(**kwargs)
442 return tw(**kwargs)
443
443
444 def wrap(line, width, initindent='', hangindent=''):
444 def wrap(line, width, initindent='', hangindent=''):
445 maxindent = max(len(hangindent), len(initindent))
445 maxindent = max(len(hangindent), len(initindent))
446 if width <= maxindent:
446 if width <= maxindent:
447 # adjust for weird terminal size
447 # adjust for weird terminal size
448 width = max(78, maxindent + 1)
448 width = max(78, maxindent + 1)
449 line = line.decode(pycompat.sysstr(encoding.encoding),
449 line = line.decode(pycompat.sysstr(encoding.encoding),
450 pycompat.sysstr(encoding.encodingmode))
450 pycompat.sysstr(encoding.encodingmode))
451 initindent = initindent.decode(pycompat.sysstr(encoding.encoding),
451 initindent = initindent.decode(pycompat.sysstr(encoding.encoding),
452 pycompat.sysstr(encoding.encodingmode))
452 pycompat.sysstr(encoding.encodingmode))
453 hangindent = hangindent.decode(pycompat.sysstr(encoding.encoding),
453 hangindent = hangindent.decode(pycompat.sysstr(encoding.encoding),
454 pycompat.sysstr(encoding.encodingmode))
454 pycompat.sysstr(encoding.encodingmode))
455 wrapper = _MBTextWrapper(width=width,
455 wrapper = _MBTextWrapper(width=width,
456 initial_indent=initindent,
456 initial_indent=initindent,
457 subsequent_indent=hangindent)
457 subsequent_indent=hangindent)
458 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
458 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
459
459
460 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
460 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
461 '0': False, 'no': False, 'false': False, 'off': False,
461 '0': False, 'no': False, 'false': False, 'off': False,
462 'never': False}
462 'never': False}
463
463
464 def parsebool(s):
464 def parsebool(s):
465 """Parse s into a boolean.
465 """Parse s into a boolean.
466
466
467 If s is not a valid boolean, returns None.
467 If s is not a valid boolean, returns None.
468 """
468 """
469 return _booleans.get(s.lower(), None)
469 return _booleans.get(s.lower(), None)
General Comments 0
You need to be logged in to leave comments. Login now