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