diff --git a/conan/cli/command.py b/conan/cli/command.py index 387112413e1..98cb9309c97 100644 --- a/conan/cli/command.py +++ b/conan/cli/command.py @@ -1,6 +1,6 @@ import argparse -import inspect import textwrap +from contextlib import redirect_stdout from conan.api.output import ConanOutput from conan.errors import ConanException @@ -68,6 +68,8 @@ def _init_formatters(self, parser): if formatters: help_message = "Select the output format: {}".format(", ".join(formatters)) parser.add_argument('-f', '--format', action=OnceArgument, help=help_message) + parser.add_argument("--out-file", action=OnceArgument, + help="Write the output of the command to the specified file instead of stdout.") @property def name(self): @@ -84,7 +86,7 @@ def doc(self): def _format(self, parser, info, *args): parser_args, _ = parser.parse_known_args(*args) - formatarg = getattr(parser_args, "format", "text") + formatarg = getattr(parser_args, "format", None) or "text" out_file = getattr(parser_args, "out_file", None) try: @@ -93,8 +95,11 @@ def _format(self, parser, info, *args): raise ConanException("{} is not a known format. Supported formatters are: {}".format( formatarg, ", ".join(self._help_formatters))) - if out_file and "out_file" in inspect.signature(formatter).parameters.keys(): - formatter(info, out_file=out_file) + if out_file: + with open(out_file, 'w') as f: + with redirect_stdout(f): + formatter(info) + ConanOutput().info(f"Formatted output saved to '{out_file}'") else: formatter(info) diff --git a/conan/cli/commands/graph.py b/conan/cli/commands/graph.py index 3d4c3218713..2b55a4b6718 100644 --- a/conan/cli/commands/graph.py +++ b/conan/cli/commands/graph.py @@ -5,7 +5,7 @@ from conan.api.output import ConanOutput, cli_out_write, Color from conan.cli import make_abs_path from conan.cli.args import common_graph_args, validate_common_graph_args -from conan.cli.command import conan_command, conan_subcommand, OnceArgument +from conan.cli.command import conan_command, conan_subcommand from conan.cli.commands.list import prepare_pkglist_compact, print_serial from conan.cli.formatters.graph import format_graph_html, format_graph_json, format_graph_dot from conan.cli.formatters.graph.build_order_html import format_build_order_html @@ -192,8 +192,6 @@ def graph_info(conan_api, parser, subparser, *args): help="Deployer output folder, base build folder by default if not set") subparser.add_argument("--build-require", action='store_true', default=False, help='Whether the provided reference is a build-require') - subparser.add_argument("--out-file", action=OnceArgument, - help="Filename of the file to save the output of the formatter.") args = parser.parse_args(*args) diff --git a/conan/cli/formatters/graph/graph.py b/conan/cli/formatters/graph/graph.py index 23034b65c62..6b174f603cd 100644 --- a/conan/cli/formatters/graph/graph.py +++ b/conan/cli/formatters/graph/graph.py @@ -11,7 +11,7 @@ BINARY_DOWNLOAD, BINARY_BUILD, BINARY_MISSING, BINARY_UPDATE from conans.client.graph.graph_error import GraphConflictError from conans.client.installer import build_id -from conans.util.files import load, save +from conans.util.files import load # FIXME: Check all this code when format_graph_[html/dot] use serialized graph @@ -138,15 +138,11 @@ def format_graph_dot(result): cli_out_write(_render_graph(graph, None, template, template_folder)) -def format_graph_json(result, out_file=None): +def format_graph_json(result): graph = result["graph"] field_filter = result.get("field_filter") package_filter = result.get("package_filter") serial = graph.serialize() serial = filter_graph(serial, package_filter=package_filter, field_filter=field_filter) json_result = json.dumps({"graph": serial}, indent=4) - if out_file is not None: - ConanOutput().info(f"Formatted output saved to '{out_file}'") - save(out_file, json_result) - return cli_out_write(json_result) diff --git a/test/integration/command/info/info_test.py b/test/integration/command/info/info_test.py index eab7c4849be..8f254820a40 100644 --- a/test/integration/command/info/info_test.py +++ b/test/integration/command/info/info_test.py @@ -435,12 +435,3 @@ def requirements(self): c.run("graph info . -c tools.graph:vendor=build --build='lib*'") c.assert_listed_binary({"lib/1.0": (NO_SETTINGS_PACKAGE_ID, "Build")}) - - -def test_write_formatter_to_file(): - c = TestClient(light=True) - c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) - c.run("graph info . --format=json --out-file=graph.json") - assert "Formatted output saved to 'graph.json'" in c.out - graph = json.loads(c.load("graph.json")) - assert len(graph["graph"]["nodes"]) == 1 diff --git a/test/integration/command_v2/test_output.py b/test/integration/command_v2/test_output.py index 534f2c46525..053588b1432 100644 --- a/test/integration/command_v2/test_output.py +++ b/test/integration/command_v2/test_output.py @@ -1,3 +1,5 @@ +import json + from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient from conan.test.utils.env import environment_update @@ -260,3 +262,32 @@ def test_exception_errors(self): t.run("create . -vquiet", assert_error=True) assert "ERROR: Tagged error" not in t.out assert "ConanException: Untagged error" not in t.out + + +def test_formatter_redirection_to_file(): + c = TestClient(light=True) + c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) + + c.run("config home --out-file=cmd_out.txt") + assert "Formatted output saved to 'cmd_out.txt'" in c.out + cmd_out = c.load("cmd_out.txt") + assert f"{c.cache_folder}\n" == cmd_out + assert not f"{c.cache_folder}\n" == c.stdout + + c.run("graph info . --format=json --out-file=graph.json") + assert "Formatted output saved to 'graph.json'" in c.out + graph = json.loads(c.load("graph.json")) + assert len(graph["graph"]["nodes"]) == 1 + assert not "nodes" in c.stdout + + c.run("graph info . --format=html --out-file=graph.html") + assert "Formatted output saved to 'graph.html'" in c.out + html = c.load("graph.html") + assert "" in html + assert not "" in c.stdout + + c.run("install . --format=json --out-file=graph.json") + assert "Formatted output saved to 'graph.json'" in c.out + graph = json.loads(c.load("graph.json")) + assert len(graph["graph"]["nodes"]) == 1 + assert not "nodes" in c.stdout