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