From d40f13fadbfa7ddb10b421b0da5dbcd576d441f3 Mon Sep 17 00:00:00 2001 From: Benjy Weinberger Date: Wed, 6 Nov 2019 14:26:57 -0500 Subject: [PATCH] A mixin for v2 goals that have non-line-oriented output. (#8565) The LineOriented mixin is reimplemented as a subclass. Also detangles print_stderr from this. Callers can access that directly at console.print_stderr. --- src/python/pants/engine/goal.py | 69 ++++++++++++++------- src/python/pants/engine/goal_test.py | 14 +++++ src/python/pants/rules/core/binary.py | 2 +- src/python/pants/rules/core/filedeps.py | 2 +- src/python/pants/rules/core/list_roots.py | 2 +- src/python/pants/rules/core/list_targets.py | 4 +- 6 files changed, 64 insertions(+), 29 deletions(-) create mode 100644 src/python/pants/engine/goal_test.py diff --git a/src/python/pants/engine/goal.py b/src/python/pants/engine/goal.py index 08b5ef03b44..396a804f635 100644 --- a/src/python/pants/engine/goal.py +++ b/src/python/pants/engine/goal.py @@ -109,42 +109,63 @@ class _GoalOptions(object): """A marker trait for the anonymous inner `Goal.Options` classes for `Goal`s.""" -class LineOriented: - """A mixin for Goal that adds Options to support the `line_oriented` context manager.""" +class Outputting: + """A mixin for Goal that adds options to support output-related context managers. + + Allows output to go to a file or to stdout. + + Useful for goals whose purpose is to emit output to the end user (as distinct from incidental logging to stderr). + """ @classmethod def register_options(cls, register): super().register_options(register) - register('--sep', default='\\n', metavar='', - help='String to use to separate result lines.') register('--output-file', metavar='', - help='Write line-oriented output to this file instead.') + help='Output to this file. If unspecified, outputs to stdout.') @classmethod @contextmanager - def line_oriented(cls, line_oriented_options, console): - """Given Goal.Options and a Console, yields functions for writing to stdout and stderr, respectively. + def output(cls, options, console): + """Given Goal.Options and a Console, yields a function for writing data to stdout, or a file. - The passed options instance will generally be the `Goal.Options` of a `LineOriented` `Goal`. + The passed options instance will generally be the `Goal.Options` of an `Outputting` `Goal`. """ - if type(line_oriented_options) != cls.Options: - raise AssertionError( - 'Expected Options for `{}`, got: {}'.format(cls.__name__, line_oriented_options)) - - output_file = line_oriented_options.values.output_file - sep = line_oriented_options.values.sep.encode().decode('unicode_escape') + with cls.output_sink(options, console) as output_sink: + yield lambda msg: output_sink.write(msg) - if output_file: - stdout_file = open(output_file, 'w') - print_stdout = lambda msg: print(msg, file=stdout_file, end=sep) + @classmethod + @contextmanager + def output_sink(cls, options, console): + if type(options) != cls.Options: + raise AssertionError('Expected Options for `{}`, got: {}'.format(cls.__name__, options)) + stdout_file = None + if options.values.output_file: + stdout_file = open(options.values.output_file, 'w') + output_sink = stdout_file else: - print_stdout = lambda msg: console.print_stdout(msg, end=sep) - - print_stderr = lambda msg: console.print_stderr(msg) - + output_sink = console.stdout try: - yield print_stdout, print_stderr + yield output_sink finally: - if output_file: + output_sink.flush() + if stdout_file: stdout_file.close() - console.flush() + + +class LineOriented(Outputting): + @classmethod + def register_options(cls, register): + super().register_options(register) + register('--sep', default='\\n', metavar='', + help='String to use to separate lines in line-oriented output.') + + @classmethod + @contextmanager + def line_oriented(cls, options, console): + """Given Goal.Options and a Console, yields a function for printing lines to stdout or a file. + + The passed options instance will generally be the `Goal.Options` of an `Outputting` `Goal`. + """ + sep = options.values.sep.encode().decode('unicode_escape') + with cls.output_sink(options, console) as output_sink: + yield lambda msg: print(msg, file=output_sink, end=sep) diff --git a/src/python/pants/engine/goal_test.py b/src/python/pants/engine/goal_test.py new file mode 100644 index 00000000000..95bbfd076c4 --- /dev/null +++ b/src/python/pants/engine/goal_test.py @@ -0,0 +1,14 @@ +# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from pants.engine.goal import Outputting +from pants_test.engine.util import MockConsole + + +def test_outputting_goal(): + class DummyGoal(Outputting): + pass + + console = MockConsole() + with DummyGoal.output(None, console) as output: + pass diff --git a/src/python/pants/rules/core/binary.py b/src/python/pants/rules/core/binary.py index cf91a6475d1..31d1f677686 100644 --- a/src/python/pants/rules/core/binary.py +++ b/src/python/pants/rules/core/binary.py @@ -32,7 +32,7 @@ class CreatedBinary: @console_rule def create_binary(addresses: BuildFileAddresses, console: Console, workspace: Workspace, options: Binary.Options) -> Binary: - with Binary.line_oriented(options, console) as (print_stdout, print_stderr): + with Binary.line_oriented(options, console) as print_stdout: print_stdout("Generating binaries in `dist/`") binaries = yield [Get(CreatedBinary, Address, address.to_address()) for address in addresses] dirs_to_materialize = tuple( diff --git a/src/python/pants/rules/core/filedeps.py b/src/python/pants/rules/core/filedeps.py index 86b4ee71749..5fb01f720bd 100644 --- a/src/python/pants/rules/core/filedeps.py +++ b/src/python/pants/rules/core/filedeps.py @@ -35,7 +35,7 @@ def file_deps( if hasattr(hydrated_target.adaptor, "sources"): uniq_set.update(hydrated_target.adaptor.sources.snapshot.files) - with Filedeps.line_oriented(filedeps_options, console) as (print_stdout, print_stderr): + with Filedeps.line_oriented(filedeps_options, console) as print_stdout: for f_path in uniq_set: print_stdout(f_path) diff --git a/src/python/pants/rules/core/list_roots.py b/src/python/pants/rules/core/list_roots.py index 65d12f0bc88..90bd4257078 100644 --- a/src/python/pants/rules/core/list_roots.py +++ b/src/python/pants/rules/core/list_roots.py @@ -46,7 +46,7 @@ def all_roots(source_root_config: SourceRootConfig) -> AllSourceRoots: @console_rule def list_roots(console: Console, options: Roots.Options, all_roots: AllSourceRoots) -> Roots: - with Roots.line_oriented(options, console) as (print_stdout, print_stderr): + with Roots.line_oriented(options, console) as print_stdout: for src_root in sorted(all_roots, key=lambda x: x.path): all_langs = ','.join(sorted(src_root.langs)) print_stdout(f"{src_root.path}: {all_langs or '*'}") diff --git a/src/python/pants/rules/core/list_targets.py b/src/python/pants/rules/core/list_targets.py index 716abf9d6b0..2e160b81b3a 100644 --- a/src/python/pants/rules/core/list_targets.py +++ b/src/python/pants/rules/core/list_targets.py @@ -68,9 +68,9 @@ def print_documented(target): collection = yield Get(BuildFileAddresses, Specs, specs) print_fn = lambda address: address.spec - with List.line_oriented(list_options, console) as (print_stdout, print_stderr): + with List.line_oriented(list_options, console) as print_stdout: if not collection.dependencies: - print_stderr('WARNING: No targets were matched in goal `{}`.'.format('list')) + console.print_stderr('WARNING: No targets were matched in goal `{}`.'.format('list')) for item in collection: result = print_fn(item)