From 637dcf054ebd3720ba16d8c8ea4ee358a95c43da Mon Sep 17 00:00:00 2001 From: czoido Date: Tue, 30 Jun 2020 09:34:05 +0200 Subject: [PATCH 01/20] add management for commands 2.0 --- conans/cli/command.py | 297 ++++++++++++++++++++++++++++++++++ conans/cli/commands/help.py | 56 +++++++ conans/cli/commands/search.py | 58 +++++++ conans/conan.py | 6 +- 4 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 conans/cli/command.py create mode 100644 conans/cli/commands/help.py create mode 100644 conans/cli/commands/search.py diff --git a/conans/cli/command.py b/conans/cli/command.py new file mode 100644 index 00000000000..17357048f50 --- /dev/null +++ b/conans/cli/command.py @@ -0,0 +1,297 @@ +import argparse +import os +import signal +import sys +import textwrap +from collections import defaultdict +from difflib import get_close_matches +import importlib +import pkgutil + +from conans import __version__ as client_version +from conans.util.env_reader import get_env +from conans.client.conan_api import Conan +from conans.errors import ConanException, ConanInvalidConfiguration, ConanMigrationError +from conans.util.files import exception_message_safe +from conans.util.log import logger + +# Exit codes for conan command: +SUCCESS = 0 # 0: Success (done) +ERROR_GENERAL = 1 # 1: General ConanException error (done) +ERROR_MIGRATION = 2 # 2: Migration error +USER_CTRL_C = 3 # 3: Ctrl+C +USER_CTRL_BREAK = 4 # 4: Ctrl+Break +ERROR_SIGTERM = 5 # 5: SIGTERM +ERROR_INVALID_CONFIGURATION = 6 # 6: Invalid configuration (done) + + +class Extender(argparse.Action): + """Allows using the same flag several times in command and creates a list with the values. + For example: + conan install MyPackage/1.2@user/channel -o qt:value -o mode:2 -s cucumber:true + It creates: + options = ['qt:value', 'mode:2'] + settings = ['cucumber:true'] + """ + + def __call__(self, parser, namespace, values, option_strings=None): # @UnusedVariable + # Need None here incase `argparse.SUPPRESS` was supplied for `dest` + dest = getattr(namespace, self.dest, None) + if not hasattr(dest, 'extend') or dest == self.default: + dest = [] + setattr(namespace, self.dest, dest) + # if default isn't set to None, this method might be called + # with the default as `values` for other arguments which + # share this destination. + parser.set_defaults(**{self.dest: None}) + + if isinstance(values, str): + dest.append(values) + elif values: + try: + dest.extend(values) + except ValueError: + dest.append(values) + + +class OnceArgument(argparse.Action): + """Allows declaring a parameter that can have only one value, by default argparse takes the + latest declared and it's very confusing. + """ + + def __call__(self, parser, namespace, values, option_string=None): + if getattr(namespace, self.dest) is not None and self.default is None: + msg = '{o} can only be specified once'.format(o=option_string) + raise argparse.ArgumentError(None, msg) + setattr(namespace, self.dest, values) + + +class SmartFormatter(argparse.HelpFormatter): + + def _fill_text(self, text, width, indent): + text = textwrap.dedent(text) + return ''.join(indent + line for line in text.splitlines(True)) + + +class ConanCommand(object): + def __init__(self, method, group=None, **kwargs): + self._formatters = {} + for kind, action in kwargs.items(): + if callable(action): + self._formatters[kind] = action + self._group = group or "Misc commands" + self._name = method.__name__.replace("_", "-") + self._method = method + self._doc = method.__doc__ or "Empty description" + self._parser = argparse.ArgumentParser(description=self._doc, + prog="conan {}".format(self._name), + formatter_class=SmartFormatter) + + def run(self, *args, **kwargs): + conan_api = kwargs["conan_api"] + info, formatter = self._method(*args, **kwargs) + if info: + self._formatters[formatter](info, conan_api.out) + + @property + def group(self): + return self._group + + @property + def name(self): + return self._name + + @property + def method(self): + return self._method + + @property + def doc(self): + return self._doc + + @property + def parser(self): + return self._parser + + +def conan_command(**kwargs): + def decorator(f): + cmd = ConanCommand(f, **kwargs) + return cmd + + return decorator + + +class Command(object): + """A single command of the conan application, with all the first level commands. Manages the + parsing of parameters and delegates functionality to the conan python api. It can also show the + help of the tool. + """ + + def __init__(self, conan_api): + assert isinstance(conan_api, Conan), "Expected 'Conan' type, got '{}'".format( + type(conan_api)) + self._conan = conan_api + self._out = conan_api.out + self._groups = defaultdict(list) + self._commands = None + + def _add_command(self, import_path, method_name): + try: + command_wrapper = getattr(importlib.import_module(import_path), method_name) + if command_wrapper.doc: + self._commands[command_wrapper.name] = command_wrapper + self._groups[command_wrapper.group].append(command_wrapper.name) + except AttributeError: + raise ConanException("There is no {} method defined in {}".format(method_name, + import_path)) + + @property + def conan_api(self): + return self._conan + + @property + def commands(self): + if self._commands is None: + self._commands = {} + conan_commands_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "commands") + for module in pkgutil.iter_modules([conan_commands_path]): + self._add_command("conans.cli.commands.{}".format(module.name), module.name) + if get_env("CONAN_USER_COMMANDS", default=False): + user_commands_path = os.path.join(self._conan.cache_folder, "commands") + sys.path.append(user_commands_path) + for module in pkgutil.iter_modules([user_commands_path]): + if module.name.startswith("cmd_"): + self._add_command(module.name, module.name.replace("cmd_", "")) + return self._commands + + @property + def groups(self): + return self._groups + + def _print_similar(self, command): + """ Looks for similar commands and prints them if found. + """ + matches = get_close_matches( + word=command, possibilities=self.commands.keys(), n=5, cutoff=0.75) + + if len(matches) == 0: + return + + if len(matches) > 1: + self._out.writeln("The most similar commands are") + else: + self._out.writeln("The most similar command is") + + for match in matches: + self._out.writeln(" %s" % match) + + self._out.writeln("") + + def help_message(self): + self.commands["help"].method(self.conan_api, self.commands, self.groups) + + def run(self, *args): + """ Entry point for executing commands, dispatcher to class + methods + """ + version = sys.version_info + if version.major == 2 or version.minor <= 4: + raise ConanException("Unsupported Python version") + + ret_code = SUCCESS + try: + try: + command_argument = args[0][0] + except IndexError: # No parameters + self.help_message() + return False + try: + command = self.commands[command_argument] + except KeyError as exc: + if command_argument in ["-v", "--version"]: + self._out.success("Conan version %s" % client_version) + return False + + if command_argument in ["-h", "--help"]: + self.help_message() + return False + + self._out.writeln( + "'%s' is not a Conan command. See 'conan --help'." % command_argument) + self._out.writeln("") + self._print_similar(command_argument) + raise ConanException("Unknown command %s" % str(exc)) + + command.run(args[0][1:], conan_api=self.conan_api, + parser=self.commands[command_argument].parser, + commands=self.commands, groups=self.groups) + + except KeyboardInterrupt as exc: + logger.error(exc) + ret_code = SUCCESS + except SystemExit as exc: + if exc.code != 0: + logger.error(exc) + self._out.error("Exiting with code: %d" % exc.code) + ret_code = exc.code + except ConanInvalidConfiguration as exc: + ret_code = ERROR_INVALID_CONFIGURATION + self._out.error(exc) + except ConanException as exc: + ret_code = ERROR_GENERAL + self._out.error(exc) + except Exception as exc: + import traceback + print(traceback.format_exc()) + ret_code = ERROR_GENERAL + msg = exception_message_safe(exc) + self._out.error(msg) + + return ret_code + + +def main(args): + """ main entry point of the conan application, using a Command to + parse parameters + + Exit codes for conan command: + + 0: Success (done) + 1: General ConanException error (done) + 2: Migration error + 3: Ctrl+C + 4: Ctrl+Break + 5: SIGTERM + 6: Invalid configuration (done) + """ + try: + conan_api, _, _ = Conan.factory() + except ConanMigrationError: # Error migrating + sys.exit(ERROR_MIGRATION) + except ConanException as e: + sys.stderr.write("Error in Conan initialization: {}".format(e)) + sys.exit(ERROR_GENERAL) + + def ctrl_c_handler(_, __): + print('You pressed Ctrl+C!') + sys.exit(USER_CTRL_C) + + def sigterm_handler(_, __): + print('Received SIGTERM!') + sys.exit(ERROR_SIGTERM) + + def ctrl_break_handler(_, __): + print('You pressed Ctrl+Break!') + sys.exit(USER_CTRL_BREAK) + + signal.signal(signal.SIGINT, ctrl_c_handler) + signal.signal(signal.SIGTERM, sigterm_handler) + + if sys.platform == 'win32': + signal.signal(signal.SIGBREAK, ctrl_break_handler) + + command = Command(conan_api) + error = command.run(args) + sys.exit(error) diff --git a/conans/cli/commands/help.py b/conans/cli/commands/help.py new file mode 100644 index 00000000000..114b916a613 --- /dev/null +++ b/conans/cli/commands/help.py @@ -0,0 +1,56 @@ +import textwrap + +from conans.client.output import Color +from conans.errors import ConanException +from conans.cli.command import conan_command + + +def output_help_cli(out, commands, groups): + """ + Prints a summary of all commands. + """ + max_len = max((len(c) for c in commands)) + 1 + fmt = ' %-{}s'.format(max_len) + + for group_name, comm_names in groups.items(): + out.writeln(group_name, Color.BRIGHT_MAGENTA) + for name in comm_names: + # future-proof way to ensure tabular formatting + out.write(fmt % name, Color.GREEN) + + # Help will be all the lines up to the first empty one + docstring_lines = commands[name].doc.split('\n') + start = False + data = [] + for line in docstring_lines: + line = line.strip() + if not line: + if start: + break + start = True + continue + data.append(line) + + txt = textwrap.fill(' '.join(data), 80, subsequent_indent=" " * (max_len + 2)) + out.writeln(txt) + + out.writeln("") + out.writeln('Conan commands. Type "conan -h" for help', Color.BRIGHT_YELLOW) + + +@conan_command(group="Misc commands", cli=output_help_cli) +def help(*args, conan_api, parser, commands, groups, **kwargs): + """ + Shows help for a specific command. + """ + + parser.add_argument("command", help='command', nargs="?") + args = parser.parse_args(*args) + if not args.command: + output_help_cli(conan_api.out, commands, groups) + return None, None + try: + commands[args.command].run(["--help"], parser=commands[args.command].parser, + conan_api=conan_api) + except KeyError: + raise ConanException("Unknown command '%s'" % args.command) diff --git a/conans/cli/commands/search.py b/conans/cli/commands/search.py new file mode 100644 index 00000000000..9836b22ab15 --- /dev/null +++ b/conans/cli/commands/search.py @@ -0,0 +1,58 @@ +import json + +from conans.client.output import Color +from conans.errors import ConanException +from conans.cli.command import OnceArgument, Extender, conan_command + + +def output_search_cli(info, out): + results = info["results"] + for remote_info in results: + source = "cache" if remote_info["remote"] is None else str(remote_info["remote"]) + out.writeln("{}:".format(source), Color.BRIGHT_WHITE) + for conan_item in remote_info["items"]: + reference = conan_item["recipe"]["id"] + out.writeln(" {}".format(reference)) + + +def output_search_json(info, out): + myjson = json.dumps(info["results"], indent=4) + out.writeln(myjson) + + +@conan_command(group="Consumer commands", cli=output_search_cli, json=output_search_json) +def search(*args, conan_api, parser, **kwargs): + """ + Searches for package recipes whose name contain in a remote or in the local cache + """ + + parser.add_argument('query', + help="Search query to find package recipe reference, e.g., 'boost', 'lib*'") + + exclusive_args = parser.add_mutually_exclusive_group() + exclusive_args.add_argument('-r', '--remote', default=None, action=Extender, nargs='?', + help="Remote to search. Accepts wildcards. To search in all remotes use *") + exclusive_args.add_argument('-c', '--cache', action="store_true", + help="Search in the local cache") + parser.add_argument('-o', '--output', default="cli", action=OnceArgument, + help="Select the output format: json, html,...") + args = parser.parse_args(*args) + + try: + def apiv2_search_recipes(query, remote_patterns=None, local_cache=False): + remote = None + if remote_patterns is not None and len(remote_patterns) > 0: + remote = remote_patterns[0].replace("*", "remote") + + search_results = {"results": [{"remote": remote, + "items": [{"recipe": {"id": "app/1.0"}}, + {"recipe": {"id": "liba/1.0"}}]}]} + return search_results + + remotes = args.remote or [] + info = apiv2_search_recipes(args.query, remote_patterns=remotes, local_cache=args.cache) + except ConanException as exc: + info = exc.info + raise + finally: + return info, args.output diff --git a/conans/conan.py b/conans/conan.py index 9111781e785..de96f2d0a5d 100755 --- a/conans/conan.py +++ b/conans/conan.py @@ -1,6 +1,10 @@ import sys +import os -from conans.client.command import main +if os.getenv("CONAN_V2_CLI"): + from conans.cli.command import main +else: + from conans.client.command import main def run(): From 64432ccfbe23e328036db2711ed841b8e4c90308 Mon Sep 17 00:00:00 2001 From: czoido Date: Mon, 6 Jul 2020 13:04:57 +0200 Subject: [PATCH 02/20] refactor cli --- conans/cli/cli.py | 248 ++++++++++++++++++++++++++++++++++ conans/cli/command.py | 247 +-------------------------------- conans/cli/commands/search.py | 3 +- conans/conan.py | 2 +- 4 files changed, 252 insertions(+), 248 deletions(-) create mode 100644 conans/cli/cli.py diff --git a/conans/cli/cli.py b/conans/cli/cli.py new file mode 100644 index 00000000000..47f2185f9d3 --- /dev/null +++ b/conans/cli/cli.py @@ -0,0 +1,248 @@ +import argparse +import os +import signal +import sys +import textwrap +from collections import defaultdict +from difflib import get_close_matches +import importlib +import pkgutil + +from conans import __version__ as client_version +from conans.util.env_reader import get_env +from conans.client.conan_api import Conan +from conans.errors import ConanException, ConanInvalidConfiguration, ConanMigrationError +from conans.util.files import exception_message_safe +from conans.util.log import logger + +# Exit codes for conan command: +SUCCESS = 0 # 0: Success (done) +ERROR_GENERAL = 1 # 1: General ConanException error (done) +ERROR_MIGRATION = 2 # 2: Migration error +USER_CTRL_C = 3 # 3: Ctrl+C +USER_CTRL_BREAK = 4 # 4: Ctrl+Break +ERROR_SIGTERM = 5 # 5: SIGTERM +ERROR_INVALID_CONFIGURATION = 6 # 6: Invalid configuration (done) + + +class Extender(argparse.Action): + """Allows using the same flag several times in command and creates a list with the values. + For example: + conan install MyPackage/1.2@user/channel -o qt:value -o mode:2 -s cucumber:true + It creates: + options = ['qt:value', 'mode:2'] + settings = ['cucumber:true'] + """ + + def __call__(self, parser, namespace, values, option_strings=None): # @UnusedVariable + # Need None here incase `argparse.SUPPRESS` was supplied for `dest` + dest = getattr(namespace, self.dest, None) + if not hasattr(dest, 'extend') or dest == self.default: + dest = [] + setattr(namespace, self.dest, dest) + # if default isn't set to None, this method might be called + # with the default as `values` for other arguments which + # share this destination. + parser.set_defaults(**{self.dest: None}) + + if isinstance(values, str): + dest.append(values) + elif values: + try: + dest.extend(values) + except ValueError: + dest.append(values) + + +class OnceArgument(argparse.Action): + """Allows declaring a parameter that can have only one value, by default argparse takes the + latest declared and it's very confusing. + """ + + def __call__(self, parser, namespace, values, option_string=None): + if getattr(namespace, self.dest) is not None and self.default is None: + msg = '{o} can only be specified once'.format(o=option_string) + raise argparse.ArgumentError(None, msg) + setattr(namespace, self.dest, values) + + +class SmartFormatter(argparse.HelpFormatter): + + def _fill_text(self, text, width, indent): + text = textwrap.dedent(text) + return ''.join(indent + line for line in text.splitlines(True)) + + +class Cli(object): + """A single command of the conan application, with all the first level commands. Manages the + parsing of parameters and delegates functionality to the conan python api. It can also show the + help of the tool. + """ + + def __init__(self, conan_api): + assert isinstance(conan_api, Conan), "Expected 'Conan' type, got '{}'".format( + type(conan_api)) + self._conan = conan_api + self._out = conan_api.out + self._groups = defaultdict(list) + self._commands = None + + def _add_command(self, import_path, method_name): + try: + command_wrapper = getattr(importlib.import_module(import_path), method_name) + if command_wrapper.doc: + self._commands[command_wrapper.name] = command_wrapper + self._groups[command_wrapper.group].append(command_wrapper.name) + except AttributeError: + raise ConanException("There is no {} method defined in {}".format(method_name, + import_path)) + + @property + def conan_api(self): + return self._conan + + @property + def commands(self): + if self._commands is None: + self._commands = {} + conan_commands_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "commands") + for module in pkgutil.iter_modules([conan_commands_path]): + self._add_command("conans.cli.commands.{}".format(module.name), module.name) + if get_env("CONAN_USER_COMMANDS", default=False): + user_commands_path = os.path.join(self._conan.cache_folder, "commands") + sys.path.append(user_commands_path) + for module in pkgutil.iter_modules([user_commands_path]): + if module.name.startswith("cmd_"): + self._add_command(module.name, module.name.replace("cmd_", "")) + return self._commands + + @property + def groups(self): + return self._groups + + def _print_similar(self, command): + """ Looks for similar commands and prints them if found. + """ + matches = get_close_matches( + word=command, possibilities=self.commands.keys(), n=5, cutoff=0.75) + + if len(matches) == 0: + return + + if len(matches) > 1: + self._out.writeln("The most similar commands are") + else: + self._out.writeln("The most similar command is") + + for match in matches: + self._out.writeln(" %s" % match) + + self._out.writeln("") + + def help_message(self): + self.commands["help"].method(self.conan_api, self.commands, self.groups) + + def run(self, *args): + """ Entry point for executing commands, dispatcher to class + methods + """ + version = sys.version_info + if version.major == 2 or version.minor <= 4: + raise ConanException("Unsupported Python version") + + ret_code = SUCCESS + try: + try: + command_argument = args[0][0] + except IndexError: # No parameters + self.help_message() + return False + try: + command = self.commands[command_argument] + except KeyError as exc: + if command_argument in ["-v", "--version"]: + self._out.success("Conan version %s" % client_version) + return False + + if command_argument in ["-h", "--help"]: + self.help_message() + return False + + self._out.writeln( + "'%s' is not a Conan command. See 'conan --help'." % command_argument) + self._out.writeln("") + self._print_similar(command_argument) + raise ConanException("Unknown command %s" % str(exc)) + + command.run(args[0][1:], conan_api=self.conan_api, + parser=self.commands[command_argument].parser, + commands=self.commands, groups=self.groups) + + except KeyboardInterrupt as exc: + logger.error(exc) + ret_code = SUCCESS + except SystemExit as exc: + if exc.code != 0: + logger.error(exc) + self._out.error("Exiting with code: %d" % exc.code) + ret_code = exc.code + except ConanInvalidConfiguration as exc: + ret_code = ERROR_INVALID_CONFIGURATION + self._out.error(exc) + except ConanException as exc: + ret_code = ERROR_GENERAL + self._out.error(exc) + except Exception as exc: + import traceback + print(traceback.format_exc()) + ret_code = ERROR_GENERAL + msg = exception_message_safe(exc) + self._out.error(msg) + + return ret_code + + +def main(args): + """ main entry point of the conan application, using a Command to + parse parameters + + Exit codes for conan command: + + 0: Success (done) + 1: General ConanException error (done) + 2: Migration error + 3: Ctrl+C + 4: Ctrl+Break + 5: SIGTERM + 6: Invalid configuration (done) + """ + try: + conan_api, _, _ = Conan.factory() + except ConanMigrationError: # Error migrating + sys.exit(ERROR_MIGRATION) + except ConanException as e: + sys.stderr.write("Error in Conan initialization: {}".format(e)) + sys.exit(ERROR_GENERAL) + + def ctrl_c_handler(_, __): + print('You pressed Ctrl+C!') + sys.exit(USER_CTRL_C) + + def sigterm_handler(_, __): + print('Received SIGTERM!') + sys.exit(ERROR_SIGTERM) + + def ctrl_break_handler(_, __): + print('You pressed Ctrl+Break!') + sys.exit(USER_CTRL_BREAK) + + signal.signal(signal.SIGINT, ctrl_c_handler) + signal.signal(signal.SIGTERM, sigterm_handler) + + if sys.platform == 'win32': + signal.signal(signal.SIGBREAK, ctrl_break_handler) + + cli = Cli(conan_api) + error = cli.run(args) + sys.exit(error) diff --git a/conans/cli/command.py b/conans/cli/command.py index 17357048f50..92f2e35a475 100644 --- a/conans/cli/command.py +++ b/conans/cli/command.py @@ -1,76 +1,6 @@ import argparse -import os -import signal -import sys -import textwrap -from collections import defaultdict -from difflib import get_close_matches -import importlib -import pkgutil -from conans import __version__ as client_version -from conans.util.env_reader import get_env -from conans.client.conan_api import Conan -from conans.errors import ConanException, ConanInvalidConfiguration, ConanMigrationError -from conans.util.files import exception_message_safe -from conans.util.log import logger - -# Exit codes for conan command: -SUCCESS = 0 # 0: Success (done) -ERROR_GENERAL = 1 # 1: General ConanException error (done) -ERROR_MIGRATION = 2 # 2: Migration error -USER_CTRL_C = 3 # 3: Ctrl+C -USER_CTRL_BREAK = 4 # 4: Ctrl+Break -ERROR_SIGTERM = 5 # 5: SIGTERM -ERROR_INVALID_CONFIGURATION = 6 # 6: Invalid configuration (done) - - -class Extender(argparse.Action): - """Allows using the same flag several times in command and creates a list with the values. - For example: - conan install MyPackage/1.2@user/channel -o qt:value -o mode:2 -s cucumber:true - It creates: - options = ['qt:value', 'mode:2'] - settings = ['cucumber:true'] - """ - - def __call__(self, parser, namespace, values, option_strings=None): # @UnusedVariable - # Need None here incase `argparse.SUPPRESS` was supplied for `dest` - dest = getattr(namespace, self.dest, None) - if not hasattr(dest, 'extend') or dest == self.default: - dest = [] - setattr(namespace, self.dest, dest) - # if default isn't set to None, this method might be called - # with the default as `values` for other arguments which - # share this destination. - parser.set_defaults(**{self.dest: None}) - - if isinstance(values, str): - dest.append(values) - elif values: - try: - dest.extend(values) - except ValueError: - dest.append(values) - - -class OnceArgument(argparse.Action): - """Allows declaring a parameter that can have only one value, by default argparse takes the - latest declared and it's very confusing. - """ - - def __call__(self, parser, namespace, values, option_string=None): - if getattr(namespace, self.dest) is not None and self.default is None: - msg = '{o} can only be specified once'.format(o=option_string) - raise argparse.ArgumentError(None, msg) - setattr(namespace, self.dest, values) - - -class SmartFormatter(argparse.HelpFormatter): - - def _fill_text(self, text, width, indent): - text = textwrap.dedent(text) - return ''.join(indent + line for line in text.splitlines(True)) +from conans.cli.cli import SmartFormatter class ConanCommand(object): @@ -120,178 +50,3 @@ def decorator(f): return cmd return decorator - - -class Command(object): - """A single command of the conan application, with all the first level commands. Manages the - parsing of parameters and delegates functionality to the conan python api. It can also show the - help of the tool. - """ - - def __init__(self, conan_api): - assert isinstance(conan_api, Conan), "Expected 'Conan' type, got '{}'".format( - type(conan_api)) - self._conan = conan_api - self._out = conan_api.out - self._groups = defaultdict(list) - self._commands = None - - def _add_command(self, import_path, method_name): - try: - command_wrapper = getattr(importlib.import_module(import_path), method_name) - if command_wrapper.doc: - self._commands[command_wrapper.name] = command_wrapper - self._groups[command_wrapper.group].append(command_wrapper.name) - except AttributeError: - raise ConanException("There is no {} method defined in {}".format(method_name, - import_path)) - - @property - def conan_api(self): - return self._conan - - @property - def commands(self): - if self._commands is None: - self._commands = {} - conan_commands_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), - "commands") - for module in pkgutil.iter_modules([conan_commands_path]): - self._add_command("conans.cli.commands.{}".format(module.name), module.name) - if get_env("CONAN_USER_COMMANDS", default=False): - user_commands_path = os.path.join(self._conan.cache_folder, "commands") - sys.path.append(user_commands_path) - for module in pkgutil.iter_modules([user_commands_path]): - if module.name.startswith("cmd_"): - self._add_command(module.name, module.name.replace("cmd_", "")) - return self._commands - - @property - def groups(self): - return self._groups - - def _print_similar(self, command): - """ Looks for similar commands and prints them if found. - """ - matches = get_close_matches( - word=command, possibilities=self.commands.keys(), n=5, cutoff=0.75) - - if len(matches) == 0: - return - - if len(matches) > 1: - self._out.writeln("The most similar commands are") - else: - self._out.writeln("The most similar command is") - - for match in matches: - self._out.writeln(" %s" % match) - - self._out.writeln("") - - def help_message(self): - self.commands["help"].method(self.conan_api, self.commands, self.groups) - - def run(self, *args): - """ Entry point for executing commands, dispatcher to class - methods - """ - version = sys.version_info - if version.major == 2 or version.minor <= 4: - raise ConanException("Unsupported Python version") - - ret_code = SUCCESS - try: - try: - command_argument = args[0][0] - except IndexError: # No parameters - self.help_message() - return False - try: - command = self.commands[command_argument] - except KeyError as exc: - if command_argument in ["-v", "--version"]: - self._out.success("Conan version %s" % client_version) - return False - - if command_argument in ["-h", "--help"]: - self.help_message() - return False - - self._out.writeln( - "'%s' is not a Conan command. See 'conan --help'." % command_argument) - self._out.writeln("") - self._print_similar(command_argument) - raise ConanException("Unknown command %s" % str(exc)) - - command.run(args[0][1:], conan_api=self.conan_api, - parser=self.commands[command_argument].parser, - commands=self.commands, groups=self.groups) - - except KeyboardInterrupt as exc: - logger.error(exc) - ret_code = SUCCESS - except SystemExit as exc: - if exc.code != 0: - logger.error(exc) - self._out.error("Exiting with code: %d" % exc.code) - ret_code = exc.code - except ConanInvalidConfiguration as exc: - ret_code = ERROR_INVALID_CONFIGURATION - self._out.error(exc) - except ConanException as exc: - ret_code = ERROR_GENERAL - self._out.error(exc) - except Exception as exc: - import traceback - print(traceback.format_exc()) - ret_code = ERROR_GENERAL - msg = exception_message_safe(exc) - self._out.error(msg) - - return ret_code - - -def main(args): - """ main entry point of the conan application, using a Command to - parse parameters - - Exit codes for conan command: - - 0: Success (done) - 1: General ConanException error (done) - 2: Migration error - 3: Ctrl+C - 4: Ctrl+Break - 5: SIGTERM - 6: Invalid configuration (done) - """ - try: - conan_api, _, _ = Conan.factory() - except ConanMigrationError: # Error migrating - sys.exit(ERROR_MIGRATION) - except ConanException as e: - sys.stderr.write("Error in Conan initialization: {}".format(e)) - sys.exit(ERROR_GENERAL) - - def ctrl_c_handler(_, __): - print('You pressed Ctrl+C!') - sys.exit(USER_CTRL_C) - - def sigterm_handler(_, __): - print('Received SIGTERM!') - sys.exit(ERROR_SIGTERM) - - def ctrl_break_handler(_, __): - print('You pressed Ctrl+Break!') - sys.exit(USER_CTRL_BREAK) - - signal.signal(signal.SIGINT, ctrl_c_handler) - signal.signal(signal.SIGTERM, sigterm_handler) - - if sys.platform == 'win32': - signal.signal(signal.SIGBREAK, ctrl_break_handler) - - command = Command(conan_api) - error = command.run(args) - sys.exit(error) diff --git a/conans/cli/commands/search.py b/conans/cli/commands/search.py index 9836b22ab15..1e7d3f2652d 100644 --- a/conans/cli/commands/search.py +++ b/conans/cli/commands/search.py @@ -2,7 +2,8 @@ from conans.client.output import Color from conans.errors import ConanException -from conans.cli.command import OnceArgument, Extender, conan_command +from conans.cli.cli import OnceArgument, Extender +from conans.cli.command import conan_command def output_search_cli(info, out): diff --git a/conans/conan.py b/conans/conan.py index de96f2d0a5d..b67fa1ae097 100755 --- a/conans/conan.py +++ b/conans/conan.py @@ -2,7 +2,7 @@ import os if os.getenv("CONAN_V2_CLI"): - from conans.cli.command import main + from conans.cli.cli import main else: from conans.client.command import main From 63b6ef092b81cc55fc9368f1c9a3e94d6976413d Mon Sep 17 00:00:00 2001 From: czoido Date: Mon, 6 Jul 2020 13:24:44 +0200 Subject: [PATCH 03/20] fail on empty docs --- conans/cli/command.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/conans/cli/command.py b/conans/cli/command.py index 92f2e35a475..7f4c907e002 100644 --- a/conans/cli/command.py +++ b/conans/cli/command.py @@ -1,6 +1,7 @@ import argparse from conans.cli.cli import SmartFormatter +from conans.errors import ConanException class ConanCommand(object): @@ -12,7 +13,12 @@ def __init__(self, method, group=None, **kwargs): self._group = group or "Misc commands" self._name = method.__name__.replace("_", "-") self._method = method - self._doc = method.__doc__ or "Empty description" + if method.__doc__: + self._doc = method.__doc__ + else: + raise ConanException("No documentation string defined for command: '{}'. Conan " + "commands should provide a documentation string explaining " + "its use briefly.".format(self._name)) self._parser = argparse.ArgumentParser(description=self._doc, prog="conan {}".format(self._name), formatter_class=SmartFormatter) From 47b18cbfb40538235ef1eb5993eca80f1ebd2165 Mon Sep 17 00:00:00 2001 From: czoido Date: Mon, 6 Jul 2020 13:27:00 +0200 Subject: [PATCH 04/20] rename _conan to _conan_api --- conans/cli/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/conans/cli/cli.py b/conans/cli/cli.py index 47f2185f9d3..085cd39190c 100644 --- a/conans/cli/cli.py +++ b/conans/cli/cli.py @@ -82,7 +82,7 @@ class Cli(object): def __init__(self, conan_api): assert isinstance(conan_api, Conan), "Expected 'Conan' type, got '{}'".format( type(conan_api)) - self._conan = conan_api + self._conan_api = conan_api self._out = conan_api.out self._groups = defaultdict(list) self._commands = None @@ -99,7 +99,7 @@ def _add_command(self, import_path, method_name): @property def conan_api(self): - return self._conan + return self._conan_api @property def commands(self): @@ -110,7 +110,7 @@ def commands(self): for module in pkgutil.iter_modules([conan_commands_path]): self._add_command("conans.cli.commands.{}".format(module.name), module.name) if get_env("CONAN_USER_COMMANDS", default=False): - user_commands_path = os.path.join(self._conan.cache_folder, "commands") + user_commands_path = os.path.join(self._conan_api.cache_folder, "commands") sys.path.append(user_commands_path) for module in pkgutil.iter_modules([user_commands_path]): if module.name.startswith("cmd_"): From af7d1d72f766b22c39a20948cf6858a83b0504f0 Mon Sep 17 00:00:00 2001 From: czoido Date: Mon, 6 Jul 2020 13:28:44 +0200 Subject: [PATCH 05/20] minimum required version --- conans/cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conans/cli/cli.py b/conans/cli/cli.py index 085cd39190c..273f2fe56a0 100644 --- a/conans/cli/cli.py +++ b/conans/cli/cli.py @@ -149,7 +149,7 @@ def run(self, *args): """ version = sys.version_info if version.major == 2 or version.minor <= 4: - raise ConanException("Unsupported Python version") + raise ConanException("Unsupported Python version. Minimum required version is Python 3.5") ret_code = SUCCESS try: From d02c311a4a8f23cfc44cc59f465f9db14daf90c5 Mon Sep 17 00:00:00 2001 From: czoido Date: Mon, 6 Jul 2020 13:30:02 +0200 Subject: [PATCH 06/20] add fixme for conan factory --- conans/cli/cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/conans/cli/cli.py b/conans/cli/cli.py index 273f2fe56a0..30953ed1219 100644 --- a/conans/cli/cli.py +++ b/conans/cli/cli.py @@ -149,7 +149,8 @@ def run(self, *args): """ version = sys.version_info if version.major == 2 or version.minor <= 4: - raise ConanException("Unsupported Python version. Minimum required version is Python 3.5") + raise ConanException( + "Unsupported Python version. Minimum required version is Python 3.5") ret_code = SUCCESS try: @@ -218,7 +219,7 @@ def main(args): 6: Invalid configuration (done) """ try: - conan_api, _, _ = Conan.factory() + conan_api, _, _ = Conan.factory() # FIXME: Conan factory will be removed in Conan 2.0 except ConanMigrationError: # Error migrating sys.exit(ERROR_MIGRATION) except ConanException as e: From 762bc92777f8122f5c6fec0629153e856187124f Mon Sep 17 00:00:00 2001 From: czoido Date: Mon, 6 Jul 2020 13:31:59 +0200 Subject: [PATCH 07/20] add discuss --- conans/cli/commands/search.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conans/cli/commands/search.py b/conans/cli/commands/search.py index 1e7d3f2652d..5c04d526a75 100644 --- a/conans/cli/commands/search.py +++ b/conans/cli/commands/search.py @@ -30,6 +30,7 @@ def search(*args, conan_api, parser, **kwargs): parser.add_argument('query', help="Search query to find package recipe reference, e.g., 'boost', 'lib*'") + # TODO: Discuss if --cache and --remote are exclusive exclusive_args = parser.add_mutually_exclusive_group() exclusive_args.add_argument('-r', '--remote', default=None, action=Extender, nargs='?', help="Remote to search. Accepts wildcards. To search in all remotes use *") From 62879335887f275c92993d5e00dd8615584b3ffb Mon Sep 17 00:00:00 2001 From: czoido Date: Mon, 6 Jul 2020 13:55:29 +0200 Subject: [PATCH 08/20] raise on not allowed formatter --- conans/cli/command.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conans/cli/command.py b/conans/cli/command.py index 7f4c907e002..ebf46be3623 100644 --- a/conans/cli/command.py +++ b/conans/cli/command.py @@ -7,7 +7,11 @@ class ConanCommand(object): def __init__(self, method, group=None, **kwargs): self._formatters = {} + self._allowed_formatters = ["cli", "json"] for kind, action in kwargs.items(): + if kind not in self._allowed_formatters: + raise ConanException("Formatter '{}' not allowed. Allowed formatters: {}" + .format(kind, " ".join(self._allowed_formatters))) if callable(action): self._formatters[kind] = action self._group = group or "Misc commands" From f12f38c39877549f4a0d1da4830bd0ed77de61a3 Mon Sep 17 00:00:00 2001 From: czoido Date: Mon, 6 Jul 2020 18:38:24 +0200 Subject: [PATCH 09/20] refactor weird try, except, finally pattern --- conans/cli/command.py | 20 +++++++++++++-- conans/cli/commands/search.py | 46 ++++++++++++++++++----------------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/conans/cli/command.py b/conans/cli/command.py index ebf46be3623..3bd52ae43aa 100644 --- a/conans/cli/command.py +++ b/conans/cli/command.py @@ -4,6 +4,20 @@ from conans.errors import ConanException +def info_handler(func): + def decorator(*args, **kwargs): + ret = {"error": None, "data": None} + try: + ret["data"] = func(*args, **kwargs) + except Exception as exc: + ret["error"] = exc + raise + finally: + return ret + + return decorator + + class ConanCommand(object): def __init__(self, method, group=None, **kwargs): self._formatters = {} @@ -30,8 +44,10 @@ def __init__(self, method, group=None, **kwargs): def run(self, *args, **kwargs): conan_api = kwargs["conan_api"] info, formatter = self._method(*args, **kwargs) - if info: - self._formatters[formatter](info, conan_api.out) + if not info["error"]: + self._formatters[formatter](info["data"], conan_api.out) + else: + raise info["error"] @property def group(self): diff --git a/conans/cli/commands/search.py b/conans/cli/commands/search.py index 5c04d526a75..3df8c4494b6 100644 --- a/conans/cli/commands/search.py +++ b/conans/cli/commands/search.py @@ -1,9 +1,9 @@ import json from conans.client.output import Color -from conans.errors import ConanException +from conans.errors import ConanException, NoRemoteAvailable from conans.cli.cli import OnceArgument, Extender -from conans.cli.command import conan_command +from conans.cli.command import conan_command, info_handler def output_search_cli(info, out): @@ -17,10 +17,27 @@ def output_search_cli(info, out): def output_search_json(info, out): - myjson = json.dumps(info["results"], indent=4) + results = info["results"] + myjson = json.dumps(results, indent=4) out.writeln(myjson) +@info_handler +def apiv2_search_recipes(query, remote_patterns=None, local_cache=False): + remote = None + if remote_patterns is not None and len(remote_patterns) > 0: + remote = remote_patterns[0].replace("*", "remote") + + if remote and "bad" in remote: + raise NoRemoteAvailable("Remote '%s' not found in remotes" % remote) + + search_results = {"results": [{"remote": remote, + "items": [{"recipe": {"id": "app/1.0"}}, + {"recipe": {"id": "liba/1.0"}}]}]} + + return search_results + + @conan_command(group="Consumer commands", cli=output_search_cli, json=output_search_json) def search(*args, conan_api, parser, **kwargs): """ @@ -37,24 +54,9 @@ def search(*args, conan_api, parser, **kwargs): exclusive_args.add_argument('-c', '--cache', action="store_true", help="Search in the local cache") parser.add_argument('-o', '--output', default="cli", action=OnceArgument, - help="Select the output format: json, html,...") + help="Select the output format: cli, json.") args = parser.parse_args(*args) - try: - def apiv2_search_recipes(query, remote_patterns=None, local_cache=False): - remote = None - if remote_patterns is not None and len(remote_patterns) > 0: - remote = remote_patterns[0].replace("*", "remote") - - search_results = {"results": [{"remote": remote, - "items": [{"recipe": {"id": "app/1.0"}}, - {"recipe": {"id": "liba/1.0"}}]}]} - return search_results - - remotes = args.remote or [] - info = apiv2_search_recipes(args.query, remote_patterns=remotes, local_cache=args.cache) - except ConanException as exc: - info = exc.info - raise - finally: - return info, args.output + remotes = args.remote or [] + info = apiv2_search_recipes(args.query, remote_patterns=remotes, local_cache=args.cache) + return info, args.output From a505db54894a015cbb7b1d496c6aff8732539ee4 Mon Sep 17 00:00:00 2001 From: czoido Date: Wed, 8 Jul 2020 09:34:05 +0200 Subject: [PATCH 10/20] change output handling --- conans/cli/command.py | 20 ++++++++++++++++---- conans/cli/commands/help.py | 2 +- conans/cli/commands/search.py | 4 +--- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/conans/cli/command.py b/conans/cli/command.py index 3bd52ae43aa..722eac55bfd 100644 --- a/conans/cli/command.py +++ b/conans/cli/command.py @@ -1,6 +1,6 @@ import argparse -from conans.cli.cli import SmartFormatter +from conans.cli.cli import SmartFormatter, OnceArgument from conans.errors import ConanException @@ -25,9 +25,13 @@ def __init__(self, method, group=None, **kwargs): for kind, action in kwargs.items(): if kind not in self._allowed_formatters: raise ConanException("Formatter '{}' not allowed. Allowed formatters: {}" - .format(kind, " ".join(self._allowed_formatters))) + .format(kind, ", ".join(self._allowed_formatters))) if callable(action): self._formatters[kind] = action + else: + raise ConanException("Invalid formatter for {}. The formatter must be" + "a valid function".format(kind)) + self._group = group or "Misc commands" self._name = method.__name__.replace("_", "-") self._method = method @@ -40,12 +44,20 @@ def __init__(self, method, group=None, **kwargs): self._parser = argparse.ArgumentParser(description=self._doc, prog="conan {}".format(self._name), formatter_class=SmartFormatter) + formatters_list = list(self._formatters.keys()) + if self._formatters: + self._output_help_message = "Select the output format: {}"\ + .format(", ".join(formatters_list)) + + self._parser.add_argument('-o', '--output', default="cli", choices=formatters_list, + action=OnceArgument, help=self._output_help_message) def run(self, *args, **kwargs): conan_api = kwargs["conan_api"] - info, formatter = self._method(*args, **kwargs) + info = self._method(*args, **kwargs) + parser_args = self._parser.parse_args(*args) if not info["error"]: - self._formatters[formatter](info["data"], conan_api.out) + self._formatters[parser_args.output](info["data"], conan_api.out) else: raise info["error"] diff --git a/conans/cli/commands/help.py b/conans/cli/commands/help.py index 114b916a613..102f75bce0b 100644 --- a/conans/cli/commands/help.py +++ b/conans/cli/commands/help.py @@ -48,7 +48,7 @@ def help(*args, conan_api, parser, commands, groups, **kwargs): args = parser.parse_args(*args) if not args.command: output_help_cli(conan_api.out, commands, groups) - return None, None + return None try: commands[args.command].run(["--help"], parser=commands[args.command].parser, conan_api=conan_api) diff --git a/conans/cli/commands/search.py b/conans/cli/commands/search.py index 3df8c4494b6..d7fb455f110 100644 --- a/conans/cli/commands/search.py +++ b/conans/cli/commands/search.py @@ -53,10 +53,8 @@ def search(*args, conan_api, parser, **kwargs): help="Remote to search. Accepts wildcards. To search in all remotes use *") exclusive_args.add_argument('-c', '--cache', action="store_true", help="Search in the local cache") - parser.add_argument('-o', '--output', default="cli", action=OnceArgument, - help="Select the output format: cli, json.") args = parser.parse_args(*args) remotes = args.remote or [] info = apiv2_search_recipes(args.query, remote_patterns=remotes, local_cache=args.cache) - return info, args.output + return info From 777ca3d1b30b4883b1673a8d662a3ec8cd256267 Mon Sep 17 00:00:00 2001 From: czoido Date: Wed, 8 Jul 2020 10:13:11 +0200 Subject: [PATCH 11/20] remove weird pattern --- conans/cli/command.py | 27 +++++++-------------------- conans/cli/commands/search.py | 7 +++---- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/conans/cli/command.py b/conans/cli/command.py index 722eac55bfd..7cf677c950e 100644 --- a/conans/cli/command.py +++ b/conans/cli/command.py @@ -4,20 +4,6 @@ from conans.errors import ConanException -def info_handler(func): - def decorator(*args, **kwargs): - ret = {"error": None, "data": None} - try: - ret["data"] = func(*args, **kwargs) - except Exception as exc: - ret["error"] = exc - raise - finally: - return ret - - return decorator - - class ConanCommand(object): def __init__(self, method, group=None, **kwargs): self._formatters = {} @@ -54,12 +40,13 @@ def __init__(self, method, group=None, **kwargs): def run(self, *args, **kwargs): conan_api = kwargs["conan_api"] - info = self._method(*args, **kwargs) - parser_args = self._parser.parse_args(*args) - if not info["error"]: - self._formatters[parser_args.output](info["data"], conan_api.out) - else: - raise info["error"] + try: + info = self._method(*args, **kwargs) + parser_args = self._parser.parse_args(*args) + if info: + self._formatters[parser_args.output](info, conan_api.out) + except Exception: + raise @property def group(self): diff --git a/conans/cli/commands/search.py b/conans/cli/commands/search.py index d7fb455f110..5f377955517 100644 --- a/conans/cli/commands/search.py +++ b/conans/cli/commands/search.py @@ -1,9 +1,9 @@ import json from conans.client.output import Color -from conans.errors import ConanException, NoRemoteAvailable -from conans.cli.cli import OnceArgument, Extender -from conans.cli.command import conan_command, info_handler +from conans.errors import NoRemoteAvailable +from conans.cli.cli import Extender +from conans.cli.command import conan_command def output_search_cli(info, out): @@ -22,7 +22,6 @@ def output_search_json(info, out): out.writeln(myjson) -@info_handler def apiv2_search_recipes(query, remote_patterns=None, local_cache=False): remote = None if remote_patterns is not None and len(remote_patterns) > 0: From 8e2f508f1a112db1b704ac86ec59089514384024 Mon Sep 17 00:00:00 2001 From: czoido Date: Wed, 8 Jul 2020 10:36:59 +0200 Subject: [PATCH 12/20] minor changes --- conans/cli/cli.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/conans/cli/cli.py b/conans/cli/cli.py index 30953ed1219..79ecf2dc0c3 100644 --- a/conans/cli/cli.py +++ b/conans/cli/cli.py @@ -85,7 +85,16 @@ def __init__(self, conan_api): self._conan_api = conan_api self._out = conan_api.out self._groups = defaultdict(list) - self._commands = None + self._commands = {} + conan_commands_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "commands") + for module in pkgutil.iter_modules([conan_commands_path]): + self._add_command("conans.cli.commands.{}".format(module.name), module.name) + if get_env("CONAN_USER_COMMANDS", default=False): + user_commands_path = os.path.join(self._conan_api.cache_folder, "commands") + sys.path.append(user_commands_path) + for module in pkgutil.iter_modules([user_commands_path]): + if module.name.startswith("cmd_"): + self._add_command(module.name, module.name.replace("cmd_", "")) def _add_command(self, import_path, method_name): try: @@ -103,18 +112,6 @@ def conan_api(self): @property def commands(self): - if self._commands is None: - self._commands = {} - conan_commands_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), - "commands") - for module in pkgutil.iter_modules([conan_commands_path]): - self._add_command("conans.cli.commands.{}".format(module.name), module.name) - if get_env("CONAN_USER_COMMANDS", default=False): - user_commands_path = os.path.join(self._conan_api.cache_folder, "commands") - sys.path.append(user_commands_path) - for module in pkgutil.iter_modules([user_commands_path]): - if module.name.startswith("cmd_"): - self._add_command(module.name, module.name.replace("cmd_", "")) return self._commands @property From 9b30dc4d6b4c0300a50572ff67e9416a7de2f3e3 Mon Sep 17 00:00:00 2001 From: czoido Date: Wed, 8 Jul 2020 16:11:21 +0200 Subject: [PATCH 13/20] move exception handling --- conans/cli/cli.py | 97 +++++++++++++++++++++++------------------------ 1 file changed, 47 insertions(+), 50 deletions(-) diff --git a/conans/cli/cli.py b/conans/cli/cli.py index 79ecf2dc0c3..f2a4d16b2c7 100644 --- a/conans/cli/cli.py +++ b/conans/cli/cli.py @@ -149,56 +149,33 @@ def run(self, *args): raise ConanException( "Unsupported Python version. Minimum required version is Python 3.5") - ret_code = SUCCESS try: - try: - command_argument = args[0][0] - except IndexError: # No parameters + command_argument = args[0][0] + except IndexError: # No parameters + self.help_message() + return SUCCESS + try: + command = self.commands[command_argument] + except KeyError as exc: + if command_argument in ["-v", "--version"]: + self._out.success("Conan version %s" % client_version) + return SUCCESS + + if command_argument in ["-h", "--help"]: self.help_message() - return False - try: - command = self.commands[command_argument] - except KeyError as exc: - if command_argument in ["-v", "--version"]: - self._out.success("Conan version %s" % client_version) - return False - - if command_argument in ["-h", "--help"]: - self.help_message() - return False - - self._out.writeln( - "'%s' is not a Conan command. See 'conan --help'." % command_argument) - self._out.writeln("") - self._print_similar(command_argument) - raise ConanException("Unknown command %s" % str(exc)) - - command.run(args[0][1:], conan_api=self.conan_api, - parser=self.commands[command_argument].parser, - commands=self.commands, groups=self.groups) - - except KeyboardInterrupt as exc: - logger.error(exc) - ret_code = SUCCESS - except SystemExit as exc: - if exc.code != 0: - logger.error(exc) - self._out.error("Exiting with code: %d" % exc.code) - ret_code = exc.code - except ConanInvalidConfiguration as exc: - ret_code = ERROR_INVALID_CONFIGURATION - self._out.error(exc) - except ConanException as exc: - ret_code = ERROR_GENERAL - self._out.error(exc) - except Exception as exc: - import traceback - print(traceback.format_exc()) - ret_code = ERROR_GENERAL - msg = exception_message_safe(exc) - self._out.error(msg) - - return ret_code + return SUCCESS + + self._out.writeln( + "'%s' is not a Conan command. See 'conan --help'." % command_argument) + self._out.writeln("") + self._print_similar(command_argument) + raise ConanException("Unknown command %s" % str(exc)) + + command.run(args[0][1:], conan_api=self.conan_api, + parser=self.commands[command_argument].parser, + commands=self.commands, groups=self.groups) + + return SUCCESS def main(args): @@ -242,5 +219,25 @@ def ctrl_break_handler(_, __): signal.signal(signal.SIGBREAK, ctrl_break_handler) cli = Cli(conan_api) - error = cli.run(args) - sys.exit(error) + + try: + exit_error = cli.run(args) + except SystemExit as exc: + if exc.code != 0: + logger.error(exc) + conan_api.out.error("Exiting with code: %d" % exc.code) + exit_error = exc.code + except ConanInvalidConfiguration as exc: + exit_error = ERROR_INVALID_CONFIGURATION + conan_api.out.error(exc) + except ConanException as exc: + exit_error = ERROR_GENERAL + conan_api.out.error(exc) + except Exception as exc: + import traceback + print(traceback.format_exc()) + exit_error = ERROR_GENERAL + msg = exception_message_safe(exc) + conan_api.out.error(msg) + + sys.exit(exit_error) From 8d10267ab9189ceab064fe874c4be93f0daf388e Mon Sep 17 00:00:00 2001 From: czoido Date: Wed, 8 Jul 2020 16:25:23 +0200 Subject: [PATCH 14/20] use formatters as dictonary --- conans/cli/command.py | 12 ++++-------- conans/cli/commands/help.py | 2 +- conans/cli/commands/search.py | 3 ++- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/conans/cli/command.py b/conans/cli/command.py index 7cf677c950e..d296f9bee51 100644 --- a/conans/cli/command.py +++ b/conans/cli/command.py @@ -5,13 +5,9 @@ class ConanCommand(object): - def __init__(self, method, group=None, **kwargs): + def __init__(self, method, group, formatters=None): self._formatters = {} - self._allowed_formatters = ["cli", "json"] - for kind, action in kwargs.items(): - if kind not in self._allowed_formatters: - raise ConanException("Formatter '{}' not allowed. Allowed formatters: {}" - .format(kind, ", ".join(self._allowed_formatters))) + for kind, action in formatters.items(): if callable(action): self._formatters[kind] = action else: @@ -69,9 +65,9 @@ def parser(self): return self._parser -def conan_command(**kwargs): +def conan_command(group, formatters=None): def decorator(f): - cmd = ConanCommand(f, **kwargs) + cmd = ConanCommand(f, group, formatters) return cmd return decorator diff --git a/conans/cli/commands/help.py b/conans/cli/commands/help.py index 102f75bce0b..9c6a13c808e 100644 --- a/conans/cli/commands/help.py +++ b/conans/cli/commands/help.py @@ -38,7 +38,7 @@ def output_help_cli(out, commands, groups): out.writeln('Conan commands. Type "conan -h" for help', Color.BRIGHT_YELLOW) -@conan_command(group="Misc commands", cli=output_help_cli) +@conan_command(group="Misc commands", formatters={"cli": output_help_cli}) def help(*args, conan_api, parser, commands, groups, **kwargs): """ Shows help for a specific command. diff --git a/conans/cli/commands/search.py b/conans/cli/commands/search.py index 5f377955517..502e30576dd 100644 --- a/conans/cli/commands/search.py +++ b/conans/cli/commands/search.py @@ -37,7 +37,8 @@ def apiv2_search_recipes(query, remote_patterns=None, local_cache=False): return search_results -@conan_command(group="Consumer commands", cli=output_search_cli, json=output_search_json) +@conan_command(group="Consumer commands", formatters={"cli": output_search_cli, + "json": output_search_json}) def search(*args, conan_api, parser, **kwargs): """ Searches for package recipes whose name contain in a remote or in the local cache From e944715553e8df95b2cb0f21d0409e5fa11a46e6 Mon Sep 17 00:00:00 2001 From: czoido Date: Wed, 8 Jul 2020 16:33:40 +0200 Subject: [PATCH 15/20] minor changes --- conans/cli/cli.py | 3 +-- conans/cli/command.py | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/conans/cli/cli.py b/conans/cli/cli.py index f2a4d16b2c7..cd4697515b2 100644 --- a/conans/cli/cli.py +++ b/conans/cli/cli.py @@ -171,8 +171,7 @@ def run(self, *args): self._print_similar(command_argument) raise ConanException("Unknown command %s" % str(exc)) - command.run(args[0][1:], conan_api=self.conan_api, - parser=self.commands[command_argument].parser, + command.run(self.conan_api, args[0][1:], parser=self.commands[command_argument].parser, commands=self.commands, groups=self.groups) return SUCCESS diff --git a/conans/cli/command.py b/conans/cli/command.py index d296f9bee51..f54537d4226 100644 --- a/conans/cli/command.py +++ b/conans/cli/command.py @@ -34,10 +34,9 @@ def __init__(self, method, group, formatters=None): self._parser.add_argument('-o', '--output', default="cli", choices=formatters_list, action=OnceArgument, help=self._output_help_message) - def run(self, *args, **kwargs): - conan_api = kwargs["conan_api"] + def run(self, conan_api, *args, **kwargs): try: - info = self._method(*args, **kwargs) + info = self._method(*args, conan_api=conan_api, **kwargs) parser_args = self._parser.parse_args(*args) if info: self._formatters[parser_args.output](info, conan_api.out) From 473080621e54d469c710c255a63a1a8edc770a3e Mon Sep 17 00:00:00 2001 From: czoido Date: Fri, 10 Jul 2020 12:07:11 +0200 Subject: [PATCH 16/20] do not allow empty remotee --- conans/cli/commands/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conans/cli/commands/search.py b/conans/cli/commands/search.py index 502e30576dd..80d4e305ef5 100644 --- a/conans/cli/commands/search.py +++ b/conans/cli/commands/search.py @@ -49,7 +49,7 @@ def search(*args, conan_api, parser, **kwargs): # TODO: Discuss if --cache and --remote are exclusive exclusive_args = parser.add_mutually_exclusive_group() - exclusive_args.add_argument('-r', '--remote', default=None, action=Extender, nargs='?', + exclusive_args.add_argument('-r', '--remote', default=None, action=Extender, help="Remote to search. Accepts wildcards. To search in all remotes use *") exclusive_args.add_argument('-c', '--cache', action="store_true", help="Search in the local cache") From da10341ef31963b1723832270a2f2b0f1d648e01 Mon Sep 17 00:00:00 2001 From: czoido Date: Mon, 13 Jul 2020 15:43:36 +0200 Subject: [PATCH 17/20] add output argument only if there are formatters --- conans/cli/command.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conans/cli/command.py b/conans/cli/command.py index f54537d4226..f00d4ed76fb 100644 --- a/conans/cli/command.py +++ b/conans/cli/command.py @@ -31,8 +31,8 @@ def __init__(self, method, group, formatters=None): self._output_help_message = "Select the output format: {}"\ .format(", ".join(formatters_list)) - self._parser.add_argument('-o', '--output', default="cli", choices=formatters_list, - action=OnceArgument, help=self._output_help_message) + self._parser.add_argument('-o', '--output', default="cli", choices=formatters_list, + action=OnceArgument, help=self._output_help_message) def run(self, conan_api, *args, **kwargs): try: From def098b9ac2ceb66149eceb2a7960600cd32e328 Mon Sep 17 00:00:00 2001 From: czoido Date: Mon, 13 Jul 2020 16:05:15 +0200 Subject: [PATCH 18/20] move exit codes --- conans/cli/cli.py | 11 ++--------- conans/cli/exit_codes.py | 8 ++++++++ conans/client/command.py | 12 ++---------- 3 files changed, 12 insertions(+), 19 deletions(-) create mode 100644 conans/cli/exit_codes.py diff --git a/conans/cli/cli.py b/conans/cli/cli.py index cd4697515b2..b9a79b56302 100644 --- a/conans/cli/cli.py +++ b/conans/cli/cli.py @@ -9,21 +9,14 @@ import pkgutil from conans import __version__ as client_version +from conans.cli.exit_codes import SUCCESS, ERROR_MIGRATION, ERROR_GENERAL, USER_CTRL_C, \ + ERROR_SIGTERM, USER_CTRL_BREAK, ERROR_INVALID_CONFIGURATION from conans.util.env_reader import get_env from conans.client.conan_api import Conan from conans.errors import ConanException, ConanInvalidConfiguration, ConanMigrationError from conans.util.files import exception_message_safe from conans.util.log import logger -# Exit codes for conan command: -SUCCESS = 0 # 0: Success (done) -ERROR_GENERAL = 1 # 1: General ConanException error (done) -ERROR_MIGRATION = 2 # 2: Migration error -USER_CTRL_C = 3 # 3: Ctrl+C -USER_CTRL_BREAK = 4 # 4: Ctrl+Break -ERROR_SIGTERM = 5 # 5: SIGTERM -ERROR_INVALID_CONFIGURATION = 6 # 6: Invalid configuration (done) - class Extender(argparse.Action): """Allows using the same flag several times in command and creates a list with the values. diff --git a/conans/cli/exit_codes.py b/conans/cli/exit_codes.py new file mode 100644 index 00000000000..5cb9c065a5d --- /dev/null +++ b/conans/cli/exit_codes.py @@ -0,0 +1,8 @@ +# Exit codes for conan command: +SUCCESS = 0 # 0: Success (done) +ERROR_GENERAL = 1 # 1: General ConanException error (done) +ERROR_MIGRATION = 2 # 2: Migration error +USER_CTRL_C = 3 # 3: Ctrl+C +USER_CTRL_BREAK = 4 # 4: Ctrl+Break +ERROR_SIGTERM = 5 # 5: SIGTERM +ERROR_INVALID_CONFIGURATION = 6 # 6: Invalid configuration (done) diff --git a/conans/client/command.py b/conans/client/command.py index 232273c244f..80eb5ece4c4 100644 --- a/conans/client/command.py +++ b/conans/client/command.py @@ -28,16 +28,8 @@ from conans.util.files import save from conans.util.log import logger from conans.assets import templates - - -# Exit codes for conan command: -SUCCESS = 0 # 0: Success (done) -ERROR_GENERAL = 1 # 1: General ConanException error (done) -ERROR_MIGRATION = 2 # 2: Migration error -USER_CTRL_C = 3 # 3: Ctrl+C -USER_CTRL_BREAK = 4 # 4: Ctrl+Break -ERROR_SIGTERM = 5 # 5: SIGTERM -ERROR_INVALID_CONFIGURATION = 6 # 6: Invalid configuration (done) +from conans.cli.exit_codes import SUCCESS, ERROR_MIGRATION, ERROR_GENERAL, USER_CTRL_C, \ + ERROR_SIGTERM, USER_CTRL_BREAK, ERROR_INVALID_CONFIGURATION class Extender(argparse.Action): From 110fe29613f778b67bd86e376d69f29861cfb434 Mon Sep 17 00:00:00 2001 From: czoido Date: Mon, 13 Jul 2020 16:15:00 +0200 Subject: [PATCH 19/20] change default formatter --- conans/cli/command.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/conans/cli/command.py b/conans/cli/command.py index f00d4ed76fb..e3ab3af4c45 100644 --- a/conans/cli/command.py +++ b/conans/cli/command.py @@ -26,12 +26,12 @@ def __init__(self, method, group, formatters=None): self._parser = argparse.ArgumentParser(description=self._doc, prog="conan {}".format(self._name), formatter_class=SmartFormatter) - formatters_list = list(self._formatters.keys()) if self._formatters: - self._output_help_message = "Select the output format: {}"\ - .format(", ".join(formatters_list)) - - self._parser.add_argument('-o', '--output', default="cli", choices=formatters_list, + formatters_list = list(self._formatters.keys()) + default_output = "cli" if "cli" in formatters_list else formatters_list[0] + self._output_help_message = "Select the output format: {}. '{}' is the default output."\ + .format(", ".join(formatters_list), default_output) + self._parser.add_argument('-o', '--output', default=default_output, choices=formatters_list, action=OnceArgument, help=self._output_help_message) def run(self, conan_api, *args, **kwargs): From 8137974be4e23966f41a777698f03e0541e24f48 Mon Sep 17 00:00:00 2001 From: czoido Date: Wed, 15 Jul 2020 14:20:03 +0200 Subject: [PATCH 20/20] add init --- conans/cli/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 conans/cli/__init__.py diff --git a/conans/cli/__init__.py b/conans/cli/__init__.py new file mode 100644 index 00000000000..e69de29bb2d