Repository: conan-io/conan
Pull request #12292 (2022-10-12 17:45:46)

Feature/develop2 refactors cli
Some pure refactors, no functional changes:
- Moved some functions from ``formatters`` -> ``printers``, as they are not formatters (thy only print to stderr)
- Moved some functions from ``common.py``, a bit too generic name to ``args.py``, those functions to help with cli args
- Moved 2 functions from ``common.py`` to ``RemotesAPI``, they seem part of the api or api helpers.


Patch:
diff --git a/conan/api/subapi/remotes.py b/conan/api/subapi/remotes.py
index 018e5c598..e165dae92 100644
--- a/conan/api/subapi/remotes.py
+++ b/conan/api/subapi/remotes.py
@@ -7,6 +7,32 @@ from conans.client.cmd.user import user_set, users_clean, users_list
 from conans.errors import ConanException
 
 
+# TODO: Redesign these, why not part of the API?
+#  Why selection logic is different for "conan list" and "conan install"
+def get_remote_selection(conan_api, remote_patterns):
+    """
+    Return a list of Remote() objects matching the specified patterns. If a pattern doesn't match
+    anything, it fails
+    """
+    ret_remotes = []
+    for pattern in remote_patterns:
+        tmp = conan_api.remotes.list(pattern=pattern, only_active=True)
+        if not tmp:
+            raise ConanException("Remotes for pattern '{}' can't be found or are "
+                                 "disabled".format(pattern))
+        ret_remotes.extend(tmp)
+    return ret_remotes
+
+
+def get_multiple_remotes(conan_api, remote_names=None):
+    if remote_names:
+        # FIXME: Check this, this can get disabled remotes, it is intended?
+        return [conan_api.remotes.get(remote_name) for remote_name in remote_names]
+    elif remote_names is None:
+        # if we don't pass any remotes we want to retrieve only the enabled ones
+        return conan_api.remotes.list(only_active=True)
+
+
 class RemotesAPI:
 
     def __init__(self, conan_api):
diff --git a/conan/cli/args.py b/conan/cli/args.py
new file mode 100644
index 000000000..ad841aed9
--- /dev/null
+++ b/conan/cli/args.py
@@ -0,0 +1,106 @@
+from conan.cli.command import OnceArgument, ExtenderValueRequired, Extender
+
+
+_help_build_policies = '''Optional, specify which packages to build from source. Combining multiple
+    '--build' options on one command line is allowed. For dependencies, the optional 'build_policy'
+    attribute in their conanfile.py takes precedence over the command line parameter.
+    Possible parameters:
+
+    --build="*"        Force build for all packages, do not use binary packages.
+    --build=never      Disallow build for all packages, use binary packages or fail if a binary
+                       package is not found. Cannot be combined with other '--build' options.
+    --build=missing    Build packages from source whose binary package is not found.
+    --build=cascade    Build packages from source that have at least one dependency being built from
+                       source.
+    --build=[pattern]  Build packages from source whose package reference matches the pattern. The
+                       pattern uses 'fnmatch' style wildcards.
+    --build=![pattern] Excluded packages, which will not be built from the source, whose package
+                       reference matches the pattern. The pattern uses 'fnmatch' style wildcards.
+
+    Default behavior: If you omit the '--build' option, the 'build_policy' attribute in conanfile.py
+    will be used if it exists, otherwise the behavior is like '--build={}'.
+'''
+
+
+def add_lockfile_args(parser):
+    parser.add_argument("-l", "--lockfile", action=OnceArgument,
+                        help="Path to a lockfile.")
+    parser.add_argument("--lockfile-partial", action="store_true",
+                        help="Do not raise an error if some dependency is not found in lockfile")
+    parser.add_argument("--lockfile-out", action=OnceArgument,
+                        help="Filename of the updated lockfile")
+    parser.add_argument("--lockfile-packages", action="store_true",
+                        help="Lock package-id and package-revision information")
+    parser.add_argument("--lockfile-clean", action="store_true", help="remove unused")
+
+
+def _add_common_install_arguments(parser, build_help, update_help=None):
+    if build_help:
+        parser.add_argument("-b", "--build", action=ExtenderValueRequired, nargs="?", help=build_help)
+
+    parser.add_argument("-r", "--remote", action=Extender, default=None,
+                        help='Look in the specified remote or remotes server')
+
+    if not update_help:
+        update_help = ("Will check the remote and in case a newer version and/or revision of "
+                       "the dependencies exists there, it will install those in the local cache. "
+                       "When using version ranges, it will install the latest version that "
+                       "satisfies the range. Also, if using revisions, it will update to the "
+                       "latest revision for the resolved version range.")
+
+    parser.add_argument("-u", "--update", action='store_true', default=False,
+                        help=update_help)
+    add_profiles_args(parser)
+
+
+def add_profiles_args(parser):
+    def profile_args(machine, short_suffix="", long_suffix=""):
+        parser.add_argument("-pr{}".format(short_suffix),
+                            "--profile{}".format(long_suffix),
+                            default=None, action=Extender,
+                            dest='profile_{}'.format(machine),
+                            help='Apply the specified profile to the {} machine'.format(machine))
+
+    def settings_args(machine, short_suffix="", long_suffix=""):
+        parser.add_argument("-s{}".format(short_suffix),
+                            "--settings{}".format(long_suffix),
+                            nargs=1, action=Extender,
+                            dest='settings_{}'.format(machine),
+                            help='Settings to build the package, overwriting the defaults'
+                                 ' ({} machine). e.g.: -s{} compiler=gcc'.format(machine,
+                                                                                 short_suffix))
+
+    def options_args(machine, short_suffix="", long_suffix=""):
+        parser.add_argument("-o{}".format(short_suffix),
+                            "--options{}".format(long_suffix),
+                            nargs=1, action=Extender,
+                            dest="options_{}".format(machine),
+                            help='Define options values ({} machine), e.g.:'
+                                 ' -o{} Pkg:with_qt=true'.format(machine, short_suffix))
+
+    def conf_args(machine, short_suffix="", long_suffix=""):
+        parser.add_argument("-c{}".format(short_suffix),
+                            "--conf{}".format(long_suffix),
+                            nargs=1, action=Extender,
+                            dest='conf_{}'.format(machine),
+                            help='Configuration to build the package, overwriting the defaults'
+                                 ' ({} machine). e.g.: -c{} '
+                                 'tools.cmake.cmaketoolchain:generator=Xcode'.format(machine,
+                                                                                     short_suffix))
+
+    for item_fn in [options_args, profile_args, settings_args, conf_args]:
+        item_fn("host", "",
+                "")  # By default it is the HOST, the one we are building binaries for
+        item_fn("build", ":b", ":build")
+        item_fn("host", ":h", ":host")
+
+
+def add_reference_args(parser):
+    parser.add_argument("--name", action=OnceArgument,
+                        help='Provide a package name if not specified in conanfile')
+    parser.add_argument("--version", action=OnceArgument,
+                        help='Provide a package version if not specified in conanfile')
+    parser.add_argument("--user", action=OnceArgument,
+                        help='Provide a user if not specified in conanfile')
+    parser.add_argument("--channel", action=OnceArgument,
+                        help='Provide a channel if not specified in conanfil')
diff --git a/conan/cli/command.py b/conan/cli/command.py
index 6f2fa5c0d..442ec0f6c 100644
--- a/conan/cli/command.py
+++ b/conan/cli/command.py
@@ -143,7 +143,11 @@ class BaseConanCommand(object):
         kwargs = {}
         if "conan_api" in parameters:
             kwargs["conan_api"] = conan_api
