From 9f90c1c65e1d274dbef93362e016dd8f26ce0808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Mon, 15 Jul 2024 15:51:30 +0200 Subject: [PATCH] Reformat gitchangelog with black --- contrib/gitchangelog/gitchangelog.py | 893 +++++++++++++++++---------- 1 file changed, 556 insertions(+), 337 deletions(-) diff --git a/contrib/gitchangelog/gitchangelog.py b/contrib/gitchangelog/gitchangelog.py index 386e1c75a5..bd71d25bff 100755 --- a/contrib/gitchangelog/gitchangelog.py +++ b/contrib/gitchangelog/gitchangelog.py @@ -76,14 +76,14 @@ try: except NameError: basestring = str ## pylint: disable=redefined-builtin -WIN32 = sys.platform == 'win32' +WIN32 = sys.platform == "win32" if WIN32: PLT_CFG = { - 'close_fds': False, + "close_fds": False, } else: PLT_CFG = { - 'close_fds': True, + "close_fds": True, } ## @@ -99,11 +99,18 @@ if WIN32 and not PY3: import ctypes import subprocess import _subprocess - from ctypes import byref, windll, c_char_p, c_wchar_p, c_void_p, \ - Structure, sizeof, c_wchar, WinError - from ctypes.wintypes import BYTE, WORD, LPWSTR, BOOL, DWORD, LPVOID, \ - HANDLE - + from ctypes import ( + byref, + windll, + c_char_p, + c_wchar_p, + c_void_p, + Structure, + sizeof, + c_wchar, + WinError, + ) + from ctypes.wintypes import BYTE, WORD, LPWSTR, BOOL, DWORD, LPVOID, HANDLE ## ## Types @@ -113,33 +120,42 @@ if WIN32 and not PY3: LPCTSTR = c_char_p LPTSTR = c_wchar_p LPSECURITY_ATTRIBUTES = c_void_p - LPBYTE = ctypes.POINTER(BYTE) + LPBYTE = ctypes.POINTER(BYTE) class STARTUPINFOW(Structure): _fields_ = [ - ("cb", DWORD), ("lpReserved", LPWSTR), - ("lpDesktop", LPWSTR), ("lpTitle", LPWSTR), - ("dwX", DWORD), ("dwY", DWORD), - ("dwXSize", DWORD), ("dwYSize", DWORD), - ("dwXCountChars", DWORD), ("dwYCountChars", DWORD), - ("dwFillAtrribute", DWORD), ("dwFlags", DWORD), - ("wShowWindow", WORD), ("cbReserved2", WORD), - ("lpReserved2", LPBYTE), ("hStdInput", HANDLE), - ("hStdOutput", HANDLE), ("hStdError", HANDLE), + ("cb", DWORD), + ("lpReserved", LPWSTR), + ("lpDesktop", LPWSTR), + ("lpTitle", LPWSTR), + ("dwX", DWORD), + ("dwY", DWORD), + ("dwXSize", DWORD), + ("dwYSize", DWORD), + ("dwXCountChars", DWORD), + ("dwYCountChars", DWORD), + ("dwFillAtrribute", DWORD), + ("dwFlags", DWORD), + ("wShowWindow", WORD), + ("cbReserved2", WORD), + ("lpReserved2", LPBYTE), + ("hStdInput", HANDLE), + ("hStdOutput", HANDLE), + ("hStdError", HANDLE), ] LPSTARTUPINFOW = ctypes.POINTER(STARTUPINFOW) - class PROCESS_INFORMATION(Structure): _fields_ = [ - ("hProcess", HANDLE), ("hThread", HANDLE), - ("dwProcessId", DWORD), ("dwThreadId", DWORD), + ("hProcess", HANDLE), + ("hThread", HANDLE), + ("dwProcessId", DWORD), + ("dwThreadId", DWORD), ] LPPROCESS_INFORMATION = ctypes.POINTER(PROCESS_INFORMATION) - class DUMMY_HANDLE(ctypes.c_void_p): def __init__(self, *a, **kw): @@ -154,23 +170,36 @@ if WIN32 and not PY3: def __int__(self): return self.value - CreateProcessW = windll.kernel32.CreateProcessW CreateProcessW.argtypes = [ - LPCTSTR, LPTSTR, LPSECURITY_ATTRIBUTES, - LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCTSTR, - LPSTARTUPINFOW, LPPROCESS_INFORMATION, + LPCTSTR, + LPTSTR, + LPSECURITY_ATTRIBUTES, + LPSECURITY_ATTRIBUTES, + BOOL, + DWORD, + LPVOID, + LPCTSTR, + LPSTARTUPINFOW, + LPPROCESS_INFORMATION, ] CreateProcessW.restype = BOOL - ## ## Patched functions/classes ## - def CreateProcess(executable, args, _p_attr, _t_attr, - inherit_handles, creation_flags, env, cwd, - startup_info): + def CreateProcess( + executable, + args, + _p_attr, + _t_attr, + inherit_handles, + creation_flags, + env, + cwd, + startup_info, + ): """Create a process supporting unicode executable and args for win32 Python implementation of CreateProcess using CreateProcessW for Win32 @@ -190,32 +219,58 @@ if WIN32 and not PY3: wenv = None if env is not None: ## LPCWSTR seems to be c_wchar_p, so let's say CWSTR is c_wchar - env = (unicode("").join([ - unicode("%s=%s\0") % (k, v) - for k, v in env.items()])) + unicode("\0") + env = ( + unicode("").join([unicode("%s=%s\0") % (k, v) for k, v in env.items()]) + ) + unicode("\0") wenv = (c_wchar * len(env))() wenv.value = env pi = PROCESS_INFORMATION() creation_flags |= CREATE_UNICODE_ENVIRONMENT - if CreateProcessW(executable, args, None, None, - inherit_handles, creation_flags, - wenv, cwd, byref(si), byref(pi)): - return (DUMMY_HANDLE(pi.hProcess), DUMMY_HANDLE(pi.hThread), - pi.dwProcessId, pi.dwThreadId) + if CreateProcessW( + executable, + args, + None, + None, + inherit_handles, + creation_flags, + wenv, + cwd, + byref(si), + byref(pi), + ): + return ( + DUMMY_HANDLE(pi.hProcess), + DUMMY_HANDLE(pi.hThread), + pi.dwProcessId, + pi.dwThreadId, + ) raise WinError() - class Popen(subprocess.Popen): """This superseeds Popen and corrects a bug in cPython 2.7 implem""" - def _execute_child(self, args, executable, preexec_fn, close_fds, - cwd, env, universal_newlines, - startupinfo, creationflags, shell, to_close, - p2cread, p2cwrite, - c2pread, c2pwrite, - errread, errwrite): + def _execute_child( + self, + args, + executable, + preexec_fn, + close_fds, + cwd, + env, + universal_newlines, + startupinfo, + creationflags, + shell, + to_close, + p2cread, + p2cwrite, + c2pread, + c2pwrite, + errread, + errwrite, + ): """Code from part of _execute_child from Python 2.7 (9fbb65e) There are only 2 little changes concerning the construction of @@ -235,16 +290,33 @@ if WIN32 and not PY3: startupinfo.wShowWindow = _subprocess.SW_HIDE comspec = os.environ.get("COMSPEC", unicode("cmd.exe")) args = unicode('{} /c "{}"').format(comspec, args) - if (_subprocess.GetVersion() >= 0x80000000 or - os.path.basename(comspec).lower() == "command.com"): + if ( + _subprocess.GetVersion() >= 0x80000000 + or os.path.basename(comspec).lower() == "command.com" + ): w9xpopen = self._find_w9xpopen() args = unicode('"%s" %s') % (w9xpopen, args) creationflags |= _subprocess.CREATE_NEW_CONSOLE - super(Popen, self)._execute_child(args, executable, - preexec_fn, close_fds, cwd, env, universal_newlines, - startupinfo, creationflags, False, to_close, p2cread, - p2cwrite, c2pread, c2pwrite, errread, errwrite) + super(Popen, self)._execute_child( + args, + executable, + preexec_fn, + close_fds, + cwd, + env, + universal_newlines, + startupinfo, + creationflags, + False, + to_close, + p2cread, + p2cwrite, + c2pread, + c2pwrite, + errread, + errwrite, + ) _subprocess.CreateProcess = CreateProcess @@ -278,6 +350,7 @@ Config file location will be resolved in this order: ## Shell command helper functions ## + def stderr(msg): print(msg, file=sys.stderr) @@ -339,9 +412,9 @@ def format_last_exception(prefix=" | "): """ - return '\n'.join( - str(prefix + line) - for line in traceback.format_exc().strip().split('\n')) + return "\n".join( + str(prefix + line) for line in traceback.format_exc().strip().split("\n") + ) ## @@ -349,8 +422,8 @@ def format_last_exception(prefix=" | "): ## _config_env = { - 'WIN32': WIN32, - 'PY3': PY3, + "WIN32": WIN32, + "PY3": PY3, } @@ -359,29 +432,32 @@ def available_in_config(f): return f -def load_config_file(filename, default_filename=None, - fail_if_not_present=True): +def load_config_file(filename, default_filename=None, fail_if_not_present=True): """Loads data from a config file.""" config = _config_env.copy() for fname in [default_filename, filename]: if fname and os.path.exists(fname): if not os.path.isfile(fname): - die("config file path '%s' exists but is not a file !" - % (fname, )) + die("config file path '%s' exists but is not a file !" % (fname,)) content = file_get_contents(fname) try: - code = compile(content, fname, 'exec') + code = compile(content, fname, "exec") exec(code, config) ## pylint: disable=exec-used except SyntaxError as e: - die('Syntax error in config file: %s\n%s' - 'File %s, line %i' - % (str(e), - (indent(e.text.rstrip(), " | ") + "\n") if e.text else "", - e.filename, e.lineno)) + die( + "Syntax error in config file: %s\n%s" + "File %s, line %i" + % ( + str(e), + (indent(e.text.rstrip(), " | ") + "\n") if e.text else "", + e.filename, + e.lineno, + ) + ) else: if fail_if_not_present: - die('%s config file is not found and is required.' % (fname, )) + die("%s config file is not found and is required." % (fname,)) return config @@ -390,6 +466,7 @@ def load_config_file(filename, default_filename=None, ## Text functions ## + @available_in_config class TextProc(object): @@ -405,12 +482,18 @@ class TextProc(object): if isinstance(value, TextProc): return TextProc(lambda text: value.fun(self.fun(text))) import inspect - (_frame, filename, lineno, _function_name, lines, _index) = \ - inspect.stack()[1] - raise SyntaxError("Invalid syntax in config file", - (filename, lineno, 0, - "Invalid chain with a non TextProc element %r:\n%s" - % (value, indent("".join(lines).strip(), " | ")))) + + (_frame, filename, lineno, _function_name, lines, _index) = inspect.stack()[1] + raise SyntaxError( + "Invalid syntax in config file", + ( + filename, + lineno, + 0, + "Invalid chain with a non TextProc element %r:\n%s" + % (value, indent("".join(lines).strip(), " | ")), + ), + ) def set_if_empty(text, msg="No commit message."): @@ -457,11 +540,9 @@ def indent(text, chars=" ", first=None): """ if first: first_line = text.split("\n")[0] - rest = '\n'.join(text.split("\n")[1:]) - return '\n'.join([(first + first_line).rstrip(), - indent(rest, chars=chars)]) - return '\n'.join([(chars + line).rstrip() - for line in text.split('\n')]) + rest = "\n".join(text.split("\n")[1:]) + return "\n".join([(first + first_line).rstrip(), indent(rest, chars=chars)]) + return "\n".join([(chars + line).rstrip() for line in text.split("\n")]) def paragraph_wrap(text, regexp="\n\n"): @@ -479,13 +560,15 @@ def paragraph_wrap(text, regexp="\n\n"): """ regexp = re.compile(regexp, re.MULTILINE) - return "\n".join("\n".join(textwrap.wrap(paragraph.strip())) - for paragraph in regexp.split(text)).strip() + return "\n".join( + "\n".join(textwrap.wrap(paragraph.strip())) for paragraph in regexp.split(text) + ).strip() def curryfy(f): return lambda *a, **kw: TextProc(lambda txt: f(txt, *a, **kw)) + ## these are curryfied version of their lower case definition Indent = curryfy(indent) @@ -495,14 +578,23 @@ noop = TextProc(lambda txt: txt) strip = TextProc(lambda txt: txt.strip()) SetIfEmpty = curryfy(set_if_empty) -for _label in ("Indent", "Wrap", "ReSub", "noop", "final_dot", - "ucfirst", "strip", "SetIfEmpty"): +for _label in ( + "Indent", + "Wrap", + "ReSub", + "noop", + "final_dot", + "ucfirst", + "strip", + "SetIfEmpty", +): _config_env[_label] = locals()[_label] ## ## File ## + def file_get_contents(filename): with open(filename) as f: out = f.read() @@ -511,8 +603,9 @@ def file_get_contents(filename): out = out.decode(_preferred_encoding) ## remove encoding declaration (for some reason, python 2.7 ## don't like it). - out = re.sub(r"^(\s*#.*\s*)coding[:=]\s*([-\w.]+\s*;?\s*)", - r"\1", out, re.DOTALL) + out = re.sub( + r"^(\s*#.*\s*)coding[:=]\s*([-\w.]+\s*;?\s*)", r"\1", out, re.DOTALL + ) return out @@ -520,9 +613,9 @@ def file_get_contents(filename): def file_put_contents(filename, string): """Write string to filename.""" if PY3: - fopen = open(filename, 'w', newline='') + fopen = open(filename, "w", newline="") else: - fopen = open(filename, 'wb') + fopen = open(filename, "wb") with fopen as f: f.write(string) @@ -532,6 +625,7 @@ def file_put_contents(filename, string): ## Inferring revision ## + def _file_regex_match(filename, pattern, **kw): if not os.path.isfile(filename): raise IOError("Can't open file '%s'." % filename) @@ -539,11 +633,11 @@ def _file_regex_match(filename, pattern, **kw): match = re.search(pattern, file_content, **kw) if match is None: stderr("file content: %r" % file_content) - if isinstance(pattern, type(re.compile(''))): + if isinstance(pattern, type(re.compile(""))): pattern = pattern.pattern raise ValueError( - "Regex %s did not match any substring in '%s'." - % (pattern, filename)) + "Regex %s did not match any substring in '%s'." % (pattern, filename) + ) return match @@ -554,13 +648,16 @@ def FileFirstRegexMatch(filename, pattern): dct = match.groupdict() if dct: if "rev" not in dct: - warn("Named pattern used, but no one are named 'rev'. " - "Using full match.") + warn( + "Named pattern used, but no one are named 'rev'. " + "Using full match." + ) return match.group(0) - if dct['rev'] is None: + if dct["rev"] is None: die("Named pattern used, but it was not valued.") - return dct['rev'] + return dct["rev"] return match.group(0) + return _call @@ -568,7 +665,10 @@ def FileFirstRegexMatch(filename, pattern): def Caret(l): def _call(): return "^%s" % eval_if_callable(l) + return _call + + ## ## System functions ## @@ -578,9 +678,10 @@ def Caret(l): ## PY2, ``sys.stdout.encoding`` without PYTHONIOENCODING set does not ## get any values set in subshells. However, if _preferred_encoding ## is not set to utf-8, it leads to encoding errors. -_preferred_encoding = os.environ.get("PYTHONIOENCODING") or \ - locale.getpreferredencoding() -DEFAULT_GIT_LOG_ENCODING = 'utf-8' +_preferred_encoding = ( + os.environ.get("PYTHONIOENCODING") or locale.getpreferredencoding() +) +DEFAULT_GIT_LOG_ENCODING = "utf-8" class Phile(object): @@ -660,10 +761,15 @@ class Proc(Popen): def __init__(self, command, env=None, encoding=_preferred_encoding): super(Proc, self).__init__( - command, shell=True, - stdin=PIPE, stdout=PIPE, stderr=PIPE, - close_fds=PLT_CFG['close_fds'], env=env, - universal_newlines=False) + command, + shell=True, + stdin=PIPE, + stdout=PIPE, + stderr=PIPE, + close_fds=PLT_CFG["close_fds"], + env=env, + universal_newlines=False, + ) self.stdin = Phile(self.stdin, encoding=encoding) self.stdout = Phile(self.stdout, encoding=encoding) @@ -672,17 +778,22 @@ class Proc(Popen): def cmd(command, env=None, shell=True): - p = Popen(command, shell=shell, - stdin=PIPE, stdout=PIPE, stderr=PIPE, - close_fds=PLT_CFG['close_fds'], env=env, - universal_newlines=False) + p = Popen( + command, + shell=shell, + stdin=PIPE, + stdout=PIPE, + stderr=PIPE, + close_fds=PLT_CFG["close_fds"], + env=env, + universal_newlines=False, + ) out, err = p.communicate() return ( - out.decode(getattr(sys.stdout, "encoding", None) or - _preferred_encoding), - err.decode(getattr(sys.stderr, "encoding", None) or - _preferred_encoding), - p.returncode) + out.decode(getattr(sys.stdout, "encoding", None) or _preferred_encoding), + err.decode(getattr(sys.stderr, "encoding", None) or _preferred_encoding), + p.returncode, + ) @available_in_config @@ -715,18 +826,23 @@ def wrap(command, ignore_errlvls=[0], env=None, shell=True): formatted = [] if out: - if out.endswith('\n'): + if out.endswith("\n"): out = out[:-1] formatted.append("stdout:\n%s" % indent(out, "| ")) if err: - if err.endswith('\n'): + if err.endswith("\n"): err = err[:-1] formatted.append("stderr:\n%s" % indent(err, "| ")) - msg = '\n'.join(formatted) + msg = "\n".join(formatted) - raise ShellError("Wrapped command %r exited with errorlevel %d.\n%s" - % (command, errlvl, indent(msg, chars=" ")), - errlvl=errlvl, command=command, out=out, err=err) + raise ShellError( + "Wrapped command %r exited with errorlevel %d.\n%s" + % (command, errlvl, indent(msg, chars=" ")), + errlvl=errlvl, + command=command, + out=out, + err=err, + ) return out @@ -741,6 +857,7 @@ def swrap(command, **kwargs): ## git information access ## + class SubGitObjectMixin(object): def __init__(self, repos): @@ -753,25 +870,25 @@ class SubGitObjectMixin(object): GIT_FORMAT_KEYS = { - 'sha1': "%H", - 'sha1_short': "%h", - 'subject': "%s", - 'author_name': "%an", - 'author_email': "%ae", - 'author_date': "%ad", - 'author_date_timestamp': "%at", - 'committer_name': "%cn", - 'committer_date_timestamp': "%ct", - 'raw_body': "%B", - 'body': "%b", + "sha1": "%H", + "sha1_short": "%h", + "subject": "%s", + "author_name": "%an", + "author_email": "%ae", + "author_date": "%ad", + "author_date_timestamp": "%at", + "committer_name": "%cn", + "committer_date_timestamp": "%ct", + "raw_body": "%B", + "body": "%b", } GIT_FULL_FORMAT_STRING = "%x00".join(GIT_FORMAT_KEYS.values()) -REGEX_RFC822_KEY_VALUE = \ - r'(^|\n)(?P[A-Z]\w+(-\w+)*): (?P[^\n]*(\n\s+[^\n]*)*)' -REGEX_RFC822_POSTFIX = \ - r'(%s)+$' % REGEX_RFC822_KEY_VALUE +REGEX_RFC822_KEY_VALUE = ( + r"(^|\n)(?P[A-Z]\w+(-\w+)*): (?P[^\n]*(\n\s+[^\n]*)*)" +) +REGEX_RFC822_POSTFIX = r"(%s)+$" % REGEX_RFC822_KEY_VALUE class GitCommit(SubGitObjectMixin): @@ -894,16 +1011,17 @@ class GitCommit(SubGitObjectMixin): missing_attrs = [l for l in attrs if l not in self.__dict__] ## some commit can be already fully specified (see ``mk_commit``) if missing_attrs: - aformat = "%x00".join(GIT_FORMAT_KEYS[l] - for l in missing_attrs) + aformat = "%x00".join(GIT_FORMAT_KEYS[l] for l in missing_attrs) try: - ret = self.git.log([identifier, "--max-count=1", - "--pretty=format:%s" % aformat, "--"]) + ret = self.git.log( + [identifier, "--max-count=1", "--pretty=format:%s" % aformat, "--"] + ) except ShellError: if DEBUG: raise - raise ValueError("Given commit identifier %r doesn't exists" - % self.identifier) + raise ValueError( + "Given commit identifier %r doesn't exists" % self.identifier + ) attr_values = ret.split("\x00") for attr, value in zip(missing_attrs, attr_values): setattr(self, attr, value.strip()) @@ -918,9 +1036,8 @@ class GitCommit(SubGitObjectMixin): dct = match.groupdict() key = dct["key"].replace("-", "_").lower() if "\n" in dct["value"]: - first_line, remaining = dct["value"].split('\n', 1) - value = "%s\n%s" % (first_line, - textwrap.dedent(remaining)) + first_line, remaining = dct["value"].split("\n", 1) + value = "%s\n%s" % (first_line, textwrap.dedent(remaining)) else: value = dct["value"] try: @@ -928,36 +1045,46 @@ class GitCommit(SubGitObjectMixin): except KeyError: setattr(self, "trailer_%s" % key, value) else: - setattr(self, "trailer_%s" % key, - prev_value + [value, ] + setattr( + self, + "trailer_%s" % key, + ( + prev_value + + [ + value, + ] if isinstance(prev_value, list) - else [prev_value, value, ]) + else [ + prev_value, + value, + ] + ), + ) self._trailer_parsed = True return getattr(self, label) @property def author_names(self): - return [re.sub(r'^([^<]+)<[^>]+>\s*$', r'\1', author).strip() - for author in self.authors] + return [ + re.sub(r"^([^<]+)<[^>]+>\s*$", r"\1", author).strip() + for author in self.authors + ] @property def authors(self): - co_authors = getattr(self, 'trailer_co_authored_by', []) - co_authors = co_authors if isinstance(co_authors, list) \ - else [co_authors] - return sorted(co_authors + - ["%s <%s>" % (self.author_name, self.author_email)]) + co_authors = getattr(self, "trailer_co_authored_by", []) + co_authors = co_authors if isinstance(co_authors, list) else [co_authors] + return sorted(co_authors + ["%s <%s>" % (self.author_name, self.author_email)]) @property def date(self): - d = datetime.datetime.utcfromtimestamp( - float(self.author_date_timestamp)) - return d.strftime('%Y-%m-%d') + d = datetime.datetime.utcfromtimestamp(float(self.author_date_timestamp)) + return d.strftime("%Y-%m-%d") @property def has_annotated_tag(self): try: - self.git.rev_parse(['%s^{tag}' % self.identifier, "--"]) + self.git.rev_parse(["%s^{tag}" % self.identifier, "--"]) return True except ShellError as e: if e.errlvl != 128: @@ -967,16 +1094,18 @@ class GitCommit(SubGitObjectMixin): @property def tagger_date_timestamp(self): if not self.has_annotated_tag: - raise ValueError("Can't access 'tagger_date_timestamp' on commit without annotated tag.") + raise ValueError( + "Can't access 'tagger_date_timestamp' on commit without annotated tag." + ) tagger_date_utc = self.git.for_each_ref( - 'refs/tags/%s' % self.identifier, format='%(taggerdate:raw)') + "refs/tags/%s" % self.identifier, format="%(taggerdate:raw)" + ) return tagger_date_utc.split(" ", 1)[0] @property def tagger_date(self): - d = datetime.datetime.utcfromtimestamp( - float(self.tagger_date_timestamp)) - return d.strftime('%Y-%m-%d') + d = datetime.datetime.utcfromtimestamp(float(self.tagger_date_timestamp)) + return d.strftime("%Y-%m-%d") def __le__(self, value): if not isinstance(value, GitCommit): @@ -1096,8 +1225,7 @@ class GitConfig(SubGitObjectMixin): res = self.git.config(label) except ShellError as e: if e.errlvl == 1 and e.out == "": - raise AttributeError("key %r is not found in git config." - % label) + raise AttributeError("key %r is not found in git config." % label) raise return res @@ -1121,15 +1249,19 @@ class GitCmd(SubGitObjectMixin): return swrap(command, **kwargs) def method(*args, **kwargs): - if (len(args) == 1 and not isinstance(args[0], basestring)): + if len(args) == 1 and not isinstance(args[0], basestring): return dir_swrap( - ['git', label, ] + args[0], + [ + "git", + label, + ] + + args[0], shell=False, - env=kwargs.get("env", None)) + env=kwargs.get("env", None), + ) cli_args = [] for key, value in kwargs.items(): - cli_key = (("-%s" if len(key) == 1 else "--%s") - % key.replace("_", "-")) + cli_key = ("-%s" if len(key) == 1 else "--%s") % key.replace("_", "-") if isinstance(value, bool): cli_args.append(cli_key) else: @@ -1138,7 +1270,15 @@ class GitCmd(SubGitObjectMixin): cli_args.extend(args) - return dir_swrap(['git', label, ] + cli_args, shell=False) + return dir_swrap( + [ + "git", + label, + ] + + cli_args, + shell=False, + ) + return method @@ -1158,7 +1298,8 @@ class GitRepos(object): raise raise EnvironmentError( "Required ``git`` command not found or broken in $PATH. " - "(calling ``git version`` failed.)") + "(calling ``git version`` failed.)" + ) ## verify that we are in a git repository try: @@ -1167,13 +1308,12 @@ class GitRepos(object): if DEBUG: raise raise EnvironmentError( - "Not in a git repository. (calling ``git remote`` failed.)") + "Not in a git repository. (calling ``git remote`` failed.)" + ) self.bare = self.git.rev_parse(is_bare_repository=True) == "true" - self.toplevel = (None if self.bare else - self.git.rev_parse(show_toplevel=True)) - self.gitdir = normpath(self.git.rev_parse(git_dir=True), - cwd=self._orig_path) + self.toplevel = None if self.bare else self.git.rev_parse(show_toplevel=True) + self.gitdir = normpath(self.git.rev_parse(git_dir=True), cwd=self._orig_path) @classmethod def create(cls, directory, *args, **kwargs): @@ -1217,29 +1357,38 @@ class GitRepos(object): ## ``git tags --sort -v:refname`` in git version >2.0. ## Sorting and reversing with command line is not available on ## git version <2.0 - return sorted([self.commit(tag) for tag in tags if tag != ''], - key=lambda x: int(x.committer_date_timestamp)) + return sorted( + [self.commit(tag) for tag in tags if tag != ""], + key=lambda x: int(x.committer_date_timestamp), + ) - def log(self, includes=["HEAD", ], excludes=[], include_merge=True, - encoding=_preferred_encoding): + def log( + self, + includes=[ + "HEAD", + ], + excludes=[], + include_merge=True, + encoding=_preferred_encoding, + ): """Reverse chronological list of git repository's commits Note: rev lists can be GitCommit instance list or identifier list. """ - refs = {'includes': includes, - 'excludes': excludes} - for ref_type in ('includes', 'excludes'): + refs = {"includes": includes, "excludes": excludes} + for ref_type in ("includes", "excludes"): for idx, ref in enumerate(refs[ref_type]): if not isinstance(ref, GitCommit): refs[ref_type][idx] = self.commit(ref) ## --topo-order: don't mix commits from separate branches. - plog = Proc("git log --stdin -z --topo-order --pretty=format:%s %s --" - % (GIT_FULL_FORMAT_STRING, - '--no-merges' if not include_merge else ''), - encoding=encoding) + plog = Proc( + "git log --stdin -z --topo-order --pretty=format:%s %s --" + % (GIT_FULL_FORMAT_STRING, "--no-merges" if not include_merge else ""), + encoding=encoding, + ) for ref in refs["includes"]: plog.stdin.write("%s\n" % ref.sha1) @@ -1258,11 +1407,10 @@ class GitRepos(object): try: while True: ## next(values) will eventualy raise a StopIteration - yield mk_commit(dict([(key, next(values)) - for key in GIT_FORMAT_KEYS])) + yield mk_commit(dict([(key, next(values)) for key in GIT_FORMAT_KEYS])) except StopIteration: pass ## since 3.7, we are not allowed anymore to trickle down - ## StopIteration. + ## StopIteration. finally: plog.stdout.close() plog.stderr.close() @@ -1290,45 +1438,46 @@ def ensure_template_file_exists(label, template_name): """ try: - template_path = GitRepos(os.getcwd()).config.get( - "gitchangelog.template-path") + template_path = GitRepos(os.getcwd()).config.get("gitchangelog.template-path") except ShellError as e: stderr( "Error parsing git config: %s." - " Won't be able to read 'template-path' if defined." - % (str(e))) + " Won't be able to read 'template-path' if defined." % (str(e)) + ) template_path = None if template_path: path_file = path_label = template_path else: path_file = os.getcwd() - path_label = os.path.join(os.path.dirname(os.path.realpath(__file__)), - "templates", label) + path_label = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "templates", label + ) - for ftn in [os.path.join(path_file, template_name), - os.path.join(path_label, "%s.tpl" % template_name)]: + for ftn in [ + os.path.join(path_file, template_name), + os.path.join(path_label, "%s.tpl" % template_name), + ]: if os.path.isfile(ftn): return ftn templates = glob.glob(os.path.join(path_label, "*.tpl")) if len(templates) > 0: - msg = ("These are the available %s templates:" % label) - msg += "\n - " + \ - "\n - ".join(os.path.basename(f).split(".")[0] - for f in templates) + msg = "These are the available %s templates:" % label + msg += "\n - " + "\n - ".join( + os.path.basename(f).split(".")[0] for f in templates + ) msg += "\nTemplates are located in %r" % path_label else: - msg = "No available %s templates found in %r." \ - % (label, path_label) - die("Error: Invalid %s template name %r.\n" % (label, template_name) + - "%s" % msg) + msg = "No available %s templates found in %r." % (label, path_label) + die("Error: Invalid %s template name %r.\n" % (label, template_name) + "%s" % msg) ## ## Output Engines ## + @available_in_config def rest_py(data, opts={}): """Returns ReStructured Text changelog content from data""" @@ -1337,17 +1486,18 @@ def rest_py(data, opts={}): return (label.strip() + "\n") + (char * len(label) + "\n") def render_version(version): - title = "%s (%s)" % (version["tag"], version["date"]) \ - if version["tag"] else \ - opts["unreleased_version_label"] + title = ( + "%s (%s)" % (version["tag"], version["date"]) + if version["tag"] + else opts["unreleased_version_label"] + ) s = rest_title(title, char="-") sections = version["sections"] nb_sections = len(sections) for section in sections: - section_label = section["label"] if section.get("label", None) \ - else "Other" + section_label = section["label"] if section.get("label", None) else "Other" if not (section_label == "Other" and nb_sections == 1): s += "\n" + rest_title(section_label, "~") @@ -1358,10 +1508,9 @@ def rest_py(data, opts={}): def render_commit(commit, opts=opts): subject = commit["subject"] - subject += " [%s]" % (", ".join(commit["authors"]), ) + subject += " [%s]" % (", ".join(commit["authors"]),) - entry = indent('\n'.join(textwrap.wrap(subject)), - first="- ").strip() + "\n" + entry = indent("\n".join(textwrap.wrap(subject)), first="- ").strip() + "\n" if commit["body"]: entry += "\n" + indent(commit["body"]) @@ -1394,19 +1543,20 @@ if pystache: def stuffed_versions(versions, opts): for version in versions: - title = "%s (%s)" % (version["tag"], version["date"]) \ - if version["tag"] else \ - opts["unreleased_version_label"] + title = ( + "%s (%s)" % (version["tag"], version["date"]) + if version["tag"] + else opts["unreleased_version_label"] + ) version["label"] = title version["label_chars"] = list(version["label"]) for section in version["sections"]: section["label_chars"] = list(section["label"]) - section["display_label"] = \ - not (section["label"] == "Other" and - len(version["sections"]) == 1) + section["display_label"] = not ( + section["label"] == "Other" and len(version["sections"]) == 1 + ) for commit in section["commits"]: - commit["author_names_joined"] = ", ".join( - commit["authors"]) + commit["author_names_joined"] = ", ".join(commit["authors"]) commit["body_indented"] = indent(commit["body"]) yield version @@ -1432,10 +1582,11 @@ else: if mako: - import mako.template ## pylint: disable=wrong-import-position + import mako.template ## pylint: disable=wrong-import-position - mako_env = dict((f.__name__, f) for f in (ucfirst, indent, textwrap, - paragraph_wrap)) + mako_env = dict( + (f.__name__, f) for f in (ucfirst, indent, textwrap, paragraph_wrap) + ) @available_in_config def makotemplate(template_name): @@ -1450,8 +1601,7 @@ if mako: def renderer(data, opts): kwargs = mako_env.copy() - kwargs.update({"data": data, - "opts": opts}) + kwargs.update({"data": data, "opts": opts}) return template.render(**kwargs) return renderer @@ -1467,13 +1617,15 @@ else: ## Publish action ## + @available_in_config def stdout(content): for chunk in content: safe_print(chunk) + + @available_in_config -def FileInsertAtFirstRegexMatch(filename, pattern, flags=0, - idx=lambda m: m.start()): +def FileInsertAtFirstRegexMatch(filename, pattern, flags=0, idx=lambda m: m.start()): def write_content(f, content): for content_line in content: @@ -1496,9 +1648,9 @@ def FileInsertAtFirstRegexMatch(filename, pattern, flags=0, offset = new_offset dst.write(line) continue - dst.write(line[0:index - offset]) + dst.write(line[0 : index - offset]) write_content(dst, content) - dst.write(line[index - offset:]) + dst.write(line[index - offset :]) postfix = True if not postfix: write_content(dst, content) @@ -1512,15 +1664,17 @@ def FileInsertAtFirstRegexMatch(filename, pattern, flags=0, @available_in_config def FileRegexSubst(filename, pattern, replace, flags=0): - replace = re.sub(r'\\([0-9+])', r'\\g<\1>', replace) + replace = re.sub(r"\\([0-9+])", r"\\g<\1>", replace) def _wrapped(content): src = file_get_contents(filename) ## Protect replacement pattern against the following expansion of '\o' src = re.sub( - pattern, - replace.replace(r'\o', "".join(content).replace('\\', '\\\\')), - src, flags=flags) + pattern, + replace.replace(r"\o", "".join(content).replace("\\", "\\\\")), + src, + flags=flags, + ) if not PY3: src = src.encode(_preferred_encoding) file_put_contents(filename, src) @@ -1532,16 +1686,19 @@ def FileRegexSubst(filename, pattern, replace, flags=0): ## Data Structure ## -def versions_data_iter(repository, revlist=None, - ignore_regexps=[], - section_regexps=[(None, '')], - tag_filter_regexp=r"\d+\.\d+(\.\d+)?", - include_merge=True, - body_process=lambda x: x, - subject_process=lambda x: x, - log_encoding=DEFAULT_GIT_LOG_ENCODING, - warn=warn, ## Mostly used for test - ): + +def versions_data_iter( + repository, + revlist=None, + ignore_regexps=[], + section_regexps=[(None, "")], + tag_filter_regexp=r"\d+\.\d+(\.\d+)?", + include_merge=True, + body_process=lambda x: x, + subject_process=lambda x: x, + log_encoding=DEFAULT_GIT_LOG_ENCODING, + warn=warn, ## Mostly used for test +): """Returns an iterator through versions data structures (see ``gitchangelog.rc.reference`` file for more info) @@ -1565,20 +1722,35 @@ def versions_data_iter(repository, revlist=None, ## Hash to speedup lookups versions_done = {} - excludes = [rev[1:] - for rev in repository.git.rev_parse([ - "--rev-only", ] + revlist + ["--", ]).split("\n") - if rev.startswith("^")] if revlist else [] + excludes = ( + [ + rev[1:] + for rev in repository.git.rev_parse( + [ + "--rev-only", + ] + + revlist + + [ + "--", + ] + ).split("\n") + if rev.startswith("^") + ] + if revlist + else [] + ) revs = repository.git.rev_list(*revlist).split("\n") if revlist else [] revs = [rev for rev in revs if rev != ""] if revlist and not revs: - die("No commits matching given revlist: %s" % (" ".join(revlist), )) + die("No commits matching given revlist: %s" % (" ".join(revlist),)) - tags = [tag - for tag in repository.tags(contains=revs[-1] if revs else None) - if re.match(tag_filter_regexp, tag.identifier)] + tags = [ + tag + for tag in repository.tags(contains=revs[-1] if revs else None) + if re.match(tag_filter_regexp, tag.identifier) + ] tags.append(repository.commit("HEAD")) @@ -1612,40 +1784,47 @@ def versions_data_iter(repository, revlist=None, sections = collections.defaultdict(list) commits = repository.log( includes=[min(tag, max_rev)], - excludes=tags[idx + 1:] + excludes, + excludes=tags[idx + 1 :] + excludes, include_merge=include_merge, - encoding=log_encoding) + encoding=log_encoding, + ) for commit in commits: - if any(re.search(pattern, commit.subject) is not None - for pattern in ignore_regexps): + if any( + re.search(pattern, commit.subject) is not None + for pattern in ignore_regexps + ): continue matched_section = first_matching(section_regexps, commit.subject) ## Finally storing the commit in the matching section - sections[matched_section].append({ - "author": commit.author_name, - "authors": commit.author_names, - "subject": subject_process(commit.subject), - "body": body_process(commit.body), - "commit": commit, - }) + sections[matched_section].append( + { + "author": commit.author_name, + "authors": commit.author_names, + "subject": subject_process(commit.subject), + "body": body_process(commit.body), + "commit": commit, + } + ) ## Flush current version - current_version["sections"] = [{"label": k, "commits": sections[k]} - for k in section_order - if k in sections] + current_version["sections"] = [ + {"label": k, "commits": sections[k]} for k in section_order if k in sections + ] if len(current_version["sections"]) != 0: yield current_version versions_done[tag] = current_version -def changelog(output_engine=rest_py, - unreleased_version_label="unreleased", - warn=warn, ## Mostly used for test - **kwargs): +def changelog( + output_engine=rest_py, + unreleased_version_label="unreleased", + warn=warn, ## Mostly used for test + **kwargs +): """Returns a string containing the changelog of given repository This function returns a string corresponding to the template rendered with @@ -1665,13 +1844,12 @@ def changelog(output_engine=rest_py, """ opts = { - 'unreleased_version_label': unreleased_version_label, + "unreleased_version_label": unreleased_version_label, } ## Setting main container of changelog elements title = None if kwargs.get("revlist") else "Changelog" - data = {"title": title, - "versions": []} + data = {"title": title, "versions": []} versions = versions_data_iter(warn=warn, **kwargs) @@ -1686,6 +1864,7 @@ def changelog(output_engine=rest_py, return output_engine(data=data, opts=opts) + ## ## Manage obsolete options ## @@ -1707,9 +1886,9 @@ def obsolete_replace_regexps(config): """ if "replace_regexps" in config: for pattern, replace in config["replace_regexps"].items(): - config["subject_process"] = \ - ReSub(pattern, replace) | \ - config.get("subject_process", ucfirst | final_dot) + config["subject_process"] = ReSub(pattern, replace) | config.get( + "subject_process", ucfirst | final_dot + ) @obsolete_option_manager @@ -1721,8 +1900,9 @@ def obsolete_body_split_regexp(config): """ if "body_split_regex" in config: - config["body_process"] = Wrap(config["body_split_regex"]) | \ - config.get("body_process", noop) + config["body_process"] = Wrap(config["body_split_regex"]) | config.get( + "body_process", noop + ) def manage_obsolete_options(config): @@ -1734,27 +1914,39 @@ def manage_obsolete_options(config): ## Command line parsing ## + def parse_cmd_line(usage, description, epilog, exname, version): import argparse - kwargs = dict(usage=usage, - description=description, - epilog="\n" + epilog, - prog=exname, - formatter_class=argparse.RawTextHelpFormatter) + + kwargs = dict( + usage=usage, + description=description, + epilog="\n" + epilog, + prog=exname, + formatter_class=argparse.RawTextHelpFormatter, + ) try: parser = argparse.ArgumentParser(version=version, **kwargs) except TypeError: ## compat with argparse from python 3.4 parser = argparse.ArgumentParser(**kwargs) - parser.add_argument('-v', '--version', - help="show program's version number and exit", - action="version", version=version) + parser.add_argument( + "-v", + "--version", + help="show program's version number and exit", + action="version", + version=version, + ) - parser.add_argument('-d', '--debug', - help="Enable debug mode (show full tracebacks).", - action="store_true", dest="debug") - parser.add_argument('revlist', nargs='*', action="store", default=[]) + parser.add_argument( + "-d", + "--debug", + help="Enable debug mode (show full tracebacks).", + action="store_true", + dest="debug", + ) + parser.add_argument("revlist", nargs="*", action="store", default=[]) ## Remove "show" as first argument for compatibility reason. @@ -1765,10 +1957,10 @@ def parse_cmd_line(usage, description, epilog, exname, version): continue if arg == "show": warn("'show' positional argument is deprecated.") - argv += sys.argv[i + 2:] + argv += sys.argv[i + 2 :] break else: - argv += sys.argv[i + 1:] + argv += sys.argv[i + 1 :] break return parser.parse_args(argv) @@ -1785,19 +1977,21 @@ def get_revision(repository, config, opts): if revs: revs = eval_if_callable(revs) if not isinstance(revs, list): - die("Invalid type for 'revs' in config file. " + die( + "Invalid type for 'revs' in config file. " "A 'list' type is required, and a %r was given." - % type(revs).__name__) - revs = [eval_if_callable(rev) - for rev in revs] + % type(revs).__name__ + ) + revs = [eval_if_callable(rev) for rev in revs] else: revs = [] for rev in revs: if not isinstance(rev, basestring): - die("Invalid type for revision in revs list from config file. " - "'str' type is required, and a %r was given." - % type(rev).__name__) + die( + "Invalid type for revision in revs list from config file. " + "'str' type is required, and a %r was given." % type(rev).__name__ + ) try: repository.git.rev_parse([rev, "--rev_only", "--"]) except ShellError: @@ -1805,7 +1999,9 @@ def get_revision(repository, config, opts): raise die("Revision %r is not valid." % rev) - if revs == ["HEAD", ]: + if revs == [ + "HEAD", + ]: return [] return revs @@ -1819,8 +2015,8 @@ def get_log_encoding(repository, config): except ShellError as e: warn( "Error parsing git config: %s." - " Couldn't check if 'i18n.logOuputEncoding' was set." - % (str(e))) + " Couldn't check if 'i18n.logOuputEncoding' was set." % (str(e)) + ) ## Final defaults coming from git defaults return log_encoding or DEFAULT_GIT_LOG_ENCODING @@ -1830,6 +2026,7 @@ def get_log_encoding(repository, config): ## Config Manager ## + class Config(dict): def __getitem__(self, label): @@ -1842,20 +2039,24 @@ class Config(dict): ## Safe print ## + def safe_print(content): if not PY3: if isinstance(content, unicode): content = content.encode(_preferred_encoding) try: - print(content, end='') + print(content, end="") sys.stdout.flush() except UnicodeEncodeError: if DEBUG: raise ## XXXvlab: should use $COLUMNS in bash and for windows: ## http://stackoverflow.com/questions/14978548 - stderr(paragraph_wrap(textwrap.dedent("""\ + stderr( + paragraph_wrap( + textwrap.dedent( + """\ UnicodeEncodeError: There was a problem outputing the resulting changelog to your console. @@ -1863,21 +2064,28 @@ def safe_print(content): This probably means that the changelog contains characters that can't be translated to characters in your current charset (%s). - """) % sys.stdout.encoding)) - if WIN32 and PY_VERSION < 3.6 and sys.stdout.encoding != 'utf-8': + """ + ) + % sys.stdout.encoding + ) + ) + if WIN32 and PY_VERSION < 3.6 and sys.stdout.encoding != "utf-8": ## As of PY 3.6, encoding is now ``utf-8`` regardless of ## PYTHONIOENCODING ## https://www.python.org/dev/peps/pep-0528/ - stderr(" You might want to try to fix that by setting " - "PYTHONIOENCODING to 'utf-8'.") + stderr( + " You might want to try to fix that by setting " + "PYTHONIOENCODING to 'utf-8'." + ) exit(1) except IOError as e: if e.errno == 0 and not PY3 and WIN32: ## Yes, had a strange IOError Errno 0 after outputing string ## that contained UTF-8 chars on Windows and PY2.7 pass ## Ignoring exception - elif ((WIN32 and e.errno == 22) or ## Invalid argument - (not WIN32 and e.errno == errno.EPIPE)): ## Broken Pipe + elif (WIN32 and e.errno == 22) or ( ## Invalid argument + not WIN32 and e.errno == errno.EPIPE + ): ## Broken Pipe ## Nobody is listening anymore to stdout it seems. Let's bailout. if PY3: try: @@ -1888,7 +2096,7 @@ def safe_print(content): except BrokenPipeError: ## expected outcome on linux pass except OSError as e2: - if e2.errno != 22: ## expected outcome on WIN32 + if e2.errno != 22: ## expected outcome on WIN32 raise ## Yay ! stdout is closed we can now exit safely. exit(0) @@ -1900,14 +2108,15 @@ def safe_print(content): ## Main ## + def main(): global DEBUG ## Basic environment infos reference_config = os.path.join( - os.path.dirname(os.path.realpath(__file__)), - "gitchangelog.rc.reference") + os.path.dirname(os.path.realpath(__file__)), "gitchangelog.rc.reference" + ) basename = os.path.basename(sys.argv[0]) if basename.endswith(".py"): @@ -1916,13 +2125,15 @@ def main(): debug_varname = "DEBUG_%s" % basename.upper() DEBUG = os.environ.get(debug_varname, False) - i = lambda x: x % {'exname': basename} + i = lambda x: x % {"exname": basename} - opts = parse_cmd_line(usage=i(usage_msg), - description=i(description_msg), - epilog=i(epilog_msg), - exname=basename, - version=__version__) + opts = parse_cmd_line( + usage=i(usage_msg), + description=i(description_msg), + epilog=i(epilog_msg), + exname=basename, + version=__version__, + ) DEBUG = DEBUG or opts.debug try: @@ -1940,19 +2151,25 @@ def main(): except ShellError as e: stderr( "Error parsing git config: %s." - " Won't be able to read 'rc-path' if defined." - % (str(e))) + " Won't be able to read 'rc-path' if defined." % (str(e)) + ) gc_rc = None gc_rc = normpath(gc_rc, cwd=repository.toplevel) if gc_rc else None ## config file lookup resolution for enforce_file_existence, fun in [ - (True, lambda: os.environ.get('GITCHANGELOG_CONFIG_FILENAME')), + (True, lambda: os.environ.get("GITCHANGELOG_CONFIG_FILENAME")), (True, lambda: gc_rc), - (False, - lambda: (os.path.join(repository.toplevel, ".%s.rc" % basename)) - if not repository.bare else None)]: + ( + False, + lambda: ( + (os.path.join(repository.toplevel, ".%s.rc" % basename)) + if not repository.bare + else None + ), + ), + ]: changelogrc = fun() if changelogrc: if not os.path.exists(changelogrc): @@ -1970,23 +2187,26 @@ def main(): config = load_config_file( os.path.expanduser(changelogrc), default_filename=reference_config, - fail_if_not_present=False) + fail_if_not_present=False, + ) config = Config(config) log_encoding = get_log_encoding(repository, config) revlist = get_revision(repository, config, opts) - config['unreleased_version_label'] = eval_if_callable( - config['unreleased_version_label']) + config["unreleased_version_label"] = eval_if_callable( + config["unreleased_version_label"] + ) manage_obsolete_options(config) try: content = changelog( - repository=repository, revlist=revlist, - ignore_regexps=config['ignore_regexps'], - section_regexps=config['section_regexps'], - unreleased_version_label=config['unreleased_version_label'], - tag_filter_regexp=config['tag_filter_regexp'], + repository=repository, + revlist=revlist, + ignore_regexps=config["ignore_regexps"], + section_regexps=config["section_regexps"], + unreleased_version_label=config["unreleased_version_label"], + tag_filter_regexp=config["tag_filter_regexp"], output_engine=config.get("output_engine", rest_py), include_merge=config.get("include_merge", True), body_process=config.get("body_process", noop), @@ -2001,23 +2221,22 @@ def main(): except KeyboardInterrupt: if DEBUG: - err("Keyboard interrupt received while running '%s':" - % (basename, )) + err("Keyboard interrupt received while running '%s':" % (basename,)) stderr(format_last_exception()) else: err("Keyboard Interrupt. Bailing out.") exit(130) ## Actual SIGINT as bash process convention. except Exception as e: ## pylint: disable=broad-except if DEBUG: - err("Exception while running '%s':" - % (basename, )) + err("Exception while running '%s':" % (basename,)) stderr(format_last_exception()) else: message = "%s" % e err(message) - stderr(" (set %s environment variable, " - "or use ``--debug`` to see full traceback)" % - (debug_varname, )) + stderr( + " (set %s environment variable, " + "or use ``--debug`` to see full traceback)" % (debug_varname,) + ) exit(255)