| # DExTer : Debugging Experience Tester |
| # ~~~~~~ ~ ~~ ~ ~~ |
| # |
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| # See https://llvm.org/LICENSE.txt for license information. |
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| """Extended Argument Parser. Extends the argparse module with some extra |
| functionality, to hopefully aid user-friendliness. |
| """ |
| |
| import argparse |
| import difflib |
| import unittest |
| |
| from dex.utils import PrettyOutput |
| from dex.utils.Exceptions import Error |
| |
| # re-export all of argparse |
| for argitem in argparse.__all__: |
| vars()[argitem] = getattr(argparse, argitem) |
| |
| |
| def _did_you_mean(val, possibles): |
| close_matches = difflib.get_close_matches(val, possibles) |
| did_you_mean = "" |
| if close_matches: |
| did_you_mean = "did you mean {}?".format( |
| " or ".join("<y>'{}'</>".format(c) for c in close_matches[:2]) |
| ) |
| return did_you_mean |
| |
| |
| def _colorize(message): |
| lines = message.splitlines() |
| for i, line in enumerate(lines): |
| lines[i] = lines[i].replace("usage:", "<g>usage:</>") |
| if line.endswith(":"): |
| lines[i] = "<g>{}</>".format(line) |
| return "\n".join(lines) |
| |
| |
| class ExtArgumentParser(argparse.ArgumentParser): |
| def error(self, message): |
| """Use the Dexception Error mechanism (including auto-colored output).""" |
| raise Error("{}\n\n{}".format(message, self.format_usage())) |
| |
| # pylint: disable=redefined-builtin |
| def _print_message(self, message, file=None): |
| if message: |
| if file and file.name == "<stdout>": |
| file = PrettyOutput.stdout |
| else: |
| file = PrettyOutput.stderr |
| |
| self.context.o.auto(message, file) |
| |
| # pylint: enable=redefined-builtin |
| |
| def format_usage(self): |
| return _colorize(super(ExtArgumentParser, self).format_usage()) |
| |
| def format_help(self): |
| return _colorize(super(ExtArgumentParser, self).format_help() + "\n\n") |
| |
| @property |
| def _valid_visible_options(self): |
| """A list of all non-suppressed command line flags.""" |
| return [ |
| item |
| for sublist in vars(self)["_actions"] |
| for item in sublist.option_strings |
| if sublist.help != argparse.SUPPRESS |
| ] |
| |
| def parse_args(self, args=None, namespace=None): |
| """Add 'did you mean' output to errors.""" |
| args, argv = self.parse_known_args(args, namespace) |
| if argv: |
| errors = [] |
| for arg in argv: |
| if arg in self._valid_visible_options: |
| error = "unexpected argument: <y>'{}'</>".format(arg) |
| else: |
| error = "unrecognized argument: <y>'{}'</>".format(arg) |
| dym = _did_you_mean(arg, self._valid_visible_options) |
| if dym: |
| error += " ({})".format(dym) |
| errors.append(error) |
| self.error("\n ".join(errors)) |
| |
| return args |
| |
| def add_argument(self, *args, **kwargs): |
| """Automatically add the default value to help text.""" |
| if "default" in kwargs: |
| default = kwargs["default"] |
| if default is None: |
| default = kwargs.pop("display_default", None) |
| |
| if ( |
| default |
| and isinstance(default, (str, int, float)) |
| and default != argparse.SUPPRESS |
| ): |
| assert ( |
| "choices" not in kwargs or default in kwargs["choices"] |
| ), "default value '{}' is not one of allowed choices: {}".format( |
| default, kwargs["choices"] |
| ) |
| if "help" in kwargs and kwargs["help"] != argparse.SUPPRESS: |
| assert isinstance(kwargs["help"], str), type(kwargs["help"]) |
| kwargs["help"] = "{} (default:{})".format(kwargs["help"], default) |
| |
| super(ExtArgumentParser, self).add_argument(*args, **kwargs) |
| |
| def __init__(self, context, *args, **kwargs): |
| self.context = context |
| super(ExtArgumentParser, self).__init__(*args, **kwargs) |
| |
| |
| class TestExtArgumentParser(unittest.TestCase): |
| def test_did_you_mean(self): |
| parser = ExtArgumentParser(None) |
| parser.add_argument("--foo") |
| parser.add_argument("--qoo", help=argparse.SUPPRESS) |
| parser.add_argument("jam", nargs="?") |
| |
| parser.parse_args(["--foo", "0"]) |
| |
| expected = ( |
| r"^unrecognized argument\: <y>'\-\-doo'</>\s+" |
| r"\(did you mean <y>'\-\-foo'</>\?\)\n" |
| r"\s*<g>usage:</>" |
| ) |
| with self.assertRaisesRegex(Error, expected): |
| parser.parse_args(["--doo"]) |
| |
| parser.add_argument("--noo") |
| |
| expected = ( |
| r"^unrecognized argument\: <y>'\-\-doo'</>\s+" |
| r"\(did you mean <y>'\-\-noo'</> or <y>'\-\-foo'</>\?\)\n" |
| r"\s*<g>usage:</>" |
| ) |
| with self.assertRaisesRegex(Error, expected): |
| parser.parse_args(["--doo"]) |
| |
| expected = r"^unrecognized argument\: <y>'\-\-bar'</>\n" r"\s*<g>usage:</>" |
| with self.assertRaisesRegex(Error, expected): |
| parser.parse_args(["--bar"]) |
| |
| expected = r"^unexpected argument\: <y>'\-\-foo'</>\n" r"\s*<g>usage:</>" |
| with self.assertRaisesRegex(Error, expected): |
| parser.parse_args(["--", "x", "--foo"]) |