-        formatter(info, **kwargs)
+        if isinstance(info, CommandResult):
+            kwargs.update(info.filter(parameters))
+            formatter(**kwargs)
+        else:
+            formatter(info, **kwargs)
 
 
 class ConanArgumentParser(argparse.ArgumentParser):
@@ -228,3 +232,11 @@ def conan_subcommand(formatters=None):
         return cmd
 
     return decorator
+
+
+class CommandResult:
+    def __init__(self, data):
+        self._data = data
+
+    def filter(self, parameters):
+        return {k: v for k, v in self._data.items() if k in parameters}
diff --git a/conan/cli/commands/build.py b/conan/cli/commands/build.py
index 5b3196807..924e0a367 100644
--- a/conan/cli/commands/build.py
+++ b/conan/cli/commands/build.py
@@ -4,8 +4,10 @@ from conan.api.output import ConanOutput
 from conan.cli.command import conan_command, COMMAND_GROUPS
 from conan.cli.commands import make_abs_path
 from conan.cli.commands.install import graph_compute, _get_conanfile_path
-from conan.cli.common import _add_common_install_arguments, _help_build_policies, \
-    get_multiple_remotes, add_lockfile_args, add_reference_args, save_lockfile_out
+from conan.cli.common import save_lockfile_out
+from conan.api.subapi.remotes import get_multiple_remotes
+from conan.cli.args import add_lockfile_args, _add_common_install_arguments, add_reference_args, \
+    _help_build_policies
 from conan.api.conan_app import ConanApp
 from conans.client.conanfile.build import run_build_method
 
diff --git a/conan/cli/commands/create.py b/conan/cli/commands/create.py
index f1fd78c7a..db92af205 100644
--- a/conan/cli/commands/create.py
+++ b/conan/cli/commands/create.py
@@ -6,10 +6,11 @@ from conan.api.output import ConanOutput, cli_out_write
 from conan.cli.command import conan_command, COMMAND_GROUPS, OnceArgument
 from conan.cli.commands.export import common_args_export
 from conan.cli.commands.install import _get_conanfile_path
-from conan.cli.common import get_lockfile, get_profiles_from_args, _add_common_install_arguments, \
-    _help_build_policies, get_multiple_remotes, add_lockfile_args, scope_options, save_lockfile_out
+from conan.cli.common import get_lockfile, get_profiles_from_args, scope_options, save_lockfile_out
+from conan.api.subapi.remotes import get_multiple_remotes
+from conan.cli.args import add_lockfile_args, _add_common_install_arguments, _help_build_policies
 from conan.api.conan_app import ConanApp
