##// END OF EJS Templates
templates: extract function to `stringutil` for getting first line of text...
Martin von Zweigbergk -
r49885:51aed118 default
parent child Browse files
Show More
@@ -1,554 +1,551 b''
1 1 # templatefilters.py - common template expansion filters
2 2 #
3 3 # Copyright 2005-2008 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8
9 9 import os
10 10 import re
11 11 import time
12 12
13 13 from .i18n import _
14 14 from .node import hex
15 15 from . import (
16 16 encoding,
17 17 error,
18 18 pycompat,
19 19 registrar,
20 20 smartset,
21 21 templateutil,
22 22 url,
23 23 util,
24 24 )
25 25 from .utils import (
26 26 cborutil,
27 27 dateutil,
28 28 stringutil,
29 29 )
30 30
31 31 urlerr = util.urlerr
32 32 urlreq = util.urlreq
33 33
34 34 # filters are callables like:
35 35 # fn(obj)
36 36 # with:
37 37 # obj - object to be filtered (text, date, list and so on)
38 38 filters = {}
39 39
40 40 templatefilter = registrar.templatefilter(filters)
41 41
42 42
43 43 @templatefilter(b'addbreaks', intype=bytes)
44 44 def addbreaks(text):
45 45 """Any text. Add an XHTML "<br />" tag before the end of
46 46 every line except the last.
47 47 """
48 48 return text.replace(b'\n', b'<br/>\n')
49 49
50 50
51 51 agescales = [
52 52 (b"year", 3600 * 24 * 365, b'Y'),
53 53 (b"month", 3600 * 24 * 30, b'M'),
54 54 (b"week", 3600 * 24 * 7, b'W'),
55 55 (b"day", 3600 * 24, b'd'),
56 56 (b"hour", 3600, b'h'),
57 57 (b"minute", 60, b'm'),
58 58 (b"second", 1, b's'),
59 59 ]
60 60
61 61
62 62 @templatefilter(b'age', intype=templateutil.date)
63 63 def age(date, abbrev=False):
64 64 """Date. Returns a human-readable date/time difference between the
65 65 given date/time and the current date/time.
66 66 """
67 67
68 68 def plural(t, c):
69 69 if c == 1:
70 70 return t
71 71 return t + b"s"
72 72
73 73 def fmt(t, c, a):
74 74 if abbrev:
75 75 return b"%d%s" % (c, a)
76 76 return b"%d %s" % (c, plural(t, c))
77 77
78 78 now = time.time()
79 79 then = date[0]
80 80 future = False
81 81 if then > now:
82 82 future = True
83 83 delta = max(1, int(then - now))
84 84 if delta > agescales[0][1] * 30:
85 85 return b'in the distant future'
86 86 else:
87 87 delta = max(1, int(now - then))
88 88 if delta > agescales[0][1] * 2:
89 89 return dateutil.shortdate(date)
90 90
91 91 for t, s, a in agescales:
92 92 n = delta // s
93 93 if n >= 2 or s == 1:
94 94 if future:
95 95 return b'%s from now' % fmt(t, n, a)
96 96 return b'%s ago' % fmt(t, n, a)
97 97
98 98
99 99 @templatefilter(b'basename', intype=bytes)
100 100 def basename(path):
101 101 """Any text. Treats the text as a path, and returns the last
102 102 component of the path after splitting by the path separator.
103 103 For example, "foo/bar/baz" becomes "baz" and "foo/bar//" becomes "".
104 104 """
105 105 return os.path.basename(path)
106 106
107 107
108 108 def _tocborencodable(obj):
109 109 if isinstance(obj, smartset.abstractsmartset):
110 110 return list(obj)
111 111 return obj
112 112
113 113
114 114 @templatefilter(b'cbor')
115 115 def cbor(obj):
116 116 """Any object. Serializes the object to CBOR bytes."""
117 117 # cborutil is stricter about type than json() filter
118 118 obj = pycompat.rapply(_tocborencodable, obj)
119 119 return b''.join(cborutil.streamencode(obj))
120 120
121 121
122 122 @templatefilter(b'commondir')
123 123 def commondir(filelist):
124 124 """List of text. Treats each list item as file name with /
125 125 as path separator and returns the longest common directory
126 126 prefix shared by all list items.
127 127 Returns the empty string if no common prefix exists.
128 128
129 129 The list items are not normalized, i.e. "foo/../bar" is handled as
130 130 file "bar" in the directory "foo/..". Leading slashes are ignored.
131 131
132 132 For example, ["foo/bar/baz", "foo/baz/bar"] becomes "foo" and
133 133 ["foo/bar", "baz"] becomes "".
134 134 """
135 135
136 136 def common(a, b):
137 137 if len(a) > len(b):
138 138 a = b[: len(a)]
139 139 elif len(b) > len(a):
140 140 b = b[: len(a)]
141 141 if a == b:
142 142 return a
143 143 for i in pycompat.xrange(len(a)):
144 144 if a[i] != b[i]:
145 145 return a[:i]
146 146 return a
147 147
148 148 try:
149 149 if not filelist:
150 150 return b""
151 151 dirlist = [f.lstrip(b'/').split(b'/')[:-1] for f in filelist]
152 152 if len(dirlist) == 1:
153 153 return b'/'.join(dirlist[0])
154 154 a = min(dirlist)
155 155 b = max(dirlist)
156 156 # The common prefix of a and b is shared with all
157 157 # elements of the list since Python sorts lexicographical
158 158 # and [1, x] after [1].
159 159 return b'/'.join(common(a, b))
160 160 except TypeError:
161 161 raise error.ParseError(_(b'argument is not a list of text'))
162 162
163 163
164 164 @templatefilter(b'count')
165 165 def count(i):
166 166 """List or text. Returns the length as an integer."""
167 167 try:
168 168 return len(i)
169 169 except TypeError:
170 170 raise error.ParseError(_(b'not countable'))
171 171
172 172
173 173 @templatefilter(b'dirname', intype=bytes)
174 174 def dirname(path):
175 175 """Any text. Treats the text as a path, and strips the last
176 176 component of the path after splitting by the path separator.
177 177 """
178 178 return os.path.dirname(path)
179 179
180 180
181 181 @templatefilter(b'domain', intype=bytes)
182 182 def domain(author):
183 183 """Any text. Finds the first string that looks like an email
184 184 address, and extracts just the domain component. Example: ``User
185 185 <user@example.com>`` becomes ``example.com``.
186 186 """
187 187 f = author.find(b'@')
188 188 if f == -1:
189 189 return b''
190 190 author = author[f + 1 :]
191 191 f = author.find(b'>')
192 192 if f >= 0:
193 193 author = author[:f]
194 194 return author
195 195
196 196
197 197 @templatefilter(b'email', intype=bytes)
198 198 def email(text):
199 199 """Any text. Extracts the first string that looks like an email
200 200 address. Example: ``User <user@example.com>`` becomes
201 201 ``user@example.com``.
202 202 """
203 203 return stringutil.email(text)
204 204
205 205
206 206 @templatefilter(b'escape', intype=bytes)
207 207 def escape(text):
208 208 """Any text. Replaces the special XML/XHTML characters "&", "<"
209 209 and ">" with XML entities, and filters out NUL characters.
210 210 """
211 211 return url.escape(text.replace(b'\0', b''), True)
212 212
213 213
214 214 para_re = None
215 215 space_re = None
216 216
217 217
218 218 def fill(text, width, initindent=b'', hangindent=b''):
219 219 '''fill many paragraphs with optional indentation.'''
220 220 global para_re, space_re
221 221 if para_re is None:
222 222 para_re = re.compile(b'(\n\n|\n\\s*[-*]\\s*)', re.M)
223 223 space_re = re.compile(br' +')
224 224
225 225 def findparas():
226 226 start = 0
227 227 while True:
228 228 m = para_re.search(text, start)
229 229 if not m:
230 230 uctext = encoding.unifromlocal(text[start:])
231 231 w = len(uctext)
232 232 while w > 0 and uctext[w - 1].isspace():
233 233 w -= 1
234 234 yield (
235 235 encoding.unitolocal(uctext[:w]),
236 236 encoding.unitolocal(uctext[w:]),
237 237 )
238 238 break
239 239 yield text[start : m.start(0)], m.group(1)
240 240 start = m.end(1)
241 241
242 242 return b"".join(
243 243 [
244 244 stringutil.wrap(
245 245 space_re.sub(b' ', stringutil.wrap(para, width)),
246 246 width,
247 247 initindent,
248 248 hangindent,
249 249 )
250 250 + rest
251 251 for para, rest in findparas()
252 252 ]
253 253 )
254 254
255 255
256 256 @templatefilter(b'fill68', intype=bytes)
257 257 def fill68(text):
258 258 """Any text. Wraps the text to fit in 68 columns."""
259 259 return fill(text, 68)
260 260
261 261
262 262 @templatefilter(b'fill76', intype=bytes)
263 263 def fill76(text):
264 264 """Any text. Wraps the text to fit in 76 columns."""
265 265 return fill(text, 76)
266 266
267 267
268 268 @templatefilter(b'firstline', intype=bytes)
269 269 def firstline(text):
270 270 """Any text. Returns the first line of text."""
271 try:
272 return text.splitlines()[0]
273 except IndexError:
274 return b''
271 return stringutil.firstline(text)
275 272
276 273
277 274 @templatefilter(b'hex', intype=bytes)
278 275 def hexfilter(text):
279 276 """Any text. Convert a binary Mercurial node identifier into
280 277 its long hexadecimal representation.
281 278 """
282 279 return hex(text)
283 280
284 281
285 282 @templatefilter(b'hgdate', intype=templateutil.date)
286 283 def hgdate(text):
287 284 """Date. Returns the date as a pair of numbers: "1157407993
288 285 25200" (Unix timestamp, timezone offset).
289 286 """
290 287 return b"%d %d" % text
291 288
292 289
293 290 @templatefilter(b'isodate', intype=templateutil.date)
294 291 def isodate(text):
295 292 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
296 293 +0200".
297 294 """
298 295 return dateutil.datestr(text, b'%Y-%m-%d %H:%M %1%2')
299 296
300 297
301 298 @templatefilter(b'isodatesec', intype=templateutil.date)
302 299 def isodatesec(text):
303 300 """Date. Returns the date in ISO 8601 format, including
304 301 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
305 302 filter.
306 303 """
307 304 return dateutil.datestr(text, b'%Y-%m-%d %H:%M:%S %1%2')
308 305
309 306
310 307 def indent(text, prefix, firstline=b''):
311 308 '''indent each non-empty line of text after first with prefix.'''
312 309 lines = text.splitlines()
313 310 num_lines = len(lines)
314 311 endswithnewline = text[-1:] == b'\n'
315 312
316 313 def indenter():
317 314 for i in pycompat.xrange(num_lines):
318 315 l = lines[i]
319 316 if l.strip():
320 317 yield prefix if i else firstline
321 318 yield l
322 319 if i < num_lines - 1 or endswithnewline:
323 320 yield b'\n'
324 321
325 322 return b"".join(indenter())
326 323
327 324
328 325 @templatefilter(b'json')
329 326 def json(obj, paranoid=True):
330 327 """Any object. Serializes the object to a JSON formatted text."""
331 328 if obj is None:
332 329 return b'null'
333 330 elif obj is False:
334 331 return b'false'
335 332 elif obj is True:
336 333 return b'true'
337 334 elif isinstance(obj, (int, int, float)):
338 335 return pycompat.bytestr(obj)
339 336 elif isinstance(obj, bytes):
340 337 return b'"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
341 338 elif isinstance(obj, type(u'')):
342 339 raise error.ProgrammingError(
343 340 b'Mercurial only does output with bytes: %r' % obj
344 341 )
345 342 elif util.safehasattr(obj, b'keys'):
346 343 out = [
347 344 b'"%s": %s'
348 345 % (encoding.jsonescape(k, paranoid=paranoid), json(v, paranoid))
349 346 for k, v in sorted(obj.items())
350 347 ]
351 348 return b'{' + b', '.join(out) + b'}'
352 349 elif util.safehasattr(obj, b'__iter__'):
353 350 out = [json(i, paranoid) for i in obj]
354 351 return b'[' + b', '.join(out) + b']'
355 352 raise error.ProgrammingError(b'cannot encode %r' % obj)
356 353
357 354
358 355 @templatefilter(b'lower', intype=bytes)
359 356 def lower(text):
360 357 """Any text. Converts the text to lowercase."""
361 358 return encoding.lower(text)
362 359
363 360
364 361 @templatefilter(b'nonempty', intype=bytes)
365 362 def nonempty(text):
366 363 """Any text. Returns '(none)' if the string is empty."""
367 364 return text or b"(none)"
368 365
369 366
370 367 @templatefilter(b'obfuscate', intype=bytes)
371 368 def obfuscate(text):
372 369 """Any text. Returns the input text rendered as a sequence of
373 370 XML entities.
374 371 """
375 372 text = str(text, pycompat.sysstr(encoding.encoding), r'replace')
376 373 return b''.join([b'&#%d;' % ord(c) for c in text])
377 374
378 375
379 376 @templatefilter(b'permissions', intype=bytes)
380 377 def permissions(flags):
381 378 if b"l" in flags:
382 379 return b"lrwxrwxrwx"
383 380 if b"x" in flags:
384 381 return b"-rwxr-xr-x"
385 382 return b"-rw-r--r--"
386 383
387 384
388 385 @templatefilter(b'person', intype=bytes)
389 386 def person(author):
390 387 """Any text. Returns the name before an email address,
391 388 interpreting it as per RFC 5322.
392 389 """
393 390 return stringutil.person(author)
394 391
395 392
396 393 @templatefilter(b'revescape', intype=bytes)
397 394 def revescape(text):
398 395 """Any text. Escapes all "special" characters, except @.
399 396 Forward slashes are escaped twice to prevent web servers from prematurely
400 397 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
401 398 """
402 399 return urlreq.quote(text, safe=b'/@').replace(b'/', b'%252F')
403 400
404 401
405 402 @templatefilter(b'rfc3339date', intype=templateutil.date)
406 403 def rfc3339date(text):
407 404 """Date. Returns a date using the Internet date format
408 405 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
409 406 """
410 407 return dateutil.datestr(text, b"%Y-%m-%dT%H:%M:%S%1:%2")
411 408
412 409
413 410 @templatefilter(b'rfc822date', intype=templateutil.date)
414 411 def rfc822date(text):
415 412 """Date. Returns a date using the same format used in email
416 413 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
417 414 """
418 415 return dateutil.datestr(text, b"%a, %d %b %Y %H:%M:%S %1%2")
419 416
420 417
421 418 @templatefilter(b'short', intype=bytes)
422 419 def short(text):
423 420 """Changeset hash. Returns the short form of a changeset hash,
424 421 i.e. a 12 hexadecimal digit string.
425 422 """
426 423 return text[:12]
427 424
428 425
429 426 @templatefilter(b'shortbisect', intype=bytes)
430 427 def shortbisect(label):
431 428 """Any text. Treats `label` as a bisection status, and
432 429 returns a single-character representing the status (G: good, B: bad,
433 430 S: skipped, U: untested, I: ignored). Returns single space if `text`
434 431 is not a valid bisection status.
435 432 """
436 433 if label:
437 434 return label[0:1].upper()
438 435 return b' '
439 436
440 437
441 438 @templatefilter(b'shortdate', intype=templateutil.date)
442 439 def shortdate(text):
443 440 """Date. Returns a date like "2006-09-18"."""
444 441 return dateutil.shortdate(text)
445 442
446 443
447 444 @templatefilter(b'slashpath', intype=bytes)
448 445 def slashpath(path):
449 446 """Any text. Replaces the native path separator with slash."""
450 447 return util.pconvert(path)
451 448
452 449
453 450 @templatefilter(b'splitlines', intype=bytes)
454 451 def splitlines(text):
455 452 """Any text. Split text into a list of lines."""
456 453 return templateutil.hybridlist(text.splitlines(), name=b'line')
457 454
458 455
459 456 @templatefilter(b'stringescape', intype=bytes)
460 457 def stringescape(text):
461 458 return stringutil.escapestr(text)
462 459
463 460
464 461 @templatefilter(b'stringify', intype=bytes)
465 462 def stringify(thing):
466 463 """Any type. Turns the value into text by converting values into
467 464 text and concatenating them.
468 465 """
469 466 return thing # coerced by the intype
470 467
471 468
472 469 @templatefilter(b'stripdir', intype=bytes)
473 470 def stripdir(text):
474 471 """Treat the text as path and strip a directory level, if
475 472 possible. For example, "foo" and "foo/bar" becomes "foo".
476 473 """
477 474 dir = os.path.dirname(text)
478 475 if dir == b"":
479 476 return os.path.basename(text)
480 477 else:
481 478 return dir
482 479
483 480
484 481 @templatefilter(b'tabindent', intype=bytes)
485 482 def tabindent(text):
486 483 """Any text. Returns the text, with every non-empty line
487 484 except the first starting with a tab character.
488 485 """
489 486 return indent(text, b'\t')
490 487
491 488
492 489 @templatefilter(b'upper', intype=bytes)
493 490 def upper(text):
494 491 """Any text. Converts the text to uppercase."""
495 492 return encoding.upper(text)
496 493
497 494
498 495 @templatefilter(b'urlescape', intype=bytes)
499 496 def urlescape(text):
500 497 """Any text. Escapes all "special" characters. For example,
501 498 "foo bar" becomes "foo%20bar".
502 499 """
503 500 return urlreq.quote(text)
504 501
505 502
506 503 @templatefilter(b'user', intype=bytes)
507 504 def userfilter(text):
508 505 """Any text. Returns a short representation of a user name or email
509 506 address."""
510 507 return stringutil.shortuser(text)
511 508
512 509
513 510 @templatefilter(b'emailuser', intype=bytes)
514 511 def emailuser(text):
515 512 """Any text. Returns the user portion of an email address."""
516 513 return stringutil.emailuser(text)
517 514
518 515
519 516 @templatefilter(b'utf8', intype=bytes)
520 517 def utf8(text):
521 518 """Any text. Converts from the local character encoding to UTF-8."""
522 519 return encoding.fromlocal(text)
523 520
524 521
525 522 @templatefilter(b'xmlescape', intype=bytes)
526 523 def xmlescape(text):
527 524 text = (
528 525 text.replace(b'&', b'&amp;')
529 526 .replace(b'<', b'&lt;')
530 527 .replace(b'>', b'&gt;')
531 528 .replace(b'"', b'&quot;')
532 529 .replace(b"'", b'&#39;')
533 530 ) # &apos; invalid in HTML
534 531 return re.sub(b'[\x00-\x08\x0B\x0C\x0E-\x1F]', b' ', text)
535 532
536 533
537 534 def websub(text, websubtable):
538 535 """:websub: Any text. Only applies to hgweb. Applies the regular
539 536 expression replacements defined in the websub section.
540 537 """
541 538 if websubtable:
542 539 for regexp, format in websubtable:
543 540 text = regexp.sub(format, text)
544 541 return text
545 542
546 543
547 544 def loadfilter(ui, extname, registrarobj):
548 545 """Load template filter from specified registrarobj"""
549 546 for name, func in registrarobj._table.items():
550 547 filters[name] = func
551 548
552 549
553 550 # tell hggettext to extract docstrings from these functions:
554 551 i18nfunctions = filters.values()
@@ -1,967 +1,975 b''
1 1 # stringutil.py - utility for generic string formatting, parsing, etc.
2 2 #
3 3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10
11 11 import ast
12 12 import codecs
13 13 import re as remod
14 14 import textwrap
15 15 import types
16 16
17 17 from ..i18n import _
18 18 from ..thirdparty import attr
19 19
20 20 from .. import (
21 21 encoding,
22 22 error,
23 23 pycompat,
24 24 )
25 25
26 26 # regex special chars pulled from https://bugs.python.org/issue29995
27 27 # which was part of Python 3.7.
28 28 _respecial = pycompat.bytestr(b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f')
29 29 _regexescapemap = {ord(i): (b'\\' + i).decode('latin1') for i in _respecial}
30 30 regexbytesescapemap = {i: (b'\\' + i) for i in _respecial}
31 31
32 32
33 33 def reescape(pat):
34 34 """Drop-in replacement for re.escape."""
35 35 # NOTE: it is intentional that this works on unicodes and not
36 36 # bytes, as it's only possible to do the escaping with
37 37 # unicode.translate, not bytes.translate. Sigh.
38 38 wantuni = True
39 39 if isinstance(pat, bytes):
40 40 wantuni = False
41 41 pat = pat.decode('latin1')
42 42 pat = pat.translate(_regexescapemap)
43 43 if wantuni:
44 44 return pat
45 45 return pat.encode('latin1')
46 46
47 47
48 48 def pprint(o, bprefix=False, indent=0, level=0):
49 49 """Pretty print an object."""
50 50 return b''.join(pprintgen(o, bprefix=bprefix, indent=indent, level=level))
51 51
52 52
53 53 def pprintgen(o, bprefix=False, indent=0, level=0):
54 54 """Pretty print an object to a generator of atoms.
55 55
56 56 ``bprefix`` is a flag influencing whether bytestrings are preferred with
57 57 a ``b''`` prefix.
58 58
59 59 ``indent`` controls whether collections and nested data structures
60 60 span multiple lines via the indentation amount in spaces. By default,
61 61 no newlines are emitted.
62 62
63 63 ``level`` specifies the initial indent level. Used if ``indent > 0``.
64 64 """
65 65
66 66 if isinstance(o, bytes):
67 67 if bprefix:
68 68 yield b"b'%s'" % escapestr(o)
69 69 else:
70 70 yield b"'%s'" % escapestr(o)
71 71 elif isinstance(o, bytearray):
72 72 # codecs.escape_encode() can't handle bytearray, so escapestr fails
73 73 # without coercion.
74 74 yield b"bytearray['%s']" % escapestr(bytes(o))
75 75 elif isinstance(o, list):
76 76 if not o:
77 77 yield b'[]'
78 78 return
79 79
80 80 yield b'['
81 81
82 82 if indent:
83 83 level += 1
84 84 yield b'\n'
85 85 yield b' ' * (level * indent)
86 86
87 87 for i, a in enumerate(o):
88 88 for chunk in pprintgen(
89 89 a, bprefix=bprefix, indent=indent, level=level
90 90 ):
91 91 yield chunk
92 92
93 93 if i + 1 < len(o):
94 94 if indent:
95 95 yield b',\n'
96 96 yield b' ' * (level * indent)
97 97 else:
98 98 yield b', '
99 99
100 100 if indent:
101 101 level -= 1
102 102 yield b'\n'
103 103 yield b' ' * (level * indent)
104 104
105 105 yield b']'
106 106 elif isinstance(o, dict):
107 107 if not o:
108 108 yield b'{}'
109 109 return
110 110
111 111 yield b'{'
112 112
113 113 if indent:
114 114 level += 1
115 115 yield b'\n'
116 116 yield b' ' * (level * indent)
117 117
118 118 for i, (k, v) in enumerate(sorted(o.items())):
119 119 for chunk in pprintgen(
120 120 k, bprefix=bprefix, indent=indent, level=level
121 121 ):
122 122 yield chunk
123 123
124 124 yield b': '
125 125
126 126 for chunk in pprintgen(
127 127 v, bprefix=bprefix, indent=indent, level=level
128 128 ):
129 129 yield chunk
130 130
131 131 if i + 1 < len(o):
132 132 if indent:
133 133 yield b',\n'
134 134 yield b' ' * (level * indent)
135 135 else:
136 136 yield b', '
137 137
138 138 if indent:
139 139 level -= 1
140 140 yield b'\n'
141 141 yield b' ' * (level * indent)
142 142
143 143 yield b'}'
144 144 elif isinstance(o, set):
145 145 if not o:
146 146 yield b'set([])'
147 147 return
148 148
149 149 yield b'set(['
150 150
151 151 if indent:
152 152 level += 1
153 153 yield b'\n'
154 154 yield b' ' * (level * indent)
155 155
156 156 for i, k in enumerate(sorted(o)):
157 157 for chunk in pprintgen(
158 158 k, bprefix=bprefix, indent=indent, level=level
159 159 ):
160 160 yield chunk
161 161
162 162 if i + 1 < len(o):
163 163 if indent:
164 164 yield b',\n'
165 165 yield b' ' * (level * indent)
166 166 else:
167 167 yield b', '
168 168
169 169 if indent:
170 170 level -= 1
171 171 yield b'\n'
172 172 yield b' ' * (level * indent)
173 173
174 174 yield b'])'
175 175 elif isinstance(o, tuple):
176 176 if not o:
177 177 yield b'()'
178 178 return
179 179
180 180 yield b'('
181 181
182 182 if indent:
183 183 level += 1
184 184 yield b'\n'
185 185 yield b' ' * (level * indent)
186 186
187 187 for i, a in enumerate(o):
188 188 for chunk in pprintgen(
189 189 a, bprefix=bprefix, indent=indent, level=level
190 190 ):
191 191 yield chunk
192 192
193 193 if i + 1 < len(o):
194 194 if indent:
195 195 yield b',\n'
196 196 yield b' ' * (level * indent)
197 197 else:
198 198 yield b', '
199 199
200 200 if indent:
201 201 level -= 1
202 202 yield b'\n'
203 203 yield b' ' * (level * indent)
204 204
205 205 yield b')'
206 206 elif isinstance(o, types.GeneratorType):
207 207 # Special case of empty generator.
208 208 try:
209 209 nextitem = next(o)
210 210 except StopIteration:
211 211 yield b'gen[]'
212 212 return
213 213
214 214 yield b'gen['
215 215
216 216 if indent:
217 217 level += 1
218 218 yield b'\n'
219 219 yield b' ' * (level * indent)
220 220
221 221 last = False
222 222
223 223 while not last:
224 224 current = nextitem
225 225
226 226 try:
227 227 nextitem = next(o)
228 228 except StopIteration:
229 229 last = True
230 230
231 231 for chunk in pprintgen(
232 232 current, bprefix=bprefix, indent=indent, level=level
233 233 ):
234 234 yield chunk
235 235
236 236 if not last:
237 237 if indent:
238 238 yield b',\n'
239 239 yield b' ' * (level * indent)
240 240 else:
241 241 yield b', '
242 242
243 243 if indent:
244 244 level -= 1
245 245 yield b'\n'
246 246 yield b' ' * (level * indent)
247 247
248 248 yield b']'
249 249 else:
250 250 yield pycompat.byterepr(o)
251 251
252 252
253 253 def prettyrepr(o):
254 254 """Pretty print a representation of a possibly-nested object"""
255 255 lines = []
256 256 rs = pycompat.byterepr(o)
257 257 p0 = p1 = 0
258 258 while p0 < len(rs):
259 259 # '... field=<type ... field=<type ...'
260 260 # ~~~~~~~~~~~~~~~~
261 261 # p0 p1 q0 q1
262 262 q0 = -1
263 263 q1 = rs.find(b'<', p1 + 1)
264 264 if q1 < 0:
265 265 q1 = len(rs)
266 266 # pytype: disable=wrong-arg-count
267 267 # TODO: figure out why pytype doesn't recognize the optional start
268 268 # arg
269 269 elif q1 > p1 + 1 and rs.startswith(b'=', q1 - 1):
270 270 # pytype: enable=wrong-arg-count
271 271 # backtrack for ' field=<'
272 272 q0 = rs.rfind(b' ', p1 + 1, q1 - 1)
273 273 if q0 < 0:
274 274 q0 = q1
275 275 else:
276 276 q0 += 1 # skip ' '
277 277 l = rs.count(b'<', 0, p0) - rs.count(b'>', 0, p0)
278 278 assert l >= 0
279 279 lines.append((l, rs[p0:q0].rstrip()))
280 280 p0, p1 = q0, q1
281 281 return b'\n'.join(b' ' * l + s for l, s in lines)
282 282
283 283
284 284 def buildrepr(r):
285 285 """Format an optional printable representation from unexpanded bits
286 286
287 287 ======== =================================
288 288 type(r) example
289 289 ======== =================================
290 290 tuple ('<not %r>', other)
291 291 bytes '<branch closed>'
292 292 callable lambda: '<branch %r>' % sorted(b)
293 293 object other
294 294 ======== =================================
295 295 """
296 296 if r is None:
297 297 return b''
298 298 elif isinstance(r, tuple):
299 299 return r[0] % pycompat.rapply(pycompat.maybebytestr, r[1:])
300 300 elif isinstance(r, bytes):
301 301 return r
302 302 elif callable(r):
303 303 return r()
304 304 else:
305 305 return pprint(r)
306 306
307 307
308 308 def binary(s):
309 309 """return true if a string is binary data"""
310 310 return bool(s and b'\0' in s)
311 311
312 312
313 313 def _splitpattern(pattern):
314 314 if pattern.startswith(b're:'):
315 315 return b're', pattern[3:]
316 316 elif pattern.startswith(b'literal:'):
317 317 return b'literal', pattern[8:]
318 318 return b'literal', pattern
319 319
320 320
321 321 def stringmatcher(pattern, casesensitive=True):
322 322 """
323 323 accepts a string, possibly starting with 're:' or 'literal:' prefix.
324 324 returns the matcher name, pattern, and matcher function.
325 325 missing or unknown prefixes are treated as literal matches.
326 326
327 327 helper for tests:
328 328 >>> def test(pattern, *tests):
329 329 ... kind, pattern, matcher = stringmatcher(pattern)
330 330 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
331 331 >>> def itest(pattern, *tests):
332 332 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
333 333 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
334 334
335 335 exact matching (no prefix):
336 336 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
337 337 ('literal', 'abcdefg', [False, False, True])
338 338
339 339 regex matching ('re:' prefix)
340 340 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
341 341 ('re', 'a.+b', [False, False, True])
342 342
343 343 force exact matches ('literal:' prefix)
344 344 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
345 345 ('literal', 're:foobar', [False, True])
346 346
347 347 unknown prefixes are ignored and treated as literals
348 348 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
349 349 ('literal', 'foo:bar', [False, False, True])
350 350
351 351 case insensitive regex matches
352 352 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
353 353 ('re', 'A.+b', [False, False, True])
354 354
355 355 case insensitive literal matches
356 356 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
357 357 ('literal', 'ABCDEFG', [False, False, True])
358 358 """
359 359 kind, pattern = _splitpattern(pattern)
360 360 if kind == b're':
361 361 try:
362 362 flags = 0
363 363 if not casesensitive:
364 364 flags = remod.I
365 365 regex = remod.compile(pattern, flags)
366 366 except remod.error as e:
367 367 raise error.ParseError(
368 368 _(b'invalid regular expression: %s') % forcebytestr(e)
369 369 )
370 370 return kind, pattern, regex.search
371 371 elif kind == b'literal':
372 372 if casesensitive:
373 373 match = pattern.__eq__
374 374 else:
375 375 ipat = encoding.lower(pattern)
376 376 match = lambda s: ipat == encoding.lower(s)
377 377 return kind, pattern, match
378 378
379 379 raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind)
380 380
381 381
382 382 def substringregexp(pattern, flags=0):
383 383 """Build a regexp object from a string pattern possibly starting with
384 384 're:' or 'literal:' prefix.
385 385
386 386 helper for tests:
387 387 >>> def test(pattern, *tests):
388 388 ... regexp = substringregexp(pattern)
389 389 ... return [bool(regexp.search(t)) for t in tests]
390 390 >>> def itest(pattern, *tests):
391 391 ... regexp = substringregexp(pattern, remod.I)
392 392 ... return [bool(regexp.search(t)) for t in tests]
393 393
394 394 substring matching (no prefix):
395 395 >>> test(b'bcde', b'abc', b'def', b'abcdefg')
396 396 [False, False, True]
397 397
398 398 substring pattern should be escaped:
399 399 >>> substringregexp(b'.bc').pattern
400 400 '\\\\.bc'
401 401 >>> test(b'.bc', b'abc', b'def', b'abcdefg')
402 402 [False, False, False]
403 403
404 404 regex matching ('re:' prefix)
405 405 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
406 406 [False, False, True]
407 407
408 408 force substring matches ('literal:' prefix)
409 409 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
410 410 [False, True]
411 411
412 412 case insensitive literal matches
413 413 >>> itest(b'BCDE', b'abc', b'def', b'abcdefg')
414 414 [False, False, True]
415 415
416 416 case insensitive regex matches
417 417 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
418 418 [False, False, True]
419 419 """
420 420 kind, pattern = _splitpattern(pattern)
421 421 if kind == b're':
422 422 try:
423 423 return remod.compile(pattern, flags)
424 424 except remod.error as e:
425 425 raise error.ParseError(
426 426 _(b'invalid regular expression: %s') % forcebytestr(e)
427 427 )
428 428 elif kind == b'literal':
429 429 return remod.compile(remod.escape(pattern), flags)
430 430
431 431 raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind)
432 432
433 433
434 434 def shortuser(user):
435 435 """Return a short representation of a user name or email address."""
436 436 f = user.find(b'@')
437 437 if f >= 0:
438 438 user = user[:f]
439 439 f = user.find(b'<')
440 440 if f >= 0:
441 441 user = user[f + 1 :]
442 442 f = user.find(b' ')
443 443 if f >= 0:
444 444 user = user[:f]
445 445 f = user.find(b'.')
446 446 if f >= 0:
447 447 user = user[:f]
448 448 return user
449 449
450 450
451 451 def emailuser(user):
452 452 """Return the user portion of an email address."""
453 453 f = user.find(b'@')
454 454 if f >= 0:
455 455 user = user[:f]
456 456 f = user.find(b'<')
457 457 if f >= 0:
458 458 user = user[f + 1 :]
459 459 return user
460 460
461 461
462 462 def email(author):
463 463 '''get email of author.'''
464 464 r = author.find(b'>')
465 465 if r == -1:
466 466 r = None
467 467 return author[author.find(b'<') + 1 : r]
468 468
469 469
470 470 def person(author):
471 471 """Returns the name before an email address,
472 472 interpreting it as per RFC 5322
473 473
474 474 >>> person(b'foo@bar')
475 475 'foo'
476 476 >>> person(b'Foo Bar <foo@bar>')
477 477 'Foo Bar'
478 478 >>> person(b'"Foo Bar" <foo@bar>')
479 479 'Foo Bar'
480 480 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
481 481 'Foo "buz" Bar'
482 482 >>> # The following are invalid, but do exist in real-life
483 483 ...
484 484 >>> person(b'Foo "buz" Bar <foo@bar>')
485 485 'Foo "buz" Bar'
486 486 >>> person(b'"Foo Bar <foo@bar>')
487 487 'Foo Bar'
488 488 """
489 489 if b'@' not in author:
490 490 return author
491 491 f = author.find(b'<')
492 492 if f != -1:
493 493 return author[:f].strip(b' "').replace(b'\\"', b'"')
494 494 f = author.find(b'@')
495 495 return author[:f].replace(b'.', b' ')
496 496
497 497
498 498 @attr.s(hash=True)
499 499 class mailmapping:
500 500 """Represents a username/email key or value in
501 501 a mailmap file"""
502 502
503 503 email = attr.ib()
504 504 name = attr.ib(default=None)
505 505
506 506
507 507 def _ismailmaplineinvalid(names, emails):
508 508 """Returns True if the parsed names and emails
509 509 in a mailmap entry are invalid.
510 510
511 511 >>> # No names or emails fails
512 512 >>> names, emails = [], []
513 513 >>> _ismailmaplineinvalid(names, emails)
514 514 True
515 515 >>> # Only one email fails
516 516 >>> emails = [b'email@email.com']
517 517 >>> _ismailmaplineinvalid(names, emails)
518 518 True
519 519 >>> # One email and one name passes
520 520 >>> names = [b'Test Name']
521 521 >>> _ismailmaplineinvalid(names, emails)
522 522 False
523 523 >>> # No names but two emails passes
524 524 >>> names = []
525 525 >>> emails = [b'proper@email.com', b'commit@email.com']
526 526 >>> _ismailmaplineinvalid(names, emails)
527 527 False
528 528 """
529 529 return not emails or not names and len(emails) < 2
530 530
531 531
532 532 def parsemailmap(mailmapcontent):
533 533 """Parses data in the .mailmap format
534 534
535 535 >>> mmdata = b"\\n".join([
536 536 ... b'# Comment',
537 537 ... b'Name <commit1@email.xx>',
538 538 ... b'<name@email.xx> <commit2@email.xx>',
539 539 ... b'Name <proper@email.xx> <commit3@email.xx>',
540 540 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
541 541 ... ])
542 542 >>> mm = parsemailmap(mmdata)
543 543 >>> for key in sorted(mm.keys()):
544 544 ... print(key)
545 545 mailmapping(email='commit1@email.xx', name=None)
546 546 mailmapping(email='commit2@email.xx', name=None)
547 547 mailmapping(email='commit3@email.xx', name=None)
548 548 mailmapping(email='commit4@email.xx', name='Commit')
549 549 >>> for val in sorted(mm.values()):
550 550 ... print(val)
551 551 mailmapping(email='commit1@email.xx', name='Name')
552 552 mailmapping(email='name@email.xx', name=None)
553 553 mailmapping(email='proper@email.xx', name='Name')
554 554 mailmapping(email='proper@email.xx', name='Name')
555 555 """
556 556 mailmap = {}
557 557
558 558 if mailmapcontent is None:
559 559 return mailmap
560 560
561 561 for line in mailmapcontent.splitlines():
562 562
563 563 # Don't bother checking the line if it is a comment or
564 564 # is an improperly formed author field
565 565 if line.lstrip().startswith(b'#'):
566 566 continue
567 567
568 568 # names, emails hold the parsed emails and names for each line
569 569 # name_builder holds the words in a persons name
570 570 names, emails = [], []
571 571 namebuilder = []
572 572
573 573 for element in line.split():
574 574 if element.startswith(b'#'):
575 575 # If we reach a comment in the mailmap file, move on
576 576 break
577 577
578 578 elif element.startswith(b'<') and element.endswith(b'>'):
579 579 # We have found an email.
580 580 # Parse it, and finalize any names from earlier
581 581 emails.append(element[1:-1]) # Slice off the "<>"
582 582
583 583 if namebuilder:
584 584 names.append(b' '.join(namebuilder))
585 585 namebuilder = []
586 586
587 587 # Break if we have found a second email, any other
588 588 # data does not fit the spec for .mailmap
589 589 if len(emails) > 1:
590 590 break
591 591
592 592 else:
593 593 # We have found another word in the committers name
594 594 namebuilder.append(element)
595 595
596 596 # Check to see if we have parsed the line into a valid form
597 597 # We require at least one email, and either at least one
598 598 # name or a second email
599 599 if _ismailmaplineinvalid(names, emails):
600 600 continue
601 601
602 602 mailmapkey = mailmapping(
603 603 email=emails[-1],
604 604 name=names[-1] if len(names) == 2 else None,
605 605 )
606 606
607 607 mailmap[mailmapkey] = mailmapping(
608 608 email=emails[0],
609 609 name=names[0] if names else None,
610 610 )
611 611
612 612 return mailmap
613 613
614 614
615 615 def mapname(mailmap, author):
616 616 """Returns the author field according to the mailmap cache, or
617 617 the original author field.
618 618
619 619 >>> mmdata = b"\\n".join([
620 620 ... b'# Comment',
621 621 ... b'Name <commit1@email.xx>',
622 622 ... b'<name@email.xx> <commit2@email.xx>',
623 623 ... b'Name <proper@email.xx> <commit3@email.xx>',
624 624 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
625 625 ... ])
626 626 >>> m = parsemailmap(mmdata)
627 627 >>> mapname(m, b'Commit <commit1@email.xx>')
628 628 'Name <commit1@email.xx>'
629 629 >>> mapname(m, b'Name <commit2@email.xx>')
630 630 'Name <name@email.xx>'
631 631 >>> mapname(m, b'Commit <commit3@email.xx>')
632 632 'Name <proper@email.xx>'
633 633 >>> mapname(m, b'Commit <commit4@email.xx>')
634 634 'Name <proper@email.xx>'
635 635 >>> mapname(m, b'Unknown Name <unknown@email.com>')
636 636 'Unknown Name <unknown@email.com>'
637 637 """
638 638 # If the author field coming in isn't in the correct format,
639 639 # or the mailmap is empty just return the original author field
640 640 if not isauthorwellformed(author) or not mailmap:
641 641 return author
642 642
643 643 # Turn the user name into a mailmapping
644 644 commit = mailmapping(name=person(author), email=email(author))
645 645
646 646 try:
647 647 # Try and use both the commit email and name as the key
648 648 proper = mailmap[commit]
649 649
650 650 except KeyError:
651 651 # If the lookup fails, use just the email as the key instead
652 652 # We call this commit2 as not to erase original commit fields
653 653 commit2 = mailmapping(email=commit.email)
654 654 proper = mailmap.get(commit2, mailmapping(None, None))
655 655
656 656 # Return the author field with proper values filled in
657 657 return b'%s <%s>' % (
658 658 proper.name if proper.name else commit.name,
659 659 proper.email if proper.email else commit.email,
660 660 )
661 661
662 662
663 663 _correctauthorformat = remod.compile(br'^[^<]+\s<[^<>]+@[^<>]+>$')
664 664
665 665
666 666 def isauthorwellformed(author):
667 667 """Return True if the author field is well formed
668 668 (ie "Contributor Name <contrib@email.dom>")
669 669
670 670 >>> isauthorwellformed(b'Good Author <good@author.com>')
671 671 True
672 672 >>> isauthorwellformed(b'Author <good@author.com>')
673 673 True
674 674 >>> isauthorwellformed(b'Bad Author')
675 675 False
676 676 >>> isauthorwellformed(b'Bad Author <author@author.com')
677 677 False
678 678 >>> isauthorwellformed(b'Bad Author author@author.com')
679 679 False
680 680 >>> isauthorwellformed(b'<author@author.com>')
681 681 False
682 682 >>> isauthorwellformed(b'Bad Author <author>')
683 683 False
684 684 """
685 685 return _correctauthorformat.match(author) is not None
686 686
687 687
688 def firstline(text):
689 """Return the first line of the input"""
690 try:
691 return text.splitlines()[0]
692 except IndexError:
693 return b''
694
695
688 696 def ellipsis(text, maxlength=400):
689 697 """Trim string to at most maxlength (default: 400) columns in display."""
690 698 return encoding.trim(text, maxlength, ellipsis=b'...')
691 699
692 700
693 701 def escapestr(s):
694 702 if isinstance(s, memoryview):
695 703 s = bytes(s)
696 704 # call underlying function of s.encode('string_escape') directly for
697 705 # Python 3 compatibility
698 706 return codecs.escape_encode(s)[0] # pytype: disable=module-attr
699 707
700 708
701 709 def unescapestr(s):
702 710 return codecs.escape_decode(s)[0] # pytype: disable=module-attr
703 711
704 712
705 713 def forcebytestr(obj):
706 714 """Portably format an arbitrary object (e.g. exception) into a byte
707 715 string."""
708 716 try:
709 717 return pycompat.bytestr(obj)
710 718 except UnicodeEncodeError:
711 719 # non-ascii string, may be lossy
712 720 return pycompat.bytestr(encoding.strtolocal(str(obj)))
713 721
714 722
715 723 def uirepr(s):
716 724 # Avoid double backslash in Windows path repr()
717 725 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
718 726
719 727
720 728 # delay import of textwrap
721 729 def _MBTextWrapper(**kwargs):
722 730 class tw(textwrap.TextWrapper):
723 731 """
724 732 Extend TextWrapper for width-awareness.
725 733
726 734 Neither number of 'bytes' in any encoding nor 'characters' is
727 735 appropriate to calculate terminal columns for specified string.
728 736
729 737 Original TextWrapper implementation uses built-in 'len()' directly,
730 738 so overriding is needed to use width information of each characters.
731 739
732 740 In addition, characters classified into 'ambiguous' width are
733 741 treated as wide in East Asian area, but as narrow in other.
734 742
735 743 This requires use decision to determine width of such characters.
736 744 """
737 745
738 746 def _cutdown(self, ucstr, space_left):
739 747 l = 0
740 748 colwidth = encoding.ucolwidth
741 749 for i in pycompat.xrange(len(ucstr)):
742 750 l += colwidth(ucstr[i])
743 751 if space_left < l:
744 752 return (ucstr[:i], ucstr[i:])
745 753 return ucstr, b''
746 754
747 755 # overriding of base class
748 756 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
749 757 space_left = max(width - cur_len, 1)
750 758
751 759 if self.break_long_words:
752 760 cut, res = self._cutdown(reversed_chunks[-1], space_left)
753 761 cur_line.append(cut)
754 762 reversed_chunks[-1] = res
755 763 elif not cur_line:
756 764 cur_line.append(reversed_chunks.pop())
757 765
758 766 # this overriding code is imported from TextWrapper of Python 2.6
759 767 # to calculate columns of string by 'encoding.ucolwidth()'
760 768 def _wrap_chunks(self, chunks):
761 769 colwidth = encoding.ucolwidth
762 770
763 771 lines = []
764 772 if self.width <= 0:
765 773 raise ValueError(b"invalid width %r (must be > 0)" % self.width)
766 774
767 775 # Arrange in reverse order so items can be efficiently popped
768 776 # from a stack of chucks.
769 777 chunks.reverse()
770 778
771 779 while chunks:
772 780
773 781 # Start the list of chunks that will make up the current line.
774 782 # cur_len is just the length of all the chunks in cur_line.
775 783 cur_line = []
776 784 cur_len = 0
777 785
778 786 # Figure out which static string will prefix this line.
779 787 if lines:
780 788 indent = self.subsequent_indent
781 789 else:
782 790 indent = self.initial_indent
783 791
784 792 # Maximum width for this line.
785 793 width = self.width - len(indent)
786 794
787 795 # First chunk on line is whitespace -- drop it, unless this
788 796 # is the very beginning of the text (i.e. no lines started yet).
789 797 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
790 798 del chunks[-1]
791 799
792 800 while chunks:
793 801 l = colwidth(chunks[-1])
794 802
795 803 # Can at least squeeze this chunk onto the current line.
796 804 if cur_len + l <= width:
797 805 cur_line.append(chunks.pop())
798 806 cur_len += l
799 807
800 808 # Nope, this line is full.
801 809 else:
802 810 break
803 811
804 812 # The current line is full, and the next chunk is too big to
805 813 # fit on *any* line (not just this one).
806 814 if chunks and colwidth(chunks[-1]) > width:
807 815 self._handle_long_word(chunks, cur_line, cur_len, width)
808 816
809 817 # If the last chunk on this line is all whitespace, drop it.
810 818 if (
811 819 self.drop_whitespace
812 820 and cur_line
813 821 and cur_line[-1].strip() == r''
814 822 ):
815 823 del cur_line[-1]
816 824
817 825 # Convert current line back to a string and store it in list
818 826 # of all lines (return value).
819 827 if cur_line:
820 828 lines.append(indent + ''.join(cur_line))
821 829
822 830 return lines
823 831
824 832 global _MBTextWrapper
825 833 _MBTextWrapper = tw
826 834 return tw(**kwargs)
827 835
828 836
829 837 def wrap(line, width, initindent=b'', hangindent=b''):
830 838 maxindent = max(len(hangindent), len(initindent))
831 839 if width <= maxindent:
832 840 # adjust for weird terminal size
833 841 width = max(78, maxindent + 1)
834 842 line = line.decode(
835 843 pycompat.sysstr(encoding.encoding),
836 844 pycompat.sysstr(encoding.encodingmode),
837 845 )
838 846 initindent = initindent.decode(
839 847 pycompat.sysstr(encoding.encoding),
840 848 pycompat.sysstr(encoding.encodingmode),
841 849 )
842 850 hangindent = hangindent.decode(
843 851 pycompat.sysstr(encoding.encoding),
844 852 pycompat.sysstr(encoding.encodingmode),
845 853 )
846 854 wrapper = _MBTextWrapper(
847 855 width=width, initial_indent=initindent, subsequent_indent=hangindent
848 856 )
849 857 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
850 858
851 859
852 860 _booleans = {
853 861 b'1': True,
854 862 b'yes': True,
855 863 b'true': True,
856 864 b'on': True,
857 865 b'always': True,
858 866 b'0': False,
859 867 b'no': False,
860 868 b'false': False,
861 869 b'off': False,
862 870 b'never': False,
863 871 }
864 872
865 873
866 874 def parsebool(s):
867 875 """Parse s into a boolean.
868 876
869 877 If s is not a valid boolean, returns None.
870 878 """
871 879 return _booleans.get(s.lower(), None)
872 880
873 881
874 882 def parselist(value):
875 883 """parse a configuration value as a list of comma/space separated strings
876 884
877 885 >>> parselist(b'this,is "a small" ,test')
878 886 ['this', 'is', 'a small', 'test']
879 887 """
880 888
881 889 def _parse_plain(parts, s, offset):
882 890 whitespace = False
883 891 while offset < len(s) and (
884 892 s[offset : offset + 1].isspace() or s[offset : offset + 1] == b','
885 893 ):
886 894 whitespace = True
887 895 offset += 1
888 896 if offset >= len(s):
889 897 return None, parts, offset
890 898 if whitespace:
891 899 parts.append(b'')
892 900 if s[offset : offset + 1] == b'"' and not parts[-1]:
893 901 return _parse_quote, parts, offset + 1
894 902 elif s[offset : offset + 1] == b'"' and parts[-1][-1:] == b'\\':
895 903 parts[-1] = parts[-1][:-1] + s[offset : offset + 1]
896 904 return _parse_plain, parts, offset + 1
897 905 parts[-1] += s[offset : offset + 1]
898 906 return _parse_plain, parts, offset + 1
899 907
900 908 def _parse_quote(parts, s, offset):
901 909 if offset < len(s) and s[offset : offset + 1] == b'"': # ""
902 910 parts.append(b'')
903 911 offset += 1
904 912 while offset < len(s) and (
905 913 s[offset : offset + 1].isspace()
906 914 or s[offset : offset + 1] == b','
907 915 ):
908 916 offset += 1
909 917 return _parse_plain, parts, offset
910 918
911 919 while offset < len(s) and s[offset : offset + 1] != b'"':
912 920 if (
913 921 s[offset : offset + 1] == b'\\'
914 922 and offset + 1 < len(s)
915 923 and s[offset + 1 : offset + 2] == b'"'
916 924 ):
917 925 offset += 1
918 926 parts[-1] += b'"'
919 927 else:
920 928 parts[-1] += s[offset : offset + 1]
921 929 offset += 1
922 930
923 931 if offset >= len(s):
924 932 real_parts = _configlist(parts[-1])
925 933 if not real_parts:
926 934 parts[-1] = b'"'
927 935 else:
928 936 real_parts[0] = b'"' + real_parts[0]
929 937 parts = parts[:-1]
930 938 parts.extend(real_parts)
931 939 return None, parts, offset
932 940
933 941 offset += 1
934 942 while offset < len(s) and s[offset : offset + 1] in [b' ', b',']:
935 943 offset += 1
936 944
937 945 if offset < len(s):
938 946 if offset + 1 == len(s) and s[offset : offset + 1] == b'"':
939 947 parts[-1] += b'"'
940 948 offset += 1
941 949 else:
942 950 parts.append(b'')
943 951 else:
944 952 return None, parts, offset
945 953
946 954 return _parse_plain, parts, offset
947 955
948 956 def _configlist(s):
949 957 s = s.rstrip(b' ,')
950 958 if not s:
951 959 return []
952 960 parser, parts, offset = _parse_plain, [b''], 0
953 961 while parser:
954 962 parser, parts, offset = parser(parts, s, offset)
955 963 return parts
956 964
957 965 if value is not None and isinstance(value, bytes):
958 966 result = _configlist(value.lstrip(b' ,\n'))
959 967 else:
960 968 result = value
961 969 return result or []
962 970
963 971
964 972 def evalpythonliteral(s):
965 973 """Evaluate a string containing a Python literal expression"""
966 974 # We could backport our tokenizer hack to rewrite '' to u'' if we want
967 975 return ast.literal_eval(s.decode('latin1'))
General Comments 0
You need to be logged in to leave comments. Login now