stringutil.py
998 lines
| 29.5 KiB
| text/x-python
|
PythonLexer
|
r37101 | # stringutil.py - utility for generic string formatting, parsing, etc. | ||
# | ||||
# Copyright 2005 K. Thananchayan <thananck@yahoo.com> | ||||
|
r47575 | # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com> | ||
|
r37101 | # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com> | ||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
|
r37494 | import ast | ||
|
r37101 | import codecs | ||
import re as remod | ||||
import textwrap | ||||
|
r39332 | import types | ||
|
r37101 | |||
|
r50470 | from typing import ( | ||
Optional, | ||||
overload, | ||||
) | ||||
|
r37101 | from ..i18n import _ | ||
|
r37227 | from ..thirdparty import attr | ||
|
r37101 | |||
from .. import ( | ||||
encoding, | ||||
error, | ||||
pycompat, | ||||
) | ||||
|
r38493 | # regex special chars pulled from https://bugs.python.org/issue29995 | ||
# which was part of Python 3.7. | ||||
|
r38496 | _respecial = pycompat.bytestr(b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f') | ||
|
r38493 | _regexescapemap = {ord(i): (b'\\' + i).decode('latin1') for i in _respecial} | ||
|
r40720 | regexbytesescapemap = {i: (b'\\' + i) for i in _respecial} | ||
|
r38493 | |||
|
r43346 | |||
|
r50470 | @overload | ||
def reescape(pat: bytes) -> bytes: | ||||
... | ||||
@overload | ||||
def reescape(pat: str) -> str: | ||||
... | ||||
|
r38493 | def reescape(pat): | ||
"""Drop-in replacement for re.escape.""" | ||||
# NOTE: it is intentional that this works on unicodes and not | ||||
# bytes, as it's only possible to do the escaping with | ||||
# unicode.translate, not bytes.translate. Sigh. | ||||
wantuni = True | ||||
if isinstance(pat, bytes): | ||||
wantuni = False | ||||
pat = pat.decode('latin1') | ||||
pat = pat.translate(_regexescapemap) | ||||
if wantuni: | ||||
return pat | ||||
return pat.encode('latin1') | ||||
|
r43346 | |||
|
r50470 | def pprint(o, bprefix: bool = False, indent: int = 0, level: int = 0) -> bytes: | ||
|
r37316 | """Pretty print an object.""" | ||
|
r40312 | return b''.join(pprintgen(o, bprefix=bprefix, indent=indent, level=level)) | ||
|
r39414 | |||
|
r43346 | |||
|
r50470 | def pprintgen(o, bprefix: bool = False, indent: int = 0, level: int = 0): | ||
|
r39414 | """Pretty print an object to a generator of atoms. | ||
|
r39389 | |||
|
r39414 | ``bprefix`` is a flag influencing whether bytestrings are preferred with | ||
a ``b''`` prefix. | ||||
``indent`` controls whether collections and nested data structures | ||||
span multiple lines via the indentation amount in spaces. By default, | ||||
no newlines are emitted. | ||||
|
r40312 | |||
``level`` specifies the initial indent level. Used if ``indent > 0``. | ||||
|
r39414 | """ | ||
|
r39389 | |||
|
r37637 | if isinstance(o, bytes): | ||
|
r37768 | if bprefix: | ||
|
r43347 | yield b"b'%s'" % escapestr(o) | ||
|
r39389 | else: | ||
|
r43347 | yield b"'%s'" % escapestr(o) | ||
|
r37637 | elif isinstance(o, bytearray): | ||
# codecs.escape_encode() can't handle bytearray, so escapestr fails | ||||
# without coercion. | ||||
|
r43347 | yield b"bytearray['%s']" % escapestr(bytes(o)) | ||
|
r37316 | elif isinstance(o, list): | ||
|
r39390 | if not o: | ||
|
r43347 | yield b'[]' | ||
|
r39390 | return | ||
|
r43347 | yield b'[' | ||
|
r39390 | |||
|
r39414 | if indent: | ||
|
r40312 | level += 1 | ||
|
r43347 | yield b'\n' | ||
yield b' ' * (level * indent) | ||||
|
r39414 | |||
|
r39390 | for i, a in enumerate(o): | ||
|
r43346 | for chunk in pprintgen( | ||
a, bprefix=bprefix, indent=indent, level=level | ||||
): | ||||
|
r39390 | yield chunk | ||
if i + 1 < len(o): | ||||
|
r39414 | if indent: | ||
|
r43347 | yield b',\n' | ||
yield b' ' * (level * indent) | ||||
|
r39414 | else: | ||
|
r43347 | yield b', ' | ||
|
r39414 | |||
if indent: | ||||
|
r40312 | level -= 1 | ||
|
r43347 | yield b'\n' | ||
yield b' ' * (level * indent) | ||||
|
r39390 | |||
|
r43347 | yield b']' | ||
|
r37316 | elif isinstance(o, dict): | ||
|
r39390 | if not o: | ||
|
r43347 | yield b'{}' | ||
|
r39390 | return | ||
|
r43347 | yield b'{' | ||
|
r39390 | |||
|
r39414 | if indent: | ||
|
r40312 | level += 1 | ||
|
r43347 | yield b'\n' | ||
yield b' ' * (level * indent) | ||||
|
r39414 | |||
|
r39390 | for i, (k, v) in enumerate(sorted(o.items())): | ||
|
r43346 | for chunk in pprintgen( | ||
k, bprefix=bprefix, indent=indent, level=level | ||||
): | ||||
|
r39390 | yield chunk | ||
|
r43347 | yield b': ' | ||
|
r39390 | |||
|
r43346 | for chunk in pprintgen( | ||
v, bprefix=bprefix, indent=indent, level=level | ||||
): | ||||
|
r39390 | yield chunk | ||
if i + 1 < len(o): | ||||
|
r39414 | if indent: | ||
|
r43347 | yield b',\n' | ||
yield b' ' * (level * indent) | ||||
|
r39414 | else: | ||
|
r43347 | yield b', ' | ||
|
r39414 | |||
if indent: | ||||
|
r40312 | level -= 1 | ||
|
r43347 | yield b'\n' | ||
yield b' ' * (level * indent) | ||||
|
r39390 | |||
|
r43347 | yield b'}' | ||
|
r39086 | elif isinstance(o, set): | ||
|
r39390 | if not o: | ||
|
r43347 | yield b'set([])' | ||
|
r39390 | return | ||
|
r43347 | yield b'set([' | ||
|
r39390 | |||
|
r39414 | if indent: | ||
|
r40312 | level += 1 | ||
|
r43347 | yield b'\n' | ||
yield b' ' * (level * indent) | ||||
|
r39414 | |||
|
r39390 | for i, k in enumerate(sorted(o)): | ||
|
r43346 | for chunk in pprintgen( | ||
k, bprefix=bprefix, indent=indent, level=level | ||||
): | ||||
|
r39390 | yield chunk | ||
if i + 1 < len(o): | ||||
|
r39414 | if indent: | ||
|
r43347 | yield b',\n' | ||
yield b' ' * (level * indent) | ||||
|
r39414 | else: | ||
|
r43347 | yield b', ' | ||
|
r39414 | |||
if indent: | ||||
|
r40312 | level -= 1 | ||
|
r43347 | yield b'\n' | ||
yield b' ' * (level * indent) | ||||
|
r39390 | |||
|
r43347 | yield b'])' | ||
|
r37951 | elif isinstance(o, tuple): | ||
|
r39390 | if not o: | ||
|
r43347 | yield b'()' | ||
|
r39390 | return | ||
|
r43347 | yield b'(' | ||
|
r39390 | |||
|
r39414 | if indent: | ||
|
r40312 | level += 1 | ||
|
r43347 | yield b'\n' | ||
yield b' ' * (level * indent) | ||||
|
r39414 | |||
|
r39390 | for i, a in enumerate(o): | ||
|
r43346 | for chunk in pprintgen( | ||
a, bprefix=bprefix, indent=indent, level=level | ||||
): | ||||
|
r39390 | yield chunk | ||
if i + 1 < len(o): | ||||
|
r39414 | if indent: | ||
|
r43347 | yield b',\n' | ||
yield b' ' * (level * indent) | ||||
|
r39414 | else: | ||
|
r43347 | yield b', ' | ||
|
r39414 | |||
if indent: | ||||
|
r40312 | level -= 1 | ||
|
r43347 | yield b'\n' | ||
yield b' ' * (level * indent) | ||||
|
r39390 | |||
|
r43347 | yield b')' | ||
|
r39332 | elif isinstance(o, types.GeneratorType): | ||
|
r39390 | # Special case of empty generator. | ||
try: | ||||
nextitem = next(o) | ||||
except StopIteration: | ||||
|
r43347 | yield b'gen[]' | ||
|
r39390 | return | ||
|
r43347 | yield b'gen[' | ||
|
r39390 | |||
|
r39414 | if indent: | ||
|
r40312 | level += 1 | ||
|
r43347 | yield b'\n' | ||
yield b' ' * (level * indent) | ||||
|
r39414 | |||
|
r39390 | last = False | ||
while not last: | ||||
current = nextitem | ||||
try: | ||||
nextitem = next(o) | ||||
except StopIteration: | ||||
last = True | ||||
|
r43346 | for chunk in pprintgen( | ||
current, bprefix=bprefix, indent=indent, level=level | ||||
): | ||||
|
r39390 | yield chunk | ||
if not last: | ||||
|
r39414 | if indent: | ||
|
r43347 | yield b',\n' | ||
yield b' ' * (level * indent) | ||||
|
r39414 | else: | ||
|
r43347 | yield b', ' | ||
|
r39414 | |||
if indent: | ||||
|
r40312 | level -= 1 | ||
|
r43347 | yield b'\n' | ||
yield b' ' * (level * indent) | ||||
|
r39390 | |||
|
r43347 | yield b']' | ||
|
r37316 | else: | ||
|
r39389 | yield pycompat.byterepr(o) | ||
|
r37316 | |||
|
r43346 | |||
|
r50470 | def prettyrepr(o) -> bytes: | ||
|
r38280 | """Pretty print a representation of a possibly-nested object""" | ||
lines = [] | ||||
rs = pycompat.byterepr(o) | ||||
|
r38283 | p0 = p1 = 0 | ||
while p0 < len(rs): | ||||
# '... field=<type ... field=<type ...' | ||||
# ~~~~~~~~~~~~~~~~ | ||||
# p0 p1 q0 q1 | ||||
q0 = -1 | ||||
|
r43347 | q1 = rs.find(b'<', p1 + 1) | ||
|
r38283 | if q1 < 0: | ||
q1 = len(rs) | ||||
|
r49310 | # pytype: disable=wrong-arg-count | ||
# TODO: figure out why pytype doesn't recognize the optional start | ||||
# arg | ||||
|
r43347 | elif q1 > p1 + 1 and rs.startswith(b'=', q1 - 1): | ||
|
r49310 | # pytype: enable=wrong-arg-count | ||
|
r38283 | # backtrack for ' field=<' | ||
|
r43347 | q0 = rs.rfind(b' ', p1 + 1, q1 - 1) | ||
|
r38283 | if q0 < 0: | ||
q0 = q1 | ||||
else: | ||||
q0 += 1 # skip ' ' | ||||
|
r43347 | l = rs.count(b'<', 0, p0) - rs.count(b'>', 0, p0) | ||
|
r38280 | assert l >= 0 | ||
|
r38283 | lines.append((l, rs[p0:q0].rstrip())) | ||
p0, p1 = q0, q1 | ||||
|
r43347 | return b'\n'.join(b' ' * l + s for l, s in lines) | ||
|
r38280 | |||
|
r43346 | |||
|
r50470 | def buildrepr(r) -> bytes: | ||
|
r38595 | """Format an optional printable representation from unexpanded bits | ||
======== ================================= | ||||
type(r) example | ||||
======== ================================= | ||||
tuple ('<not %r>', other) | ||||
bytes '<branch closed>' | ||||
callable lambda: '<branch %r>' % sorted(b) | ||||
object other | ||||
======== ================================= | ||||
""" | ||||
if r is None: | ||||
|
r43347 | return b'' | ||
|
r38595 | elif isinstance(r, tuple): | ||
return r[0] % pycompat.rapply(pycompat.maybebytestr, r[1:]) | ||||
elif isinstance(r, bytes): | ||||
return r | ||||
elif callable(r): | ||||
return r() | ||||
else: | ||||
|
r39087 | return pprint(r) | ||
|
r38595 | |||
|
r43346 | |||
|
r50470 | def binary(s: bytes) -> bool: | ||
|
r37101 | """return true if a string is binary data""" | ||
|
r43347 | return bool(s and b'\0' in s) | ||
|
r37101 | |||
|
r43346 | |||
|
r50470 | def _splitpattern(pattern: bytes): | ||
|
r46314 | if pattern.startswith(b're:'): | ||
return b're', pattern[3:] | ||||
elif pattern.startswith(b'literal:'): | ||||
return b'literal', pattern[8:] | ||||
return b'literal', pattern | ||||
|
r50470 | def stringmatcher(pattern: bytes, casesensitive: bool = True): | ||
|
r37101 | """ | ||
accepts a string, possibly starting with 're:' or 'literal:' prefix. | ||||
returns the matcher name, pattern, and matcher function. | ||||
missing or unknown prefixes are treated as literal matches. | ||||
helper for tests: | ||||
>>> def test(pattern, *tests): | ||||
... kind, pattern, matcher = stringmatcher(pattern) | ||||
... return (kind, pattern, [bool(matcher(t)) for t in tests]) | ||||
>>> def itest(pattern, *tests): | ||||
... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False) | ||||
... return (kind, pattern, [bool(matcher(t)) for t in tests]) | ||||
exact matching (no prefix): | ||||
>>> test(b'abcdefg', b'abc', b'def', b'abcdefg') | ||||
('literal', 'abcdefg', [False, False, True]) | ||||
regex matching ('re:' prefix) | ||||
>>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar') | ||||
('re', 'a.+b', [False, False, True]) | ||||
force exact matches ('literal:' prefix) | ||||
>>> test(b'literal:re:foobar', b'foobar', b're:foobar') | ||||
('literal', 're:foobar', [False, True]) | ||||
unknown prefixes are ignored and treated as literals | ||||
>>> test(b'foo:bar', b'foo', b'bar', b'foo:bar') | ||||
('literal', 'foo:bar', [False, False, True]) | ||||
case insensitive regex matches | ||||
>>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar') | ||||
('re', 'A.+b', [False, False, True]) | ||||
case insensitive literal matches | ||||
>>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg') | ||||
('literal', 'ABCDEFG', [False, False, True]) | ||||
""" | ||||
|
r46314 | kind, pattern = _splitpattern(pattern) | ||
if kind == b're': | ||||
|
r37101 | try: | ||
flags = 0 | ||||
if not casesensitive: | ||||
flags = remod.I | ||||
regex = remod.compile(pattern, flags) | ||||
except remod.error as e: | ||||
|
r46315 | raise error.ParseError( | ||
_(b'invalid regular expression: %s') % forcebytestr(e) | ||||
) | ||||
|
r46314 | return kind, pattern, regex.search | ||
elif kind == b'literal': | ||||
if casesensitive: | ||||
match = pattern.__eq__ | ||||
else: | ||||
ipat = encoding.lower(pattern) | ||||
match = lambda s: ipat == encoding.lower(s) | ||||
return kind, pattern, match | ||||
|
r37101 | |||
|
r46314 | raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind) | ||
|
r37101 | |||
|
r43346 | |||
|
r50470 | def substringregexp(pattern: bytes, flags: int = 0): | ||
|
r46316 | """Build a regexp object from a string pattern possibly starting with | ||
're:' or 'literal:' prefix. | ||||
helper for tests: | ||||
>>> def test(pattern, *tests): | ||||
... regexp = substringregexp(pattern) | ||||
... return [bool(regexp.search(t)) for t in tests] | ||||
>>> def itest(pattern, *tests): | ||||
... regexp = substringregexp(pattern, remod.I) | ||||
... return [bool(regexp.search(t)) for t in tests] | ||||
substring matching (no prefix): | ||||
>>> test(b'bcde', b'abc', b'def', b'abcdefg') | ||||
[False, False, True] | ||||
substring pattern should be escaped: | ||||
>>> substringregexp(b'.bc').pattern | ||||
'\\\\.bc' | ||||
>>> test(b'.bc', b'abc', b'def', b'abcdefg') | ||||
[False, False, False] | ||||
regex matching ('re:' prefix) | ||||
>>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar') | ||||
[False, False, True] | ||||
force substring matches ('literal:' prefix) | ||||
>>> test(b'literal:re:foobar', b'foobar', b're:foobar') | ||||
[False, True] | ||||
case insensitive literal matches | ||||
>>> itest(b'BCDE', b'abc', b'def', b'abcdefg') | ||||
[False, False, True] | ||||
case insensitive regex matches | ||||
>>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar') | ||||
[False, False, True] | ||||
""" | ||||
kind, pattern = _splitpattern(pattern) | ||||
if kind == b're': | ||||
try: | ||||
return remod.compile(pattern, flags) | ||||
except remod.error as e: | ||||
raise error.ParseError( | ||||
_(b'invalid regular expression: %s') % forcebytestr(e) | ||||
) | ||||
elif kind == b'literal': | ||||
return remod.compile(remod.escape(pattern), flags) | ||||
raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind) | ||||
|
r50470 | def shortuser(user: bytes) -> bytes: | ||
|
r37101 | """Return a short representation of a user name or email address.""" | ||
|
r43347 | f = user.find(b'@') | ||
|
r37101 | if f >= 0: | ||
user = user[:f] | ||||
|
r43347 | f = user.find(b'<') | ||
|
r37101 | if f >= 0: | ||
|
r43346 | user = user[f + 1 :] | ||
|
r43347 | f = user.find(b' ') | ||
|
r37101 | if f >= 0: | ||
user = user[:f] | ||||
|
r43347 | f = user.find(b'.') | ||
|
r37101 | if f >= 0: | ||
user = user[:f] | ||||
return user | ||||
|
r43346 | |||
|
r50470 | def emailuser(user: bytes) -> bytes: | ||
|
r37101 | """Return the user portion of an email address.""" | ||
|
r43347 | f = user.find(b'@') | ||
|
r37101 | if f >= 0: | ||
user = user[:f] | ||||
|
r43347 | f = user.find(b'<') | ||
|
r37101 | if f >= 0: | ||
|
r43346 | user = user[f + 1 :] | ||
|
r37101 | return user | ||
|
r43346 | |||
|
r50470 | def email(author: bytes) -> bytes: | ||
|
r37101 | '''get email of author.''' | ||
|
r43347 | r = author.find(b'>') | ||
|
r37101 | if r == -1: | ||
r = None | ||||
|
r43347 | return author[author.find(b'<') + 1 : r] | ||
|
r43346 | |||
|
r37101 | |||
|
r50470 | def person(author: bytes) -> bytes: | ||
|
r37173 | """Returns the name before an email address, | ||
interpreting it as per RFC 5322 | ||||
>>> person(b'foo@bar') | ||||
'foo' | ||||
>>> person(b'Foo Bar <foo@bar>') | ||||
'Foo Bar' | ||||
>>> person(b'"Foo Bar" <foo@bar>') | ||||
'Foo Bar' | ||||
>>> person(b'"Foo \"buz\" Bar" <foo@bar>') | ||||
'Foo "buz" Bar' | ||||
>>> # The following are invalid, but do exist in real-life | ||||
... | ||||
>>> person(b'Foo "buz" Bar <foo@bar>') | ||||
'Foo "buz" Bar' | ||||
>>> person(b'"Foo Bar <foo@bar>') | ||||
'Foo Bar' | ||||
""" | ||||
|
r43347 | if b'@' not in author: | ||
|
r37173 | return author | ||
|
r43347 | f = author.find(b'<') | ||
|
r37173 | if f != -1: | ||
|
r43347 | return author[:f].strip(b' "').replace(b'\\"', b'"') | ||
f = author.find(b'@') | ||||
return author[:f].replace(b'.', b' ') | ||||
|
r37173 | |||
|
r43346 | |||
|
r37227 | @attr.s(hash=True) | ||
|
r49801 | class mailmapping: | ||
|
r46554 | """Represents a username/email key or value in | ||
a mailmap file""" | ||||
|
r43346 | |||
|
r37227 | email = attr.ib() | ||
name = attr.ib(default=None) | ||||
|
r43346 | |||
|
r37263 | def _ismailmaplineinvalid(names, emails): | ||
|
r46554 | """Returns True if the parsed names and emails | ||
|
r37263 | in a mailmap entry are invalid. | ||
>>> # No names or emails fails | ||||
>>> names, emails = [], [] | ||||
>>> _ismailmaplineinvalid(names, emails) | ||||
True | ||||
>>> # Only one email fails | ||||
>>> emails = [b'email@email.com'] | ||||
>>> _ismailmaplineinvalid(names, emails) | ||||
True | ||||
>>> # One email and one name passes | ||||
>>> names = [b'Test Name'] | ||||
>>> _ismailmaplineinvalid(names, emails) | ||||
False | ||||
>>> # No names but two emails passes | ||||
>>> names = [] | ||||
>>> emails = [b'proper@email.com', b'commit@email.com'] | ||||
>>> _ismailmaplineinvalid(names, emails) | ||||
False | ||||
|
r46554 | """ | ||
|
r37263 | return not emails or not names and len(emails) < 2 | ||
|
r43346 | |||
|
r37227 | def parsemailmap(mailmapcontent): | ||
"""Parses data in the .mailmap format | ||||
>>> mmdata = b"\\n".join([ | ||||
... b'# Comment', | ||||
... b'Name <commit1@email.xx>', | ||||
... b'<name@email.xx> <commit2@email.xx>', | ||||
... b'Name <proper@email.xx> <commit3@email.xx>', | ||||
... b'Name <proper@email.xx> Commit <commit4@email.xx>', | ||||
... ]) | ||||
>>> mm = parsemailmap(mmdata) | ||||
>>> for key in sorted(mm.keys()): | ||||
... print(key) | ||||
mailmapping(email='commit1@email.xx', name=None) | ||||
mailmapping(email='commit2@email.xx', name=None) | ||||
mailmapping(email='commit3@email.xx', name=None) | ||||
mailmapping(email='commit4@email.xx', name='Commit') | ||||
>>> for val in sorted(mm.values()): | ||||
... print(val) | ||||
mailmapping(email='commit1@email.xx', name='Name') | ||||
mailmapping(email='name@email.xx', name=None) | ||||
mailmapping(email='proper@email.xx', name='Name') | ||||
mailmapping(email='proper@email.xx', name='Name') | ||||
""" | ||||
mailmap = {} | ||||
if mailmapcontent is None: | ||||
return mailmap | ||||
for line in mailmapcontent.splitlines(): | ||||
# Don't bother checking the line if it is a comment or | ||||
# is an improperly formed author field | ||||
|
r43347 | if line.lstrip().startswith(b'#'): | ||
|
r37227 | continue | ||
|
r37262 | # names, emails hold the parsed emails and names for each line | ||
|
r37227 | # name_builder holds the words in a persons name | ||
|
r37262 | names, emails = [], [] | ||
|
r37227 | namebuilder = [] | ||
for element in line.split(): | ||||
|
r43347 | if element.startswith(b'#'): | ||
|
r37227 | # If we reach a comment in the mailmap file, move on | ||
break | ||||
|
r43347 | elif element.startswith(b'<') and element.endswith(b'>'): | ||
|
r37227 | # We have found an email. | ||
# Parse it, and finalize any names from earlier | ||||
|
r37262 | emails.append(element[1:-1]) # Slice off the "<>" | ||
|
r37227 | |||
if namebuilder: | ||||
|
r43347 | names.append(b' '.join(namebuilder)) | ||
|
r37227 | namebuilder = [] | ||
# Break if we have found a second email, any other | ||||
# data does not fit the spec for .mailmap | ||||
|
r37262 | if len(emails) > 1: | ||
|
r37227 | break | ||
else: | ||||
# We have found another word in the committers name | ||||
namebuilder.append(element) | ||||
|
r37263 | # Check to see if we have parsed the line into a valid form | ||
# We require at least one email, and either at least one | ||||
# name or a second email | ||||
if _ismailmaplineinvalid(names, emails): | ||||
continue | ||||
|
r37227 | mailmapkey = mailmapping( | ||
|
r46554 | email=emails[-1], | ||
name=names[-1] if len(names) == 2 else None, | ||||
|
r37227 | ) | ||
mailmap[mailmapkey] = mailmapping( | ||||
|
r46554 | email=emails[0], | ||
name=names[0] if names else None, | ||||
|
r37227 | ) | ||
return mailmap | ||||
|
r43346 | |||
|
r50470 | def mapname(mailmap, author: bytes) -> bytes: | ||
|
r37227 | """Returns the author field according to the mailmap cache, or | ||
the original author field. | ||||
>>> mmdata = b"\\n".join([ | ||||
... b'# Comment', | ||||
... b'Name <commit1@email.xx>', | ||||
... b'<name@email.xx> <commit2@email.xx>', | ||||
... b'Name <proper@email.xx> <commit3@email.xx>', | ||||
... b'Name <proper@email.xx> Commit <commit4@email.xx>', | ||||
... ]) | ||||
>>> m = parsemailmap(mmdata) | ||||
>>> mapname(m, b'Commit <commit1@email.xx>') | ||||
'Name <commit1@email.xx>' | ||||
>>> mapname(m, b'Name <commit2@email.xx>') | ||||
'Name <name@email.xx>' | ||||
>>> mapname(m, b'Commit <commit3@email.xx>') | ||||
'Name <proper@email.xx>' | ||||
>>> mapname(m, b'Commit <commit4@email.xx>') | ||||
'Name <proper@email.xx>' | ||||
>>> mapname(m, b'Unknown Name <unknown@email.com>') | ||||
'Unknown Name <unknown@email.com>' | ||||
""" | ||||
# If the author field coming in isn't in the correct format, | ||||
# or the mailmap is empty just return the original author field | ||||
if not isauthorwellformed(author) or not mailmap: | ||||
return author | ||||
|
r37264 | # Turn the user name into a mailmapping | ||
|
r37227 | commit = mailmapping(name=person(author), email=email(author)) | ||
try: | ||||
# Try and use both the commit email and name as the key | ||||
proper = mailmap[commit] | ||||
except KeyError: | ||||
# If the lookup fails, use just the email as the key instead | ||||
# We call this commit2 as not to erase original commit fields | ||||
commit2 = mailmapping(email=commit.email) | ||||
proper = mailmap.get(commit2, mailmapping(None, None)) | ||||
# Return the author field with proper values filled in | ||||
|
r43347 | return b'%s <%s>' % ( | ||
|
r37227 | proper.name if proper.name else commit.name, | ||
proper.email if proper.email else commit.email, | ||||
) | ||||
|
r43346 | |||
|
r44474 | _correctauthorformat = remod.compile(br'^[^<]+\s<[^<>]+@[^<>]+>$') | ||
|
r37172 | |||
|
r43346 | |||
|
r50470 | def isauthorwellformed(author: bytes) -> bool: | ||
|
r46554 | """Return True if the author field is well formed | ||
|
r37172 | (ie "Contributor Name <contrib@email.dom>") | ||
>>> isauthorwellformed(b'Good Author <good@author.com>') | ||||
True | ||||
>>> isauthorwellformed(b'Author <good@author.com>') | ||||
True | ||||
>>> isauthorwellformed(b'Bad Author') | ||||
False | ||||
>>> isauthorwellformed(b'Bad Author <author@author.com') | ||||
False | ||||
>>> isauthorwellformed(b'Bad Author author@author.com') | ||||
False | ||||
>>> isauthorwellformed(b'<author@author.com>') | ||||
False | ||||
>>> isauthorwellformed(b'Bad Author <author>') | ||||
False | ||||
|
r46554 | """ | ||
|
r37172 | return _correctauthorformat.match(author) is not None | ||
|
r43346 | |||
|
r50470 | def firstline(text: bytes) -> bytes: | ||
|
r49885 | """Return the first line of the input""" | ||
|
r49894 | # Try to avoid running splitlines() on the whole string | ||
i = text.find(b'\n') | ||||
if i != -1: | ||||
text = text[:i] | ||||
|
r49885 | try: | ||
return text.splitlines()[0] | ||||
except IndexError: | ||||
return b'' | ||||
|
r50470 | def ellipsis(text: bytes, maxlength: int = 400) -> bytes: | ||
|
r37101 | """Trim string to at most maxlength (default: 400) columns in display.""" | ||
|
r43347 | return encoding.trim(text, maxlength, ellipsis=b'...') | ||
|
r37101 | |||
|
r43346 | |||
|
r50470 | def escapestr(s: bytes) -> bytes: | ||
# "bytes" is also a typing shortcut for bytes, bytearray, and memoryview | ||||
|
r39098 | if isinstance(s, memoryview): | ||
s = bytes(s) | ||||
|
r37101 | # call underlying function of s.encode('string_escape') directly for | ||
# Python 3 compatibility | ||||
|
r49310 | return codecs.escape_encode(s)[0] # pytype: disable=module-attr | ||
|
r37101 | |||
|
r43346 | |||
|
r50470 | def unescapestr(s: bytes) -> bytes: | ||
|
r49310 | return codecs.escape_decode(s)[0] # pytype: disable=module-attr | ||
|
r37101 | |||
|
r43346 | |||
|
r37101 | def forcebytestr(obj): | ||
"""Portably format an arbitrary object (e.g. exception) into a byte | ||||
string.""" | ||||
try: | ||||
return pycompat.bytestr(obj) | ||||
except UnicodeEncodeError: | ||||
# non-ascii string, may be lossy | ||||
return pycompat.bytestr(encoding.strtolocal(str(obj))) | ||||
|
r43346 | |||
|
r50470 | def uirepr(s: bytes) -> bytes: | ||
|
r37101 | # Avoid double backslash in Windows path repr() | ||
return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\') | ||||
|
r43346 | |||
|
r37101 | # delay import of textwrap | ||
def _MBTextWrapper(**kwargs): | ||||
class tw(textwrap.TextWrapper): | ||||
""" | ||||
Extend TextWrapper for width-awareness. | ||||
Neither number of 'bytes' in any encoding nor 'characters' is | ||||
appropriate to calculate terminal columns for specified string. | ||||
Original TextWrapper implementation uses built-in 'len()' directly, | ||||
so overriding is needed to use width information of each characters. | ||||
In addition, characters classified into 'ambiguous' width are | ||||
treated as wide in East Asian area, but as narrow in other. | ||||
This requires use decision to determine width of such characters. | ||||
""" | ||||
|
r43346 | |||
|
r37101 | def _cutdown(self, ucstr, space_left): | ||
l = 0 | ||||
colwidth = encoding.ucolwidth | ||||
|
r50179 | for i in range(len(ucstr)): | ||
|
r37101 | l += colwidth(ucstr[i]) | ||
if space_left < l: | ||||
return (ucstr[:i], ucstr[i:]) | ||||
|
r43347 | return ucstr, b'' | ||
|
r37101 | |||
# overriding of base class | ||||
def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): | ||||
space_left = max(width - cur_len, 1) | ||||
if self.break_long_words: | ||||
cut, res = self._cutdown(reversed_chunks[-1], space_left) | ||||
cur_line.append(cut) | ||||
reversed_chunks[-1] = res | ||||
elif not cur_line: | ||||
cur_line.append(reversed_chunks.pop()) | ||||
# this overriding code is imported from TextWrapper of Python 2.6 | ||||
# to calculate columns of string by 'encoding.ucolwidth()' | ||||
def _wrap_chunks(self, chunks): | ||||
colwidth = encoding.ucolwidth | ||||
lines = [] | ||||
if self.width <= 0: | ||||
|
r43347 | raise ValueError(b"invalid width %r (must be > 0)" % self.width) | ||
|
r37101 | |||
# Arrange in reverse order so items can be efficiently popped | ||||
# from a stack of chucks. | ||||
chunks.reverse() | ||||
while chunks: | ||||
# Start the list of chunks that will make up the current line. | ||||
# cur_len is just the length of all the chunks in cur_line. | ||||
cur_line = [] | ||||
cur_len = 0 | ||||
# Figure out which static string will prefix this line. | ||||
if lines: | ||||
indent = self.subsequent_indent | ||||
else: | ||||
indent = self.initial_indent | ||||
# Maximum width for this line. | ||||
width = self.width - len(indent) | ||||
# First chunk on line is whitespace -- drop it, unless this | ||||
# is the very beginning of the text (i.e. no lines started yet). | ||||
|
r43906 | if self.drop_whitespace and chunks[-1].strip() == '' and lines: | ||
|
r37101 | del chunks[-1] | ||
while chunks: | ||||
l = colwidth(chunks[-1]) | ||||
# Can at least squeeze this chunk onto the current line. | ||||
if cur_len + l <= width: | ||||
cur_line.append(chunks.pop()) | ||||
cur_len += l | ||||
# Nope, this line is full. | ||||
else: | ||||
break | ||||
# The current line is full, and the next chunk is too big to | ||||
# fit on *any* line (not just this one). | ||||
if chunks and colwidth(chunks[-1]) > width: | ||||
self._handle_long_word(chunks, cur_line, cur_len, width) | ||||
# If the last chunk on this line is all whitespace, drop it. | ||||
|
r43346 | if ( | ||
self.drop_whitespace | ||||
and cur_line | ||||
and cur_line[-1].strip() == r'' | ||||
): | ||||
|
r37101 | del cur_line[-1] | ||
# Convert current line back to a string and store it in list | ||||
# of all lines (return value). | ||||
if cur_line: | ||||
|
r43906 | lines.append(indent + ''.join(cur_line)) | ||
|
r37101 | |||
return lines | ||||
global _MBTextWrapper | ||||
_MBTextWrapper = tw | ||||
return tw(**kwargs) | ||||
|
r43346 | |||
|
r50470 | def wrap( | ||
line: bytes, width: int, initindent: bytes = b'', hangindent: bytes = b'' | ||||
) -> bytes: | ||||
|
r37101 | maxindent = max(len(hangindent), len(initindent)) | ||
if width <= maxindent: | ||||
# adjust for weird terminal size | ||||
width = max(78, maxindent + 1) | ||||
|
r43346 | line = line.decode( | ||
pycompat.sysstr(encoding.encoding), | ||||
pycompat.sysstr(encoding.encodingmode), | ||||
) | ||||
initindent = initindent.decode( | ||||
pycompat.sysstr(encoding.encoding), | ||||
pycompat.sysstr(encoding.encodingmode), | ||||
) | ||||
hangindent = hangindent.decode( | ||||
pycompat.sysstr(encoding.encoding), | ||||
pycompat.sysstr(encoding.encodingmode), | ||||
) | ||||
wrapper = _MBTextWrapper( | ||||
width=width, initial_indent=initindent, subsequent_indent=hangindent | ||||
) | ||||
|
r37101 | return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding)) | ||
|
r43346 | |||
_booleans = { | ||||
|
r43347 | b'1': True, | ||
b'yes': True, | ||||
b'true': True, | ||||
b'on': True, | ||||
b'always': True, | ||||
b'0': False, | ||||
b'no': False, | ||||
b'false': False, | ||||
b'off': False, | ||||
b'never': False, | ||||
|
r43346 | } | ||
|
r37101 | |||
|
r50470 | def parsebool(s: bytes) -> Optional[bool]: | ||
|
r37101 | """Parse s into a boolean. | ||
If s is not a valid boolean, returns None. | ||||
""" | ||||
return _booleans.get(s.lower(), None) | ||||
|
r37306 | |||
|
r43346 | |||
|
r50470 | # TODO: make arg mandatory (and fix code below?) | ||
def parselist(value: Optional[bytes]): | ||||
r47960 | """parse a configuration value as a list of comma/space separated strings | |||
>>> parselist(b'this,is "a small" ,test') | ||||
['this', 'is', 'a small', 'test'] | ||||
""" | ||||
def _parse_plain(parts, s, offset): | ||||
whitespace = False | ||||
while offset < len(s) and ( | ||||
s[offset : offset + 1].isspace() or s[offset : offset + 1] == b',' | ||||
): | ||||
whitespace = True | ||||
offset += 1 | ||||
if offset >= len(s): | ||||
return None, parts, offset | ||||
if whitespace: | ||||
parts.append(b'') | ||||
if s[offset : offset + 1] == b'"' and not parts[-1]: | ||||
return _parse_quote, parts, offset + 1 | ||||
elif s[offset : offset + 1] == b'"' and parts[-1][-1:] == b'\\': | ||||
parts[-1] = parts[-1][:-1] + s[offset : offset + 1] | ||||
return _parse_plain, parts, offset + 1 | ||||
parts[-1] += s[offset : offset + 1] | ||||
return _parse_plain, parts, offset + 1 | ||||
def _parse_quote(parts, s, offset): | ||||
if offset < len(s) and s[offset : offset + 1] == b'"': # "" | ||||
parts.append(b'') | ||||
offset += 1 | ||||
while offset < len(s) and ( | ||||
s[offset : offset + 1].isspace() | ||||
or s[offset : offset + 1] == b',' | ||||
): | ||||
offset += 1 | ||||
return _parse_plain, parts, offset | ||||
while offset < len(s) and s[offset : offset + 1] != b'"': | ||||
if ( | ||||
s[offset : offset + 1] == b'\\' | ||||
and offset + 1 < len(s) | ||||
and s[offset + 1 : offset + 2] == b'"' | ||||
): | ||||
offset += 1 | ||||
parts[-1] += b'"' | ||||
else: | ||||
parts[-1] += s[offset : offset + 1] | ||||
offset += 1 | ||||
if offset >= len(s): | ||||
real_parts = _configlist(parts[-1]) | ||||
if not real_parts: | ||||
parts[-1] = b'"' | ||||
else: | ||||
real_parts[0] = b'"' + real_parts[0] | ||||
parts = parts[:-1] | ||||
parts.extend(real_parts) | ||||
return None, parts, offset | ||||
offset += 1 | ||||
while offset < len(s) and s[offset : offset + 1] in [b' ', b',']: | ||||
offset += 1 | ||||
if offset < len(s): | ||||
if offset + 1 == len(s) and s[offset : offset + 1] == b'"': | ||||
parts[-1] += b'"' | ||||
offset += 1 | ||||
else: | ||||
parts.append(b'') | ||||
else: | ||||
return None, parts, offset | ||||
return _parse_plain, parts, offset | ||||
def _configlist(s): | ||||
s = s.rstrip(b' ,') | ||||
if not s: | ||||
return [] | ||||
parser, parts, offset = _parse_plain, [b''], 0 | ||||
while parser: | ||||
parser, parts, offset = parser(parts, s, offset) | ||||
return parts | ||||
if value is not None and isinstance(value, bytes): | ||||
result = _configlist(value.lstrip(b' ,\n')) | ||||
else: | ||||
result = value | ||||
return result or [] | ||||
|
r50470 | def evalpythonliteral(s: bytes): | ||
|
r37494 | """Evaluate a string containing a Python literal expression""" | ||
# We could backport our tokenizer hack to rewrite '' to u'' if we want | ||||
|
r49766 | return ast.literal_eval(s.decode('latin1')) | ||