-from conan.cli.formatters.graph import print_graph_basic, print_graph_packages
+from conan.cli.printers.graph import print_graph_basic, print_graph_packages
 from conans.client.conanfile.build import run_build_method
 from conans.errors import ConanException, conanfile_exception_formatter
 from conans.util.files import chdir, mkdir
diff --git a/conan/cli/commands/export.py b/conan/cli/commands/export.py
index 41414e46f..587e02400 100644
--- a/conan/cli/commands/export.py
+++ b/conan/cli/commands/export.py
@@ -4,7 +4,8 @@ import os
 from conan.api.output import ConanOutput, cli_out_write
 from conan.cli.command import conan_command, COMMAND_GROUPS, OnceArgument
 from conan.cli.commands.install import _get_conanfile_path
-from conan.cli.common import get_lockfile, add_reference_args
+from conan.cli.common import get_lockfile
+from conan.cli.args import add_reference_args
 
 
 def common_args_export(parser):
diff --git a/conan/cli/commands/export_pkg.py b/conan/cli/commands/export_pkg.py
index 7d0496744..223a03f35 100644
--- a/conan/cli/commands/export_pkg.py
+++ b/conan/cli/commands/export_pkg.py
@@ -4,8 +4,9 @@ import os
 from conan.api.output import cli_out_write
 from conan.cli.command import conan_command, COMMAND_GROUPS
 from conan.cli.commands.install import _get_conanfile_path
-from conan.cli.common import get_lockfile, add_profiles_args, get_profiles_from_args, \
-    add_lockfile_args, add_reference_args, scope_options, save_lockfile_out
+from conan.cli.common import get_lockfile, get_profiles_from_args, \
+    scope_options, save_lockfile_out
+from conan.cli.args import add_lockfile_args, add_profiles_args, add_reference_args
 
 
 def json_export_pkg(info):
diff --git a/conan/cli/commands/graph.py b/conan/cli/commands/graph.py
index 495ba060b..39657eca5 100644
--- a/conan/cli/commands/graph.py
+++ b/conan/cli/commands/graph.py
@@ -4,12 +4,12 @@ import os
 from conan.api.output import ConanOutput, cli_out_write
 from conan.api.subapi.install import do_deploys
 from conan.cli.command import conan_command, COMMAND_GROUPS, conan_subcommand, \
-    Extender
+    Extender, CommandResult
 from conan.cli.commands import make_abs_path
 from conan.cli.commands.install import graph_compute, common_graph_args
 from conan.cli.common import save_lockfile_out
-from conan.cli.formatters.graph import format_graph_html, format_graph_json, format_graph_dot, \
-    print_graph_info
+from conan.cli.formatters.graph import format_graph_html, format_graph_json, format_graph_dot
+from conan.cli.formatters.graph.graph_info_text import format_graph_info
 from conans.client.graph.install_graph import InstallGraph
 from conans.errors import ConanException
 
@@ -74,7 +74,8 @@ def graph_build_order_merge(conan_api, parser, subparser, *args):
     return install_order_serialized
 
 
