brintos

brintos / llvm-project-archived public Read only

0
0
Text · 27.7 KiB · dcc0a53 Raw
859 lines · plain
1#!/usr/bin/env python32#3# ===- git-clang-format - ClangFormat Git Integration -------*- python -*--=== #4#5# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.6# See https://llvm.org/LICENSE.txt for license information.7# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception8#9# ===----------------------------------------------------------------------=== #10 11r"""12clang-format git integration13============================14 15This file provides a clang-format integration for git. Put it somewhere in your16path and ensure that it is executable. Then, "git clang-format" will invoke17clang-format on the changes in current files or a specific commit.18 19For further details, run:20git clang-format -h21 22Requires Python version >=3.823"""24 25from __future__ import absolute_import, division, print_function26import argparse27import collections28import contextlib29import errno30import os31import re32import subprocess33import sys34 35usage = "git clang-format [OPTIONS] [<commit>] [<commit>|--staged] [--] [<file>...]"36 37desc = """38If zero or one commits are given, run clang-format on all lines that differ39between the working directory and <commit>, which defaults to HEAD.  Changes are40only applied to the working directory, or in the stage/index.41 42Examples:43  To format staged changes, i.e everything that's been `git add`ed:44    git clang-format45 46  To also format everything touched in the most recent commit:47    git clang-format HEAD~148 49  If you're on a branch off main, to format everything touched on your branch:50    git clang-format main51 52If two commits are given (requires --diff), run clang-format on all lines in the53second <commit> that differ from the first <commit>.54 55The following git-config settings set the default of the corresponding option:56  clangFormat.binary57  clangFormat.commit58  clangFormat.extensions59  clangFormat.style60"""61 62# Name of the temporary index file in which save the output of clang-format.63# This file is created within the .git directory.64temp_index_basename = "clang-format-index"65 66 67Range = collections.namedtuple("Range", "start, count")68 69 70def main():71    config = load_git_config()72 73    # In order to keep '--' yet allow options after positionals, we need to74    # check for '--' ourselves.  (Setting nargs='*' throws away the '--', while75    # nargs=argparse.REMAINDER disallows options after positionals.)76    argv = sys.argv[1:]77    try:78        idx = argv.index("--")79    except ValueError:80        dash_dash = []81    else:82        dash_dash = argv[idx:]83        argv = argv[:idx]84 85    default_extensions = ",".join(86        [87            # From clang/lib/Frontend/FrontendOptions.cpp, all lower case88            "c",89            "h",  # C90            "m",  # ObjC91            "mm",  # ObjC++92            "cc",93            "cp",94            "cpp",95            "c++",96            "cxx",97            "hh",98            "hpp",99            "hxx",100            "inc",  # C++101            "ccm",102            "cppm",103            "cxxm",104            "c++m",  # C++ Modules105            "cu",106            "cuh",  # CUDA107            "cl",  # OpenCL108            # Other languages that clang-format supports109            "proto",110            "protodevel",  # Protocol Buffers111            "java",  # Java112            "js",113            "mjs",114            "cjs",  # JavaScript115            "ts",  # TypeScript116            "cs",  # C Sharp117            "json",118            "ipynb",  # JSON119            "sv",120            "svh",121            "v",122            "vh",  # Verilog123            "td",  # TableGen124            "txtpb",125            "textpb",126            "pb.txt",127            "textproto",128            "asciipb",  # TextProto129        ]130    )131 132    p = argparse.ArgumentParser(133        usage=usage,134        formatter_class=argparse.RawDescriptionHelpFormatter,135        description=desc,136    )137    p.add_argument(138        "--binary",139        default=config.get("clangformat.binary", "clang-format"),140        help="path to clang-format",141    ),142    p.add_argument(143        "--commit",144        default=config.get("clangformat.commit", "HEAD"),145        help="default commit to use if none is specified",146    ),147    p.add_argument(148        "--diff",149        action="store_true",150        help="print a diff instead of applying the changes",151    )152    p.add_argument(153        "--diffstat",154        action="store_true",155        help="print a diffstat instead of applying the changes",156    )157    p.add_argument(158        "--extensions",159        default=config.get("clangformat.extensions", default_extensions),160        help=(161            "comma-separated list of file extensions to format, "162            "excluding the period and case-insensitive"163        ),164    ),165    p.add_argument(166        "-f",167        "--force",168        action="store_true",169        help="allow changes to unstaged files",170    )171    p.add_argument(172        "-p", "--patch", action="store_true", help="select hunks interactively"173    )174    p.add_argument(175        "-q",176        "--quiet",177        action="count",178        default=0,179        help="print less information",180    )181    p.add_argument(182        "--staged",183        "--cached",184        action="store_true",185        help="format lines in the stage instead of the working dir",186    )187    p.add_argument(188        "--style",189        default=config.get("clangformat.style", None),190        help="passed to clang-format",191    ),192    p.add_argument(193        "-v",194        "--verbose",195        action="count",196        default=0,197        help="print extra information",198    )199    p.add_argument(200        "--diff_from_common_commit",201        action="store_true",202        help=(203            "diff from the last common commit for commits in "204            "separate branches rather than the exact point of the "205            "commits"206        ),207    )208    # We gather all the remaining positional arguments into 'args' since we need209    # to use some heuristics to determine whether or not <commit> was present.210    # However, to print pretty messages, we make use of metavar and help.211    p.add_argument(212        "args",213        nargs="*",214        metavar="<commit>",215        help="revision from which to compute the diff",216    )217    p.add_argument(218        "ignored",219        nargs="*",220        metavar="<file>...",221        help="if specified, only consider differences in these files",222    )223    opts = p.parse_args(argv)224 225    opts.verbose -= opts.quiet226    del opts.quiet227 228    commits, files = interpret_args(opts.args, dash_dash, opts.commit)229    if len(commits) > 2:230        die("at most two commits allowed; %d given" % len(commits))231    if len(commits) == 2:232        if opts.staged:233            die("--staged is not allowed when two commits are given")234        if not opts.diff:235            die("--diff is required when two commits are given")236    elif opts.diff_from_common_commit:237        die("--diff_from_common_commit is only allowed when two commits are given")238 239    if os.path.dirname(opts.binary):240        opts.binary = os.path.abspath(opts.binary)241 242    changed_lines = compute_diff_and_extract_lines(243        commits, files, opts.staged, opts.diff_from_common_commit244    )245    if opts.verbose >= 1:246        ignored_files = set(changed_lines)247    filter_by_extension(changed_lines, opts.extensions.lower().split(","))248    # The computed diff outputs absolute paths, so we must cd before accessing249    # those files.250    cd_to_toplevel()251    filter_symlinks(changed_lines)252    filter_ignored_files(changed_lines, binary=opts.binary)253    if opts.verbose >= 1:254        ignored_files.difference_update(changed_lines)255        if ignored_files:256            print(257                "Ignoring the following files (wrong extension, symlink, or "258                "ignored by clang-format):"259            )260            for filename in ignored_files:261                print("    %s" % filename)262        if changed_lines:263            print("Running clang-format on the following files:")264            for filename in changed_lines:265                print("    %s" % filename)266 267    if not changed_lines:268        if opts.verbose >= 0:269            print("no modified files to format")270        return 0271 272    if len(commits) > 1:273        old_tree = commits[1]274        revision = old_tree275    elif opts.staged:276        old_tree = create_tree_from_index(changed_lines)277        revision = ""278    else:279        old_tree = create_tree_from_workdir(changed_lines)280        revision = None281    new_tree = run_clang_format_and_save_to_tree(282        changed_lines, revision, binary=opts.binary, style=opts.style283    )284    if opts.verbose >= 1:285        print("old tree: %s" % old_tree)286        print("new tree: %s" % new_tree)287 288    if old_tree == new_tree:289        if opts.verbose >= 0:290            print("clang-format did not modify any files")291        return 0292 293    if opts.diff:294        return print_diff(old_tree, new_tree)295    if opts.diffstat:296        return print_diffstat(old_tree, new_tree)297 298    changed_files = apply_changes(299        old_tree, new_tree, force=opts.force, patch_mode=opts.patch300    )301    if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1:302        print("changed files:")303        for filename in changed_files:304            print("    %s" % filename)305 306    return 1307 308 309def load_git_config(non_string_options=None):310    """Return the git configuration as a dictionary.311 312    All options are assumed to be strings unless in `non_string_options`, in313    which is a dictionary mapping option name (in lower case) to either "--bool"314    or "--int"."""315    if non_string_options is None:316        non_string_options = {}317    out = {}318    for entry in run("git", "config", "--list", "--null").split("\0"):319        if entry:320            if "\n" in entry:321                name, value = entry.split("\n", 1)322            else:323                # A setting with no '=' ('\n' with --null) is implicitly 'true'324                name = entry325                value = "true"326            if name in non_string_options:327                value = run("git", "config", non_string_options[name], name)328            out[name] = value329    return out330 331 332def interpret_args(args, dash_dash, default_commit):333    """Interpret `args` as "[commits] [--] [files]" and return (commits, files).334 335    It is assumed that "--" and everything that follows has been removed from336    args and placed in `dash_dash`.337 338    If "--" is present (i.e., `dash_dash` is non-empty), the arguments to its339    left (if present) are taken as commits.  Otherwise, the arguments are340    checked from left to right if they are commits or files.  If commits are not341    given, a list with `default_commit` is used."""342    if dash_dash:343        if len(args) == 0:344            commits = [default_commit]345        else:346            commits = args347        for commit in commits:348            object_type = get_object_type(commit)349            if object_type not in ("commit", "tag"):350                if object_type is None:351                    die("'%s' is not a commit" % commit)352                else:353                    die(354                        "'%s' is a %s, but a commit was expected"355                        % (commit, object_type)356                    )357        files = dash_dash[1:]358    elif args:359        commits = []360        while args:361            if not disambiguate_revision(args[0]):362                break363            commits.append(args.pop(0))364        if not commits:365            commits = [default_commit]366        files = args367    else:368        commits = [default_commit]369        files = []370    return commits, files371 372 373def disambiguate_revision(value):374    """Returns True if `value` is a revision, False if it is a file, or dies."""375    # If `value` is ambiguous (neither a commit nor a file), the following376    # command will die with an appropriate error message.377    run("git", "rev-parse", value, verbose=False)378    object_type = get_object_type(value)379    if object_type is None:380        return False381    if object_type in ("commit", "tag"):382        return True383    die("`%s` is a %s, but a commit or filename was expected" % (value, object_type))384 385 386def get_object_type(value):387    """Returns a string description of an object's type, or None if it is not388    a valid git object."""389    cmd = ["git", "cat-file", "-t", value]390    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)391    stdout, stderr = p.communicate()392    if p.returncode != 0:393        return None394    return convert_string(stdout.strip())395 396 397def compute_diff_and_extract_lines(commits, files, staged, diff_common_commit):398    """Calls compute_diff() followed by extract_lines()."""399    diff_process = compute_diff(commits, files, staged, diff_common_commit)400    changed_lines = extract_lines(diff_process.stdout)401    diff_process.stdout.close()402    diff_process.wait()403    if diff_process.returncode != 0:404        # Assume error was already printed to stderr.405        sys.exit(2)406    return changed_lines407 408 409def compute_diff(commits, files, staged, diff_common_commit):410    """Return a subprocess object producing the diff from `commits`.411 412    The return value's `stdin` file object will produce a patch with the413    differences between the working directory (or stage if --staged is used) and414    the first commit if a single one was specified, or the difference between415    both specified commits, filtered on `files` (if non-empty).416    Zero context lines are used in the patch."""417    git_tool = "diff-index"418    extra_args = []419    if len(commits) == 2:420        git_tool = "diff-tree"421        if diff_common_commit:422            extra_args += ["--merge-base"]423    elif staged:424        extra_args += ["--cached"]425 426    cmd = ["git", git_tool, "-p", "-U0"] + extra_args + commits + ["--"]427    cmd.extend(files)428    p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)429    p.stdin.close()430    return p431 432 433def extract_lines(patch_file):434    """Extract the changed lines in `patch_file`.435 436    The return value is a dictionary mapping filename to a list of (start_line,437    line_count) pairs.438 439    The input must have been produced with ``-U0``, meaning unidiff format with440    zero lines of context.  The return value is a dict mapping filename to a441    list of line `Range`s."""442    matches = {}443    for line in patch_file:444        line = convert_string(line)445        match = re.search(r"^\+\+\+\ [^/]+/(.*)", line)446        if match:447            filename = match.group(1).rstrip("\r\n\t")448        match = re.search(r"^@@ -[0-9,]+ \+(\d+)(,(\d+))?", line)449        if match:450            start_line = int(match.group(1))451            line_count = 1452            if match.group(3):453                line_count = int(match.group(3))454            if line_count == 0:455                line_count = 1456            if start_line == 0:457                continue458            matches.setdefault(filename, []).append(Range(start_line, line_count))459    return matches460 461 462def filter_by_extension(dictionary, allowed_extensions):463    """Delete every key in `dictionary` that doesn't have an allowed extension.464 465    `allowed_extensions` must be a collection of lowercase file extensions,466    excluding the period."""467    allowed_extensions = frozenset(allowed_extensions)468    for filename in list(dictionary.keys()):469        base_ext = filename.rsplit(".", 1)470        if len(base_ext) == 1 and "" in allowed_extensions:471            continue472        if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions:473            del dictionary[filename]474 475 476def filter_symlinks(dictionary):477    """Delete every key in `dictionary` that is a symlink."""478    for filename in list(dictionary.keys()):479        if os.path.islink(filename):480            del dictionary[filename]481 482 483def filter_ignored_files(dictionary, binary):484    """Delete every key in `dictionary` that is ignored by clang-format."""485    ignored_files = run(binary, "-list-ignored", *dictionary.keys())486    if not ignored_files:487        return488    ignored_files = ignored_files.split("\n")489    for filename in ignored_files:490        del dictionary[filename]491 492 493def cd_to_toplevel():494    """Change to the top level of the git repository."""495    toplevel = run("git", "rev-parse", "--show-toplevel")496    os.chdir(toplevel)497 498 499def create_tree_from_workdir(filenames):500    """Create a new git tree with the given files from the working directory.501 502    Returns the object ID (SHA-1) of the created tree."""503    return create_tree(filenames, "--stdin")504 505 506def create_tree_from_index(filenames):507    # Copy the environment, because the files have to be read from the original508    # index.509    env = os.environ.copy()510 511    def index_contents_generator():512        for filename in filenames:513            git_ls_files_cmd = [514                "git",515                "ls-files",516                "--stage",517                "-z",518                "--",519                filename,520            ]521            git_ls_files = subprocess.Popen(522                git_ls_files_cmd,523                env=env,524                stdin=subprocess.PIPE,525                stdout=subprocess.PIPE,526            )527            stdout = git_ls_files.communicate()[0]528            yield convert_string(stdout.split(b"\0")[0])529 530    return create_tree(index_contents_generator(), "--index-info")531 532 533def run_clang_format_and_save_to_tree(534    changed_lines, revision=None, binary="clang-format", style=None535):536    """Run clang-format on each file and save the result to a git tree.537 538    Returns the object ID (SHA-1) of the created tree."""539    # Copy the environment when formatting the files in the index, because the540    # files have to be read from the original index.541    env = os.environ.copy() if revision == "" else None542 543    def iteritems(container):544        try:545            return container.iteritems()  # Python 2546        except AttributeError:547            return container.items()  # Python 3548 549    def index_info_generator():550        for filename, line_ranges in iteritems(changed_lines):551            if revision is not None:552                if len(revision) > 0:553                    git_metadata_cmd = [554                        "git",555                        "ls-tree",556                        "%s:%s" % (revision, os.path.dirname(filename)),557                        os.path.basename(filename),558                    ]559                else:560                    git_metadata_cmd = [561                        "git",562                        "ls-files",563                        "--stage",564                        "--",565                        filename,566                    ]567                git_metadata = subprocess.Popen(568                    git_metadata_cmd,569                    env=env,570                    stdin=subprocess.PIPE,571                    stdout=subprocess.PIPE,572                )573                stdout = git_metadata.communicate()[0]574                mode = oct(int(stdout.split()[0], 8))575            else:576                mode = oct(os.stat(filename).st_mode)577            # Adjust python3 octal format so that it matches what git expects578            if mode.startswith("0o"):579                mode = "0" + mode[2:]580            blob_id = clang_format_to_blob(581                filename,582                line_ranges,583                revision=revision,584                binary=binary,585                style=style,586                env=env,587            )588            yield "%s %s\t%s" % (mode, blob_id, filename)589 590    return create_tree(index_info_generator(), "--index-info")591 592 593def create_tree(input_lines, mode):594    """Create a tree object from the given input.595 596    If mode is '--stdin', it must be a list of filenames.  If mode is597    '--index-info' is must be a list of values suitable for "git update-index598    --index-info", such as "<mode> <SP> <sha1> <TAB> <filename>".  Any other599    mode is invalid."""600    assert mode in ("--stdin", "--index-info")601    cmd = ["git", "update-index", "--add", "-z", mode]602    with temporary_index_file():603        p = subprocess.Popen(cmd, stdin=subprocess.PIPE)604        for line in input_lines:605            p.stdin.write(to_bytes("%s\0" % line))606        p.stdin.close()607        if p.wait() != 0:608            die("`%s` failed" % " ".join(cmd))609        tree_id = run("git", "write-tree")610        return tree_id611 612 613def clang_format_to_blob(614    filename,615    line_ranges,616    revision=None,617    binary="clang-format",618    style=None,619    env=None,620):621    """Run clang-format on the given file and save the result to a git blob.622 623    Runs on the file in `revision` if not None, or on the file in the working624    directory if `revision` is None. Revision can be set to an empty string to625    run clang-format on the file in the index.626 627    Returns the object ID (SHA-1) of the created blob."""628    clang_format_cmd = [binary]629    if style:630        clang_format_cmd.extend(["--style=" + style])631    clang_format_cmd.extend(632        [633            "--lines=%s:%s" % (start_line, start_line + line_count - 1)634            for start_line, line_count in line_ranges635        ]636    )637    if revision is not None:638        clang_format_cmd.extend(["--assume-filename=" + filename])639        git_show_cmd = [640            "git",641            "cat-file",642            "blob",643            "%s:%s" % (revision, filename),644        ]645        git_show = subprocess.Popen(646            git_show_cmd, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE647        )648        git_show.stdin.close()649        clang_format_stdin = git_show.stdout650    else:651        clang_format_cmd.extend([filename])652        git_show = None653        clang_format_stdin = subprocess.PIPE654    try:655        clang_format = subprocess.Popen(656            clang_format_cmd, stdin=clang_format_stdin, stdout=subprocess.PIPE657        )658        if clang_format_stdin == subprocess.PIPE:659            clang_format_stdin = clang_format.stdin660    except OSError as e:661        if e.errno == errno.ENOENT:662            die('cannot find executable "%s"' % binary)663        else:664            raise665    clang_format_stdin.close()666    hash_object_cmd = [667        "git",668        "hash-object",669        "-w",670        "--path=" + filename,671        "--stdin",672    ]673    hash_object = subprocess.Popen(674        hash_object_cmd, stdin=clang_format.stdout, stdout=subprocess.PIPE675    )676    clang_format.stdout.close()677    stdout = hash_object.communicate()[0]678    if hash_object.returncode != 0:679        die("`%s` failed" % " ".join(hash_object_cmd))680    if clang_format.wait() != 0:681        die("`%s` failed" % " ".join(clang_format_cmd))682    if git_show and git_show.wait() != 0:683        die("`%s` failed" % " ".join(git_show_cmd))684    return convert_string(stdout).rstrip("\r\n")685 686 687@contextlib.contextmanager688def temporary_index_file(tree=None):689    """Context manager for setting GIT_INDEX_FILE to a temporary file and690    deleting the file afterward."""691    index_path = create_temporary_index(tree)692    old_index_path = os.environ.get("GIT_INDEX_FILE")693    os.environ["GIT_INDEX_FILE"] = index_path694    try:695        yield696    finally:697        if old_index_path is None:698            del os.environ["GIT_INDEX_FILE"]699        else:700            os.environ["GIT_INDEX_FILE"] = old_index_path701        os.remove(index_path)702 703 704def create_temporary_index(tree=None):705    """Create a temporary index file and return the created file's path.706 707    If `tree` is not None, use that as the tree to read in.  Otherwise, an708    empty index is created."""709    gitdir = run("git", "rev-parse", "--git-dir")710    path = os.path.join(gitdir, temp_index_basename)711    if tree is None:712        tree = "--empty"713    run("git", "read-tree", "--index-output=" + path, tree)714    return path715 716 717def print_diff(old_tree, new_tree):718    """Print the diff between the two trees to stdout."""719    # We use the porcelain 'diff' and not plumbing 'diff-tree' because the720    # output is expected to be viewed by the user, and only the former does nice721    # things like color and pagination.722    #723    # We also only print modified files since `new_tree` only contains the files724    # that were modified, so unmodified files would show as deleted without the725    # filter.726    return subprocess.run(727        ["git", "diff", "--diff-filter=M", "--exit-code", old_tree, new_tree]728    ).returncode729 730 731def print_diffstat(old_tree, new_tree):732    """Print the diffstat between the two trees to stdout."""733    # We use the porcelain 'diff' and not plumbing 'diff-tree' because the734    # output is expected to be viewed by the user, and only the former does nice735    # things like color and pagination.736    #737    # We also only print modified files since `new_tree` only contains the files738    # that were modified, so unmodified files would show as deleted without the739    # filter.740    return subprocess.run(741        [742            "git",743            "diff",744            "--diff-filter=M",745            "--exit-code",746            "--stat",747            old_tree,748            new_tree,749        ]750    ).returncode751 752 753def apply_changes(old_tree, new_tree, force=False, patch_mode=False):754    """Apply the changes in `new_tree` to the working directory.755 756    Bails if there are local changes in those files and not `force`.  If757    `patch_mode`, runs `git checkout --patch` to select hunks interactively."""758    changed_files = (759        run(760            "git",761            "diff-tree",762            "--diff-filter=M",763            "-r",764            "-z",765            "--name-only",766            old_tree,767            new_tree,768        )769        .rstrip("\0")770        .split("\0")771    )772    if not force:773        unstaged_files = run("git", "diff-files", "--name-status", *changed_files)774        if unstaged_files:775            print(776                "The following files would be modified but have unstaged changes:",777                file=sys.stderr,778            )779            print(unstaged_files, file=sys.stderr)780            print("Please commit, stage, or stash them first.", file=sys.stderr)781            sys.exit(2)782    if patch_mode:783        # In patch mode, we could just as well create an index from the new tree784        # and checkout from that, but then the user will be presented with a785        # message saying "Discard ... from worktree".  Instead, we use the old786        # tree as the index and checkout from new_tree, which gives the slightly787        # better message, "Apply ... to index and worktree".  This is not quite788        # right, since it won't be applied to the user's index, but oh well.789        with temporary_index_file(old_tree):790            subprocess.run(["git", "checkout", "--patch", new_tree], check=True)791        index_tree = old_tree792    else:793        with temporary_index_file(new_tree):794            run("git", "checkout-index", "-f", "--", *changed_files)795    return changed_files796 797 798def run(*args, **kwargs):799    stdin = kwargs.pop("stdin", "")800    verbose = kwargs.pop("verbose", True)801    strip = kwargs.pop("strip", True)802    for name in kwargs:803        raise TypeError("run() got an unexpected keyword argument '%s'" % name)804    p = subprocess.Popen(805        args,806        stdout=subprocess.PIPE,807        stderr=subprocess.PIPE,808        stdin=subprocess.PIPE,809    )810    stdout, stderr = p.communicate(input=stdin)811 812    stdout = convert_string(stdout)813    stderr = convert_string(stderr)814 815    if p.returncode == 0:816        if stderr:817            if verbose:818                print("`%s` printed to stderr:" % " ".join(args), file=sys.stderr)819            print(stderr.rstrip(), file=sys.stderr)820        if strip:821            stdout = stdout.rstrip("\r\n")822        return stdout823    if verbose:824        print("`%s` returned %s" % (" ".join(args), p.returncode), file=sys.stderr)825    if stderr:826        print(stderr.rstrip(), file=sys.stderr)827    sys.exit(2)828 829 830def die(message):831    print("error:", message, file=sys.stderr)832    sys.exit(2)833 834 835def to_bytes(str_input):836    # Encode to UTF-8 to get binary data.837    if isinstance(str_input, bytes):838        return str_input839    return str_input.encode("utf-8")840 841 842def to_string(bytes_input):843    if isinstance(bytes_input, str):844        return bytes_input845    return bytes_input.encode("utf-8")846 847 848def convert_string(bytes_input):849    try:850        return to_string(bytes_input.decode("utf-8"))851    except AttributeError:  # 'str' object has no attribute 'decode'.852        return str(bytes_input)853    except UnicodeError:854        return str(bytes_input)855 856 857if __name__ == "__main__":858    sys.exit(main())859