| # 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']) |