-@conan_subcommand(formatters={"html": format_graph_html,
+@conan_subcommand(formatters={"text": format_graph_info,
+                              "html": format_graph_html,
                               "json": format_graph_json,
                               "dot": format_graph_dot})
 def graph_info(conan_api, parser, subparser, *args):
@@ -101,13 +102,12 @@ def graph_info(conan_api, parser, subparser, *args):
 
     deps_graph, lockfile = graph_compute(args, conan_api, partial=args.lockfile_partial,
                                          allow_error=True)
-    if not args.format:
-        # TODO: Convert to text formatter
-        print_graph_info(deps_graph, args.filter, args.package_filter)
 
     save_lockfile_out(args, deps_graph, lockfile, os.getcwd())
     if args.deploy:
         base_folder = os.getcwd()
         do_deploys(conan_api, deps_graph, args.deploy, base_folder)
 
-    return deps_graph
+    return CommandResult({"graph": deps_graph,
+                          "field_filter": args.filter,
+                          "package_filter": args.package_filter})
diff --git a/conan/cli/commands/install.py b/conan/cli/commands/install.py
index 85c44ae84..a7b762ce9 100644
--- a/conan/cli/commands/install.py
+++ b/conan/cli/commands/install.py
@@ -4,10 +4,12 @@ import os
 from conan.api.output import ConanOutput, cli_out_write
 from conan.cli.command import conan_command, Extender, COMMAND_GROUPS
 from conan.cli.commands import make_abs_path
-from conan.cli.common import _add_common_install_arguments, _help_build_policies, \
-    get_profiles_from_args, get_lockfile, get_multiple_remotes, add_lockfile_args, \
-    add_reference_args, scope_options, save_lockfile_out
-from conan.cli.formatters.graph import print_graph_basic, print_graph_packages
+from conan.cli.common import get_profiles_from_args, get_lockfile, scope_options, save_lockfile_out
+from conan.api.subapi.remotes import get_multiple_remotes
+from conan.cli.args import add_lockfile_args, _add_common_install_arguments, add_reference_args, \
+    _help_build_policies
+from conan.cli.printers.graph import print_graph_basic, print_graph_packages
+
 from conans.errors import ConanException
 from conans.model.recipe_ref import RecipeReference
 
diff --git a/conan/cli/commands/list.py b/conan/cli/commands/list.py
index 672f33a79..19916dfd4 100644
--- a/conan/cli/commands/list.py
+++ b/conan/cli/commands/list.py
@@ -4,7 +4,7 @@ from collections import OrderedDict
 from conan.api.output import Color, cli_out_write
 from conan.cli.command import conan_command, conan_subcommand, Extender, COMMAND_GROUPS
 from conan.cli.commands import default_json_formatter
-from conan.cli.common import get_remote_selection
+from conan.api.subapi.remotes import get_remote_selection
 from conan.cli.formatters.list import list_packages_html
 from conans.errors import ConanException, InvalidNameException, NotFoundException
 from conans.model.package_ref import PkgReference
@@ -206,14 +206,14 @@ def list_package_revisions(conan_api, parser, subparser, *args):
     return results
 
 
-def _list_packages_json(data):
-    for remote, d in data.items():
+def _list_packages_json(results):
+    for remote, d in results.items():
         d["reference"] = repr(d["reference"])
         try:
             d["packages"] = {k.repr_notime(): v for k, v in d["packages"].items()}
         except KeyError:
             pass
-    myjson = json.dumps(data, indent=4)
+    myjson = json.dumps(results, indent=4)
     cli_out_write(myjson)
 
 
diff --git a/conan/cli/commands/profile.py b/conan/cli/commands/profile.py
index 241b21798..510f9d9e3 100644
--- a/conan/cli/commands/profile.py
+++ b/conan/cli/commands/profile.py
@@ -3,7 +3,8 @@ import os
 from conan.api.output import ConanOutput, cli_out_write
 from conan.cli.command import conan_command, conan_subcommand, COMMAND_GROUPS
 from conan.cli.commands import default_text_formatter, default_json_formatter
-from conan.cli.common import add_profiles_args, get_profiles_from_args
+from conan.cli.common import get_profiles_from_args
+from conan.cli.args import add_profiles_args
 from conans.errors import ConanException
 from conans.util.files import save
 
diff --git a/conan/cli/commands/search.py b/conan/cli/commands/search.py
index e8d48899a..0ddfdfbc8 100644
--- a/conan/cli/commands/search.py
+++ b/conan/cli/commands/search.py
@@ -3,7 +3,7 @@ from collections import OrderedDict
 from conan.api.conan_api import ConanAPIV2
 from conan.cli.command import conan_command, Extender, COMMAND_GROUPS
 from conan.cli.commands.list import print_list_recipes, default_json_formatter
-from conan.cli.common import get_remote_selection
+from conan.api.subapi.remotes import get_remote_selection
 
 
 # FIXME: "conan search" == "conan list recipes -r="*" -c" --> implement @conan_alias_command??
diff --git a/conan/cli/commands/source.py b/conan/cli/commands/source.py
index 14bec33e9..cc3832ab9 100644
--- a/conan/cli/commands/source.py
+++ b/conan/cli/commands/source.py
@@ -2,7 +2,7 @@ import os
 
 from conan.cli.command import conan_command, COMMAND_GROUPS
 from conan.cli.commands.install import _get_conanfile_path
-from conan.cli.common import add_reference_args
+from conan.cli.args import add_reference_args
 from conan.api.conan_app import ConanApp
 from conans.client.conanfile.configure import run_configure_method
 from conans.client.graph.graph import CONTEXT_HOST
diff --git a/conan/cli/common.py b/conan/cli/common.py
index 34727806f..944a36af7 100644
--- a/conan/cli/common.py
+++ b/conan/cli/common.py
@@ -1,115 +1,10 @@
 import os
 
 from conan.api.output import ConanOutput
-from conan.cli.command import Extender, OnceArgument, ExtenderValueRequired
 from conan.cli.commands import make_abs_path
 from conans.errors import ConanException
 from conans.model.graph_lock import LOCKFILE, Lockfile
 
-_help_build_policies = '''Optional, specify which packages to build from source. Combining multiple
-    '--build' options on one command line is allowed. For dependencies, the optional 'build_policy'
-    attribute in their conanfile.py takes precedence over the command line parameter.
-    Possible parameters:
-
-    --build="*"        Force build for all packages, do not use binary packages.
-    --build=never      Disallow build for all packages, use binary packages or fail if a binary
-                       package is not found. Cannot be combined with other '--build' options.
-    --build=missing    Build packages from source whose binary package is not found.
-    --build=cascade    Build packages from source that have at least one dependency being built from
-                       source.
-    --build=[pattern]  Build packages from source whose package reference matches the pattern. The
-                       pattern uses 'fnmatch' style wildcards.
-    --build=![pattern] Excluded packages, which will not be built from the source, whose package
-                       reference matches the pattern. The pattern uses 'fnmatch' style wildcards.
-
-    Default behavior: If you omit the '--build' option, the 'build_policy' attribute in conanfile.py
-    will be used if it exists, otherwise the behavior is like '--build={}'.
-'''
-
-
-def add_reference_args(parser):
-    parser.add_argument("--name", action=OnceArgument,
-                        help='Provide a package name if not specified in conanfile')
-    parser.add_argument("--version", action=OnceArgument,
-                        help='Provide a package version if not specified in conanfile')
-    parser.add_argument("--user", action=OnceArgument,
-                        help='Provide a user if not specified in conanfile')
-    parser.add_argument("--channel", action=OnceArgument,
-                        help='Provide a channel if not specified in conanfil')
-
-
-def add_profiles_args(parser):
-    def profile_args(machine, short_suffix="", long_suffix=""):
-        parser.add_argument("-pr{}".format(short_suffix),
-                            "--profile{}".format(long_suffix),
-                            default=None, action=Extender,
-                            dest='profile_{}'.format(machine),
-                            help='Apply the specified profile to the {} machine'.format(machine))
-
-    def settings_args(machine, short_suffix="", long_suffix=""):
-        parser.add_argument("-s{}".format(short_suffix),
-                            "--settings{}".format(long_suffix),
-                            nargs=1, action=Extender,
-                            dest='settings_{}'.format(machine),
-                            help='Settings to build the package, overwriting the defaults'
-                                 ' ({} machine). e.g.: -s{} compiler=gcc'.format(machine,
-                                                                                 short_suffix))
-
-    def options_args(machine, short_suffix="", long_suffix=""):
-        parser.add_argument("-o{}".format(short_suffix),
-                            "--options{}".format(long_suffix),
-                            nargs=1, action=Extender,
-                            dest="options_{}".format(machine),
-                            help='Define options values ({} machine), e.g.:'
-                                 ' -o{} Pkg:with_qt=true'.format(machine, short_suffix))
-
-    def conf_args(machine, short_suffix="", long_suffix=""):
-        parser.add_argument("-c{}".format(short_suffix),
-                            "--conf{}".format(long_suffix),
-                            nargs=1, action=Extender,
-                            dest='conf_{}'.format(machine),
-                            help='Configuration to build the package, overwriting the defaults'
-                                 ' ({} machine). e.g.: -c{} '
-                                 'tools.cmake.cmaketoolchain:generator=Xcode'.format(machine,
-                                                                                     short_suffix))
-
-    for item_fn in [options_args, profile_args, settings_args, conf_args]:
-        item_fn("host", "",
-                "")  # By default it is the HOST, the one we are building binaries for
-        item_fn("build", ":b", ":build")
-        item_fn("host", ":h", ":host")
-
-
-def _add_common_install_arguments(parser, build_help, update_help=None):
-    if build_help:
-        parser.add_argument("-b", "--build", action=ExtenderValueRequired, nargs="?", help=build_help)
-
-    parser.add_argument("-r", "--remote", action=Extender, default=None,
-                        help='Look in the specified remote or remotes server')
-
-    if not update_help:
-        update_help = ("Will check the remote and in case a newer version and/or revision of "
-                       "the dependencies exists there, it will install those in the local cache. "
-                       "When using version ranges, it will install the latest version that "
-                       "satisfies the range. Also, if using revisions, it will update to the "
-                       "latest revision for the resolved version range.")
-
-    parser.add_argument("-u", "--update", action='store_true', default=False,
-                        help=update_help)
-    add_profiles_args(parser)
-
-
-def add_lockfile_args(parser):
-    parser.add_argument("-l", "--lockfile", action=OnceArgument,
-                        help="Path to a lockfile.")
-    parser.add_argument("--lockfile-partial", action="store_true",
-                        help="Do not raise an error if some dependency is not found in lockfile")
-    parser.add_argument("--lockfile-out", action=OnceArgument,
-                        help="Filename of the updated lockfile")
-    parser.add_argument("--lockfile-packages", action="store_true",
-                        help="Lock package-id and package-revision information")
-    parser.add_argument("--lockfile-clean", action="store_true", help="remove unused")
-
 
 def get_profiles_from_args(conan_api, args):
     build = [
@@ -123,21 +18,6 @@ def get_profiles_from_args(conan_api, args):
     return profile_host, profile_build
 
 
-def get_remote_selection(conan_api, remote_patterns):
-    """
-    Return a list of Remote() objects matching the specified patterns. If a pattern doesn't match
-    anything, it fails
-    """
-    ret_remotes = []
-    for pattern in remote_patterns:
-        tmp = conan_api.remotes.list(pattern=pattern, only_active=True)
-        if not tmp:
-            raise ConanException("Remotes for pattern '{}' can't be found or are "
-                                 "disabled".format(pattern))
-        ret_remotes.extend(tmp)
-    return ret_remotes
-
-
 def get_lockfile(lockfile_path, cwd, conanfile_path, partial=False):
     if lockfile_path == "None":
         # Allow a way with ``--lockfile=None`` to opt-out automatic usage of conan.lock
@@ -172,14 +52,6 @@ def save_lockfile_out(args, graph, lockfile, cwd):
     return lockfile
 
 
-def get_multiple_remotes(conan_api, remote_names=None):
-    if remote_names:
-        return [conan_api.remotes.get(remote_name) for remote_name in remote_names]
-    elif remote_names is None:
-        # if we don't pass any remotes we want to retrieve only the enabled ones
-        return conan_api.remotes.list(only_active=True)
-
-
 def scope_options(profile, requires, tool_requires):
     """
     Command line helper to scope options when ``command -o myoption=myvalue`` is used,
diff --git a/conan/cli/formatters/graph/__init__.py b/conan/cli/formatters/graph/__init__.py
index 796ee94c3..7e835e7e3 100644
--- a/conan/cli/formatters/graph/__init__.py
+++ b/conan/cli/formatters/graph/__init__.py
@@ -1,6 +1,3 @@
-from .graph import print_graph_basic
-from .graph import print_graph_info
-from .graph import print_graph_packages
 from .graph import format_graph_html
 from .graph import format_graph_dot
 from .graph import format_graph_json
diff --git a/conan/cli/formatters/graph/graph.py b/conan/cli/formatters/graph/graph.py
index aad2dc225..fbb7f89b2 100644
--- a/conan/cli/formatters/graph/graph.py
+++ b/conan/cli/formatters/graph/graph.py
@@ -1,142 +1,18 @@
-import fnmatch
 import json
 import os
 
 from jinja2 import Template, select_autoescape
 
-from conan.api.output import ConanOutput, Color
+
 from conan.api.output import cli_out_write
 from conan.cli.formatters.graph.info_graph_dot import graph_info_dot
 from conan.cli.formatters.graph.info_graph_html import graph_info_html
-from conans.client.graph.graph import CONTEXT_BUILD, RECIPE_CONSUMER, RECIPE_VIRTUAL, BINARY_CACHE, \
+from conans.client.graph.graph import BINARY_CACHE, \
     BINARY_DOWNLOAD, BINARY_BUILD, BINARY_MISSING, BINARY_UPDATE
 from conans.client.installer import build_id
 from conans.util.files import load
 
 
-def print_graph_basic(graph):
-    # I am excluding the "download"-"cache" or remote information, that is not
-    # the definition of the graph, but some history how it was computed
-    # maybe we want to summarize that info after the "GraphBuilder" ends?
-    # TODO: Should all of this be printed from a json representation of the graph? (the same json
-    #   that would be in the json_formatter for the graph?)
-    output = ConanOutput()
-    requires = {}
-    build_requires = {}
-    python_requires = {}
-    deprecated = {}
-    for node in graph.nodes:
-        if hasattr(node.conanfile, "python_requires"):
-            for r in node.conanfile.python_requires._pyrequires.values():  # TODO: improve interface
-                python_requires[r.ref] = r.recipe, r.remote
-        if node.recipe in (RECIPE_CONSUMER, RECIPE_VIRTUAL):
-            continue
-        if node.context == CONTEXT_BUILD:
-            build_requires[node.ref] = node.recipe, node.remote
-        else:
-            requires[node.ref] = node.recipe, node.remote
-        if node.conanfile.deprecated:
-            deprecated[node.ref] = node.conanfile.deprecated
-
-    output.info("Graph root", Color.BRIGHT_YELLOW)
-    path = ": {}".format(graph.root.path) if graph.root.path else ""
-    output.info("    {}{}".format(graph.root, path), Color.BRIGHT_CYAN)
-
-    def _format_requires(title, reqs_to_print):
-        if not reqs_to_print:
-            return
-        output.info(title, Color.BRIGHT_YELLOW)
-        for ref, (recipe, remote) in sorted(reqs_to_print.items()):
-            if remote is not None:
-                recipe = "{} ({})".format(recipe, remote.name)
-            output.info("    {} - {}".format(ref.repr_notime(), recipe), Color.BRIGHT_CYAN)
-
-    _format_requires("Requirements", requires)
-    _format_requires("Build requirements", build_requires)
-    _format_requires("Python requires", python_requires)
-
-    def _format_resolved(title, reqs_to_print):
-        if not reqs_to_print:
-            return
-        output.info(title, Color.BRIGHT_YELLOW)
-        for k, v in sorted(reqs_to_print.items()):
-            output.info("    {}: {}".format(k, v), Color.BRIGHT_CYAN)
-
-    _format_resolved("Resolved alias", graph.aliased)
-    _format_resolved("Resolved version ranges", graph.resolved_ranges)
-
-    if deprecated:
-        output.info("Deprecated", Color.BRIGHT_YELLOW)
-        for d, reason in deprecated.items():
-            reason = f": {reason}" if reason else ""
-            output.info("    {}{}".format(d, reason), Color.BRIGHT_CYAN)
-
-    if graph.error:
-        output.info("Graph error", Color.BRIGHT_RED)
-        output.info("    {}".format(graph.error), Color.BRIGHT_RED)
-
-
-def print_graph_packages(graph):
-    # I am excluding the "download"-"cache" or remote information, that is not
-    # the definition of the graph, but some history how it was computed
-    # maybe we want to summarize that info after the "GraphBuilder" ends?
-    output = ConanOutput()
-    requires = {}
-    build_requires = {}
-    for node in graph.nodes:
-        if node.recipe in (RECIPE_CONSUMER, RECIPE_VIRTUAL):
-            continue
-        if node.context == CONTEXT_BUILD:
-            build_requires[node.pref] = node.binary, node.binary_remote
-        else:
-            requires[node.pref] = node.binary, node.binary_remote
-
-    def _format_requires(title, reqs_to_print):
-        if not reqs_to_print:
-            return
-        output.info(title, Color.BRIGHT_YELLOW)
-        for pref, (status, remote) in sorted(reqs_to_print.items(), key=repr):
-            if remote is not None:
-                status = "{} ({})".format(status, remote.name)
-            output.info("    {} - {}".format(pref.repr_notime(), status), Color.BRIGHT_CYAN)
-
-    _format_requires("Requirements", requires)
-    _format_requires("Build requirements", build_requires)
-
-
-def print_graph_info(deps_graph, field_filter, package_filter):
-    """ More complete graph output, including information for every node in the graph
-    Used for 'graph info' command
-    """
-    out = ConanOutput()
-    out.title("Basic graph information")
-    serial = deps_graph.serialize()
-    for n in serial["nodes"]:
-        if package_filter is not None:
-            display = False
-            for p in package_filter:
-                if fnmatch.fnmatch(n["ref"] or "", p):
-                    display = True
-                    break
-            if not display:
-                continue
-        out.writeln(f"{n['ref']}:")  # FIXME: This can be empty for consumers and it is ugly ":"
-        _serial_pretty_printer(n, field_filter, indent="  ")
-
-
-def _serial_pretty_printer(data, field_filter, indent=""):
-    out = ConanOutput()
-    for k, v in data.items():
-        if field_filter is not None and k not in field_filter:
-            continue
-        if isinstance(v, dict):
-            out.writeln(f"{indent}{k}:")
-            # TODO: increment color too
-            _serial_pretty_printer(v, None, indent=indent+"  ")
-        else:
-            out.writeln(f"{indent}{k}: {v}")
-
-
 class _PrinterGraphItem(object):
     def __init__(self, _id, node, is_build_time_node):
         self.id = _id
@@ -178,7 +54,7 @@ class _PrinterGraphItem(object):
         }
 
 
-class Grapher(object):
+class _Grapher(object):
     def __init__(self, deps_graph):
         self._deps_graph = deps_graph
         self.nodes, self.edges = self._build_graph()
@@ -214,7 +90,7 @@ class Grapher(object):
 
 
 def _render_graph(graph, template, template_folder):
-    graph = Grapher(graph)
+    graph = _Grapher(graph)
     from conans import __version__ as client_version
     template = Template(template, autoescape=select_autoescape(['html', 'xml']))
     return template.render(graph=graph, base_template_path=template_folder, version=client_version)
diff --git a/conan/cli/formatters/graph/graph_info_text.py b/conan/cli/formatters/graph/graph_info_text.py
new file mode 100644
index 000000000..a0a5974ca
--- /dev/null
+++ b/conan/cli/formatters/graph/graph_info_text.py
@@ -0,0 +1,36 @@
+import fnmatch
+
+from conan.api.output import ConanOutput
+
+
+def format_graph_info(graph, field_filter, package_filter):
+    """ More complete graph output, including information for every node in the graph
+    Used for 'graph info' command
+    """
+    out = ConanOutput()
+    out.title("Basic graph information")
+    serial = graph.serialize()
+    for n in serial["nodes"]:
+        if package_filter is not None:
+            display = False
+            for p in package_filter:
+                if fnmatch.fnmatch(n["ref"] or "", p):
+                    display = True
+                    break
+            if not display:
+                continue
+        out.writeln(f"{n['ref']}:")  # FIXME: This can be empty for consumers and it is ugly ":"
+        _serial_pretty_printer(n, field_filter, indent="  ")
+
+
+def _serial_pretty_printer(data, field_filter, indent=""):
+    out = ConanOutput()
+    for k, v in data.items():
+        if field_filter is not None and k not in field_filter:
+            continue
+        if isinstance(v, dict):
+            out.writeln(f"{indent}{k}:")
+            # TODO: increment color too
+            _serial_pretty_printer(v, None, indent=indent+"  ")
+        else:
+            out.writeln(f"{indent}{k}: {v}")
diff --git a/conan/cli/printers/__init__.py b/conan/cli/printers/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/conan/cli/printers/graph.py b/conan/cli/printers/graph.py
new file mode 100644
index 000000000..e3e4a1178
--- /dev/null
+++ b/conan/cli/printers/graph.py
@@ -0,0 +1,92 @@
+from conan.api.output import ConanOutput, Color
+from conans.client.graph.graph import RECIPE_CONSUMER, RECIPE_VIRTUAL, CONTEXT_BUILD
+
+
+def print_graph_basic(graph):
+    # I am excluding the "download"-"cache" or remote information, that is not
+    # the definition of the graph, but some history how it was computed
+    # maybe we want to summarize that info after the "GraphBuilder" ends?
+    # TODO: Should all of this be printed from a json representation of the graph? (the same json
+    #   that would be in the json_formatter for the graph?)
+    output = ConanOutput()
+    requires = {}
+    build_requires = {}
+    python_requires = {}
+    deprecated = {}
+    for node in graph.nodes:
+        if hasattr(node.conanfile, "python_requires"):
+            for r in node.conanfile.python_requires._pyrequires.values():  # TODO: improve interface
+                python_requires[r.ref] = r.recipe, r.remote
+        if node.recipe in (RECIPE_CONSUMER, RECIPE_VIRTUAL):
+            continue
+        if node.context == CONTEXT_BUILD:
+            build_requires[node.ref] = node.recipe, node.remote
+        else:
+            requires[node.ref] = node.recipe, node.remote
+        if node.conanfile.deprecated:
+            deprecated[node.ref] = node.conanfile.deprecated
+
+    output.info("Graph root", Color.BRIGHT_YELLOW)
+    path = ": {}".format(graph.root.path) if graph.root.path else ""
+    output.info("    {}{}".format(graph.root, path), Color.BRIGHT_CYAN)
+
+    def _format_requires(title, reqs_to_print):
+        if not reqs_to_print:
+            return
+        output.info(title, Color.BRIGHT_YELLOW)
+        for ref, (recipe, remote) in sorted(reqs_to_print.items()):
+            if remote is not None:
+                recipe = "{} ({})".format(recipe, remote.name)
+            output.info("    {} - {}".format(ref.repr_notime(), recipe), Color.BRIGHT_CYAN)
+
+    _format_requires("Requirements", requires)
+    _format_requires("Build requirements", build_requires)
+    _format_requires("Python requires", python_requires)
+
+    def _format_resolved(title, reqs_to_print):
+        if not reqs_to_print:
+            return
+        output.info(title, Color.BRIGHT_YELLOW)
+        for k, v in sorted(reqs_to_print.items()):
+            output.info("    {}: {}".format(k, v), Color.BRIGHT_CYAN)
+
+    _format_resolved("Resolved alias", graph.aliased)
+    _format_resolved("Resolved version ranges", graph.resolved_ranges)
+
+    if deprecated:
+        output.info("Deprecated", Color.BRIGHT_YELLOW)
+        for d, reason in deprecated.items():
+            reason = f": {reason}" if reason else ""
+            output.info("    {}{}".format(d, reason), Color.BRIGHT_CYAN)
+
+    if graph.error:
+        output.info("Graph error", Color.BRIGHT_RED)
+        output.info("    {}".format(graph.error), Color.BRIGHT_RED)
+
+
+def print_graph_packages(graph):
+    # I am excluding the "download"-"cache" or remote information, that is not
+    # the definition of the graph, but some history how it was computed
+    # maybe we want to summarize that info after the "GraphBuilder" ends?
+    output = ConanOutput()
+    requires = {}
+    build_requires = {}
+    for node in graph.nodes:
+        if node.recipe in (RECIPE_CONSUMER, RECIPE_VIRTUAL):
+            continue
+        if node.context == CONTEXT_BUILD:
+            build_requires[node.pref] = node.binary, node.binary_remote
+        else:
+            requires[node.pref] = node.binary, node.binary_remote
+
+    def _format_requires(title, reqs_to_print):
+        if not reqs_to_print:
+            return
+        output.info(title, Color.BRIGHT_YELLOW)
+        for pref, (status, remote) in sorted(reqs_to_print.items(), key=repr):
+            if remote is not None:
+                status = "{} ({})".format(status, remote.name)
+            output.info("    {} - {}".format(pref.repr_notime(), status), Color.BRIGHT_CYAN)
+
+    _format_requires("Requirements", requires)
+    _format_requires("Build requirements", build_requires)


Test patch:
diff --git a/conan/cli/commands/test.py b/conan/cli/commands/test.py
index f7da063d3..107bf5e31 100644
--- a/conan/cli/commands/test.py
+++ b/conan/cli/commands/test.py
@@ -4,9 +4,10 @@ from conan.api.output import ConanOutput
 from conan.cli.command import conan_command, COMMAND_GROUPS, OnceArgument
 from conan.cli.commands.create import test_package, _check_tested_reference_matches
 from conan.cli.commands.install import _get_conanfile_path
-from conan.cli.common import get_lockfile, get_profiles_from_args, _add_common_install_arguments, \
-    get_multiple_remotes, add_lockfile_args, save_lockfile_out
-from conan.cli.formatters.graph import print_graph_basic, print_graph_packages
+from conan.cli.common import get_lockfile, get_profiles_from_args, save_lockfile_out
+from conan.api.subapi.remotes import get_multiple_remotes
+from conan.cli.args import add_lockfile_args, _add_common_install_arguments
+from conan.cli.printers.graph import print_graph_basic, print_graph_packages
 from conans.model.recipe_ref import RecipeReference
 
 
diff --git a/conans/test/unittests/client/command/parse_arguments_test.py b/conans/test/unittests/client/command/parse_arguments_test.py
index fd3bda256..178e87968 100644
--- a/conans/test/unittests/client/command/parse_arguments_test.py
+++ b/conans/test/unittests/client/command/parse_arguments_test.py
@@ -5,7 +5,7 @@ import unittest
 
 from parameterized.parameterized import parameterized_class
 
-from conan.cli.common import add_profiles_args
+from conan.cli.args import add_profiles_args
 
 
 @parameterized_class([{"argument": ["options", "-o", "--options"]},