class CLITestCase(TestCase): def setUp(self): self.cli = CLI() self.cli.logger = MagicMock() def test_init_with_colorlog(self): with patch('clinner.cli._colorlog', True), patch('colorlog.StreamHandler') as handler_mock: CLI() self.assertTrue(handler_mock.call_count, 1) self.assertTrue(handler_mock.return_value.setFormatter.call_count, 1) def test_init_without_colorlog(self): with patch('clinner.cli._colorlog', False), patch( 'clinner.cli.logging.StreamHandler') as handler_mock: CLI() self.assertTrue(handler_mock.call_count, 1) def test_disable(self): self.cli.disable() self.assertEqual(self.cli.logger.removeHandler.call_count, 1) def test_enable(self): self.cli.enable() self.assertEqual(self.cli.logger.addHandler.call_count, 1) def test_print_return_ok(self): self.cli.print_return(0) expected_calls = [call(logging.INFO, 'Return code: %d', 0)] self.assertCountEqual(self.cli.logger.log.call_args_list, expected_calls) def test_print_return_error(self): self.cli.print_return(1) expected_calls = [call(logging.ERROR, 'Return code: %d', 1)] self.assertCountEqual(self.cli.logger.log.call_args_list, expected_calls) def test_print_header(self): self.cli.print_header(foo=True, bar=1) msg = self.cli.logger.info.call_args[0][0] self.assertIn('Foo: True', msg) self.assertIn('Bar: 1', msg) def tearDown(self): pass
class BaseMain(metaclass=MainMeta): commands = [] description = None def __init__(self, args=None, parse_args=True): self.args, self.unknown_args = argparse.Namespace(), [] self.cli = CLI() if parse_args: self.args, self.unknown_args = self.parse_arguments(args=args) # Set logging verbosity if self.args.quiet: self.cli.disable() elif self.args.verbose == 1: self.cli.set_level(logging.INFO) elif self.args.verbose >= 2: self.cli.set_level(logging.DEBUG) else: # Default log level self.cli.set_level(logging.WARNING) # Inject parameters related to current stage as environment variables self.inject() # Get settings from args or envvar self.settings = self.args.settings or os.environ.get( "CLINNER_SETTINGS") # Load settings settings.build_from_module(self.settings) def _commands_arguments(self, parser: "argparse.ArgumentParser", parser_class=None): """ Add arguments for each command to parser. :param parser: Parser """ # Create subparser for each command subparsers_kwargs = { "parser_class": lambda **kwargs: parser_class(self, **kwargs) } if parser_class else {} subparsers = parser.add_subparsers(title="Commands", dest="command", **subparsers_kwargs) subparsers.required = True cmds = self._commands if self._commands is not None else command.register for cmd_name, cmd in cmds.items(): subparser_opts = cmd["parser"] if cmd["type"] == Type.SHELL: subparser_opts["add_help"] = False p = subparsers.add_parser(cmd_name, **subparser_opts) if callable(cmd["arguments"]): cmd["arguments"](p) else: for argument in cmd["arguments"]: try: if len(argument) == 2: args, kwargs = argument elif len(argument) == 1: args = argument[0] kwargs = {} else: args, kwargs = None, None assert isinstance(args, (tuple, list)) assert isinstance(kwargs, dict) except AssertionError: raise CommandArgParseError(str(argument)) else: p.add_argument(*args, **kwargs) @abstractmethod def add_arguments(self, parser: "argparse.ArgumentParser"): """ Add to parser all necessary arguments for this Main. :param parser: Argument parser. """ pass def parse_arguments(self, args=None, parser=None, parser_class=None): """ command Line application arguments. """ if parser is None: parser = argparse.ArgumentParser(description=self.description, conflict_handler="resolve") # Call inner method that adds arguments from all classes (defined in metaclass) self._add_arguments(parser, parser_class) return parser.parse_known_args(args=args) def run_python(self, cmd, *args, **kwargs): """ Run a python command in a different process. :param cmd: Python command. :param args: List of args passed to Process. :param kwargs: Dict of kwargs passed to Process. :return: Command return code. """ self.cli.logger.debug("- [python] %s.%s", str(cmd.__module__), str(cmd.__qualname__)) result = 0 if not getattr(self.args, "dry_run", False): # Run command if asyncio.iscoroutinefunction(cmd.func.func): result = asyncio.get_event_loop().run_until_complete( cmd(*args, **kwargs)) else: result = cmd(*args, **kwargs) return result def run_shell(self, cmd, *args, **kwargs): """ Run a shell command in a different process. :param cmd: Shell command. :param args: List of args passed to Popen. :param kwargs: Dict of kwargs passed to Popen. :return: Command return code. """ self.cli.logger.info("[shell] %s", " ".join(cmd)) result = 0 if not getattr(self.args, "dry_run", False): # Run command p = Popen(args=cmd, *args, **kwargs) while p.returncode is None: # pragma: no cover try: p.wait() except KeyboardInterrupt: self.cli.logger.info( "Soft quit signal received, waiting the process to stop" ) p.send_signal(signal.SIGINT) try: p.wait() except KeyboardInterrupt: self.cli.logger.info( "Hard quit signal received, killing the process immediately" ) p.send_signal(signal.SIGKILL) p.wait(timeout=3) result = p.returncode return result def run_command(self, input_command, *args, **kwargs): """ Run the given command, building it with arguments. :param input_command: Command to execute. :param args: List of args passed to run_<type> command. :param kwargs: Dict of kwargs passed to run_<type> command. :return: Command return code. """ # Print header self.cli.print_header(input_command, **kwargs) # Get list of commands commands, command_type = Builder.build_command(input_command, *args, **kwargs) # Print command list self.cli.print_commands_list(commands, command_type) return_code = 0 for c in commands: if command_type == Type.PYTHON: return_code = self.run_python(c) elif command_type in (Type.SHELL, Type.SHELL_WITH_HELP): return_code = self.run_shell(c) else: # pragma: no cover raise CommandTypeError(command_type) self.cli.print_return(return_code) # Break on non-zero exit code. if return_code != 0: return return_code return return_code @abstractmethod def run(self, *args, **kwargs): pass