diff --git a/src/python/pants/core_tasks/register.py b/src/python/pants/core_tasks/register.py index 66d3e46af99..f6946386bdc 100644 --- a/src/python/pants/core_tasks/register.py +++ b/src/python/pants/core_tasks/register.py @@ -17,7 +17,6 @@ RunTestPrepCommand, ) from pants.core_tasks.substitute_aliased_targets import SubstituteAliasedTargets -from pants.core_tasks.targets_help import TargetsHelp from pants.goal.goal import Goal from pants.goal.task_registrar import TaskRegistrar as task @@ -59,7 +58,6 @@ def register_goals(): # Getting help. task(name="options", action=ExplainOptionsTask).install() - task(name="targets", action=TargetsHelp).install() # Stub for other goals to schedule 'compile'. See noop_exec_task.py for why this is useful. task(name="compile", action=NoopCompile).install("compile") diff --git a/src/python/pants/core_tasks/targets_help.py b/src/python/pants/core_tasks/targets_help.py deleted file mode 100644 index 246950bdb0e..00000000000 --- a/src/python/pants/core_tasks/targets_help.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md). -# Licensed under the Apache License, Version 2.0 (see LICENSE). - - -from colors import blue, cyan, green - -from pants.help.build_dictionary_info_extracter import BuildDictionaryInfoExtracter -from pants.task.console_task import ConsoleTask - - -class TargetsHelp(ConsoleTask): - """List available target types.""" - - _register_console_transitivity_option = False - - @classmethod - def register_options(cls, register): - super().register_options(register) - register("--details", help="Show details about this target type.") - - def console_output(self, targets): - buildfile_aliases = self.context.build_configuration.registered_aliases() - extracter = BuildDictionaryInfoExtracter(buildfile_aliases) - - alias = self.get_options().details - if alias: - tti = next(x for x in extracter.get_target_type_info() if x.symbol == alias) - yield blue("\n{}\n".format(tti.description)) - yield blue("{}(".format(alias)) - - for arg in tti.args: - default = green("(default: {})".format(arg.default) if arg.has_default else "") - yield "{:<30} {}".format( - cyan(" {} = ...,".format(arg.name)), - " {}{}{}".format(arg.description, " " if arg.description else "", default), - ) - - yield blue(")") - else: - for tti in extracter.get_target_type_info(): - yield "{} {}".format(cyan("{:>30}:".format(tti.symbol)), tti.description) diff --git a/src/python/pants/help/BUILD b/src/python/pants/help/BUILD index cfa48f10d24..2b103daf63f 100644 --- a/src/python/pants/help/BUILD +++ b/src/python/pants/help/BUILD @@ -3,16 +3,17 @@ python_library( dependencies=[ + '3rdparty/python:ansicolors', '3rdparty/python:dataclasses', '3rdparty/python:typing-extensions', 'src/python/pants/base:build_environment', - 'src/python/pants/base:exceptions', - 'src/python/pants/build_graph', + 'src/python/pants/base:deprecated', + 'src/python/pants/engine:goal', 'src/python/pants/engine:unions', 'src/python/pants/goal', 'src/python/pants/option:global_options', + 'src/python/pants/option', 'src/python/pants/subsystem', - 'src/python/pants/util:memo', ], tags = {"partially_type_checked"}, ) @@ -21,10 +22,10 @@ python_tests( name='tests', sources=['*_test.py', '!*_integration_test.py'], dependencies=[ - 'src/python/pants/engine/internals:native', - 'src/python/pants/build_graph', - 'src/python/pants/help', + ':help', + '3rdparty/python:dataclasses', 'src/python/pants/option', + 'src/python/pants/option:global_options', 'src/python/pants/subsystem', 'src/python/pants/task', ], diff --git a/src/python/pants/help/build_dictionary_info_extracter.py b/src/python/pants/help/build_dictionary_info_extracter.py deleted file mode 100644 index be41c1a137f..00000000000 --- a/src/python/pants/help/build_dictionary_info_extracter.py +++ /dev/null @@ -1,277 +0,0 @@ -# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md). -# Licensed under the Apache License, Version 2.0 (see LICENSE). - -import inspect -import re -import textwrap -from collections import OrderedDict, namedtuple - -from pants.base.exceptions import TaskError -from pants.build_graph.target import Target -from pants.util.memo import memoized_method - - -class FunctionArg(namedtuple("_FunctionArg", ["name", "description", "has_default", "default"])): - """An argument to a function.""" - - pass - - -class BuildSymbolInfo( - namedtuple("_BuildSymbolInfo", ["symbol", "description", "details_lines", "args"]) -): - """A container for help information about a symbol that can be used in a BUILD file. - - symbol: The name of the symbol. - description: A single line of text providing a summary description. - details_lines: A list of lines of text providing further details (possibly empty). - args: A list of FunctionArg instances. - """ - - def details(self): - return "\n".join(self.details_lines) - - -class BuildDictionaryInfoExtracter: - """Extracts help information about the symbols that may be used in BUILD files.""" - - ADD_DESCR = "" - - basic_target_args = [ - FunctionArg("dependencies", "", True, []), - FunctionArg("description", "", True, None), - FunctionArg("name", "", False, None), - FunctionArg("no_cache", "", True, False), - FunctionArg("tags", "", True, None), - ] - - @classmethod - def _is_custom_init(cls, init_func): - """Check if init derives from object or from a custom implementation. - - In Py2, we could check if __init__ was overridden with inspect.ismethod(obj_type). This no - longer works in Py3, so we have to use an alternative check. - """ - return "slot wrapper" not in str(init_func) - - @classmethod - def get_description_from_docstring(cls, obj): - """Returns a pair (description, details) from the obj's docstring. - - description is a single line. details is a list of subsequent lines, possibly empty. - """ - doc = obj.__doc__ or "" - p = doc.find("\n") - if p == -1: - return doc, [] - else: - description = doc[:p] - details = textwrap.dedent(doc[p + 1 :]).splitlines() - # Remove leading and trailing empty lines. - while details and not details[0].strip(): - details = details[1:] - while details and not details[-1].strip(): - details.pop() - - recording = True - details_without_params = [] - for detail_line in details: - if ":param" in detail_line: - recording = False - if not detail_line.strip(): - recording = True - if recording: - details_without_params.append(detail_line) - return description, details_without_params - - @classmethod - @memoized_method - def _get_stanza_first_line_re(cls): - """Returns a regex that can be used to find the first line of a stanza in a docstring. - - The returned regex can be used to find the first line where there is not a data type in the - arg name (e.g., :param a:), where there is a data type in the arg name (e.g., :param str - a:), where there is a single word between the colons (e.g., :returns:), and where a newline - immediately follows the second colon in the stanza. - """ - return re.compile(r":(\w+)\s*(\w+\s+)?(\w*):\s*(.*)") - - @classmethod - @memoized_method - def _get_default_value_re(cls): - return re.compile(r" \([Dd]efault: (.*)\)") - - @classmethod - def get_arg_descriptions_from_docstring(cls, obj): - """Returns an ordered map of arg name -> arg description found in :param: stanzas.""" - - ret = OrderedDict() - name = "" - doc = obj.__doc__ or "" - lines = [s.strip() for s in doc.split("\n")] - stanza_first_line_re = cls._get_stanza_first_line_re() - for line in lines: - m = stanza_first_line_re.match(line) - if m and m.group(1) == "param": - # If first line of a parameter description, set name and description. - name, description = m.group(3, 4) - ret[name] = description - elif m and m.group(1) != "param": - # If first line of a description of an item other than a parameter, clear name. - name = "" - elif name and line: - # If subsequent line of a parameter description, add to existing description (if any) for - # that parameter. - ret[name] += (" " + line) if ret[name] else line - # Ignore subsequent lines of descriptions of items other than parameters. - return ret - - @classmethod - def get_args_for_target_type(cls, target_type): - return list(cls._get_args_for_target_type(target_type)) - - @classmethod - def _get_args_for_target_type(cls, target_type): - args = {} # name: info. - - # Target.__init__ has several args that are passed to it by TargetAddressable and not by - # the BUILD file author, so we can't naively inspect it. Instead we special-case its - # true BUILD-file-facing arguments here. - for arg in cls.basic_target_args: - args[arg.name] = arg # Don't yield yet; subclass might supply a better description. - - # Non-BUILD-file-facing Target.__init__ args that some Target subclasses capture in their - # own __init__ for various reasons. - ignore_args = {"address", "payload"} - - # Now look at the MRO, in reverse (so we see the more 'common' args first). - # If we see info for an arg, it's more specific than whatever description we have so far, - # so clobber its entry in the args dict. - methods_seen = set() # Ensure we only look at each __init__ method once. - for _type in reversed([t for t in target_type.mro() if issubclass(t, Target)]): - if ( - cls._is_custom_init(_type.__init__) - and _type.__init__ not in methods_seen - and _type.__init__ != Target.__init__ - ): - for arg in cls._get_function_args(_type.__init__): - args[arg.name] = arg - methods_seen.add(_type.__init__) - - for arg_name in sorted(args.keys()): - if arg_name not in ignore_args: - yield args[arg_name] - - @classmethod - def get_function_args(cls, func): - """Returns pairs (arg, default) for each argument of func, in declaration order. - - Ignores *args, **kwargs. Ignores self for methods. - """ - return list(cls._get_function_args(func)) - - @classmethod - def _get_function_args(cls, func): - arg_descriptions = cls.get_arg_descriptions_from_docstring(func) - argspec = inspect.getfullargspec(func) - arg_names = argspec.args - if arg_names and arg_names[0] in {"self", "cls"}: - arg_names = arg_names[1:] - num_defaulted_args = len(argspec.defaults) if argspec.defaults is not None else 0 - first_defaulted_arg = len(arg_names) - num_defaulted_args - for i in range(0, first_defaulted_arg): - yield FunctionArg(arg_names[i], arg_descriptions.pop(arg_names[i], ""), False, None) - for i in range(first_defaulted_arg, len(arg_names)): - yield FunctionArg( - arg_names[i], - arg_descriptions.pop(arg_names[i], ""), - True, - argspec.defaults[i - first_defaulted_arg], - ) - if argspec.varargs: - yield FunctionArg( - "*{}".format(argspec.varargs), - arg_descriptions.pop(argspec.varargs, None), - False, - None, - ) - - if argspec.varkw: - # Any remaining arg_descriptions are for kwargs. - for arg_name, descr in arg_descriptions.items(): - # Get the default value out of the description, if present. - mo = cls._get_default_value_re().search(descr) - default_value = mo.group(1) if mo else None - descr_sans_default = ( - "{}{}".format(descr[: mo.start()], descr[mo.end() :]) if mo else descr - ) - yield FunctionArg(arg_name, descr_sans_default, True, default_value) - - def __init__(self, buildfile_aliases): - self._buildfile_aliases = buildfile_aliases - - def get_target_args(self, alias): - """Returns a list of FunctionArgs for the specified target_type.""" - target_types = list(self._buildfile_aliases.target_types_by_alias.get(alias)) - if not target_types: - raise TaskError("No such target type: {}".format(alias)) - return self.get_args_for_target_type(target_types[0]) - - def get_object_args(self, alias): - obj_type = self._buildfile_aliases.objects.get(alias) - if not obj_type: - raise TaskError("No such object type: {}".format(alias)) - if inspect.isfunction(obj_type) or inspect.ismethod(obj_type): - return self.get_function_args(obj_type) - elif inspect.isclass(obj_type) and self._is_custom_init(obj_type.__init__): - return self.get_function_args(obj_type.__init__) - elif inspect.isclass(obj_type): - return self.get_function_args(obj_type.__new__) - elif hasattr(obj_type, "__call__"): - return self.get_function_args(obj_type.__call__) - else: - return [] - - def get_object_factory_args(self, alias): - obj_factory = self._buildfile_aliases.context_aware_object_factories.get(alias) - if not obj_factory: - raise TaskError("No such context aware object factory: {}".format(alias)) - return self.get_function_args(obj_factory.__call__) - - def get_target_type_info(self): - """Returns a sorted list of BuildSymbolInfo for all known target types.""" - return sorted(self._get_target_type_info()) - - def _get_target_type_info(self): - for alias, target_type in self._buildfile_aliases.target_types.items(): - description, details = self.get_description_from_docstring(target_type) - description = description or self.ADD_DESCR - yield BuildSymbolInfo(alias, description, details, self.get_target_args(alias)) - for alias, target_macro_factory in self._buildfile_aliases.target_macro_factories.items(): - # Take the description from the first target type we encounter that has one. - target_args = self.get_target_args(alias) - for target_type in target_macro_factory.target_types: - description, details = self.get_description_from_docstring(target_type) - if description: - yield BuildSymbolInfo(alias, description, details, target_args) - break - else: - yield BuildSymbolInfo(alias, self.ADD_DESCR, [], target_args) - - def get_object_info(self): - return sorted(self._get_object_info()) - - def _get_object_info(self): - for alias, obj in self._buildfile_aliases.objects.items(): - description, details = self.get_description_from_docstring(obj) - description = description or self.ADD_DESCR - yield BuildSymbolInfo(alias, description, details, self.get_object_args(alias)) - - def get_object_factory_info(self): - return sorted(self._get_object_factory_info()) - - def _get_object_factory_info(self): - for alias, factory_type in self._buildfile_aliases.context_aware_object_factories.items(): - description, details = self.get_description_from_docstring(factory_type) - description = description or self.ADD_DESCR - yield BuildSymbolInfo(alias, description, details, self.get_object_factory_args(alias)) diff --git a/src/python/pants/help/build_dictionary_info_extracter_test.py b/src/python/pants/help/build_dictionary_info_extracter_test.py deleted file mode 100644 index 44919c9e98d..00000000000 --- a/src/python/pants/help/build_dictionary_info_extracter_test.py +++ /dev/null @@ -1,274 +0,0 @@ -# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md). -# Licensed under the Apache License, Version 2.0 (see LICENSE). - -import unittest - -from pants.build_graph.build_file_aliases import BuildFileAliases, TargetMacro -from pants.build_graph.target import Target -from pants.help.build_dictionary_info_extracter import ( - BuildDictionaryInfoExtracter, - BuildSymbolInfo, - FunctionArg, -) - - -class BuildDictionaryInfoExtracterTest(unittest.TestCase): - def setUp(self): - super().setUp() - self.maxDiff = None - - def test_get_description_from_docstring(self): - class Test1(object): - """First line. - - Subsequent - lines. - - with indentations - """ - - self.assertEqual( - ("First line.", ["Subsequent", "lines.", "", " with indentations"]), - BuildDictionaryInfoExtracter.get_description_from_docstring(Test1), - ) - - class Test2(object): - """Only one line.""" - - self.assertEqual( - ("Only one line.", []), - BuildDictionaryInfoExtracter.get_description_from_docstring(Test2), - ) - - def test_get_arg_descriptions_from_docstring(self): - def func(a, b, c): - """Foo function. - - :param a: Parameter a. - :param str b: Parameter b. - :param c: Parameter c. - """ - - self.assertEqual( - {"a": "Parameter a.", "b": "Parameter b.", "c": "Parameter c."}, - BuildDictionaryInfoExtracter.get_arg_descriptions_from_docstring(func), - ) - - def test_get_multiline_arg_descriptions_from_docstring(self): - # Test multiline parameter descriptions, including where all help is on subsequent line. - def func(a, b, c, d, e): - """Foo function. - - :param a: Parameter a. - :param str b: Parameter b. - :param c: Parameter c - Second line Parameter c. - :param d: - Parameter d. - :param e: Parameter e. - """ - - self.assertEqual( - { - "a": "Parameter a.", - "b": "Parameter b.", - "c": "Parameter c Second line Parameter c.", - "d": "Parameter d.", - "e": "Parameter e.", - }, - BuildDictionaryInfoExtracter.get_arg_descriptions_from_docstring(func), - ) - - def test_get_arg_descriptions_with_nonparams_from_docstring(self): - # Test parameter help where help for items other than parameters is present. - def func(a, b, c): - """Foo function. - - :param a: Parameter a. - :type j: Type j. - :type k: Type k. - Second line Type k. - :param str b: Parameter b. - :param c: Parameter c. - :returns: Return. - """ - - self.assertEqual( - {"a": "Parameter a.", "b": "Parameter b.", "c": "Parameter c."}, - BuildDictionaryInfoExtracter.get_arg_descriptions_from_docstring(func), - ) - - def test_get_function_args(self): - # Test standalone function. - def func(arg1, arg2, arg3=42, arg4=None, arg5="foo"): - pass - - self.assertEqual( - [ - FunctionArg("arg1", "", False, None), - FunctionArg("arg2", "", False, None), - FunctionArg("arg3", "", True, 42), - FunctionArg("arg4", "", True, None), - FunctionArg("arg5", "", True, "foo"), - ], - BuildDictionaryInfoExtracter.get_function_args(func), - ) - - # Test member function. - class TestCls: - def __init__(self, arg1, arg2=False): - pass - - self.assertEqual( - [FunctionArg("arg1", "", False, None), FunctionArg("arg2", "", True, False)], - BuildDictionaryInfoExtracter.get_function_args(TestCls.__init__), - ) - - # Test *args, **kwargs situation. - def generic_func(arg1, arg2=42, *args, **kwargs): - """ - :param arg1: The first arg. - :param arg2: The second arg. - :param args: Some extra varargs. - :param arg3: The third arg. - :param arg4: The fourth arg (default: 'Foo'). - """ - - self.assertEqual( - [ - FunctionArg("arg1", "The first arg.", False, None), - FunctionArg("arg2", "The second arg.", True, 42), - FunctionArg("*args", "Some extra varargs.", False, None), - FunctionArg("arg3", "The third arg.", True, None), - FunctionArg("arg4", "The fourth arg.", True, "'Foo'"), - ], - BuildDictionaryInfoExtracter.get_function_args(generic_func), - ) - - def test_get_target_args(self): - class Target1(Target): - def __init__(self, arg1, arg2=42, **kwargs): - """ - :param arg1: The first arg. - :param arg2: The second arg. - """ - super(Target1, self).__init__(**kwargs) - - class Target2(Target1): - pass - - class Target3(Target2): - def __init__(self, arg3, arg4=None, **kwargs): - super(Target1, self).__init__(**kwargs) - - self.assertEqual( - sorted( - BuildDictionaryInfoExtracter.basic_target_args - + [ - FunctionArg("arg1", "The first arg.", False, None), - FunctionArg("arg2", "The second arg.", True, 42), - FunctionArg("arg3", "", False, None), - FunctionArg("arg4", "", True, None), - ] - ), - sorted(BuildDictionaryInfoExtracter.get_args_for_target_type(Target3)), - ) - - # Check a trivial case. - class Target4(Target): - pass - - self.assertEqual( - BuildDictionaryInfoExtracter.basic_target_args, - BuildDictionaryInfoExtracter.get_args_for_target_type(Target4), - ) - - def test_get_target_type_info(self): - class Target1(Target): - """Target1 docstring.""" - - pass - - class Target2(Target): - """Target2 docstring.""" - - pass - - class Target3(Target): - """Target3 docstring.""" - - pass - - # We shouldn't get as far as invoking the context factory, so it can be trivial. - macro_factory = TargetMacro.Factory.wrap(lambda ctx: None, Target2) - - bfa = BuildFileAliases( - targets={"target1": Target1, "target2": macro_factory, "target3": Target3}, - objects={}, - context_aware_object_factories={}, - ) - - extracter = BuildDictionaryInfoExtracter(bfa) - args = BuildDictionaryInfoExtracter.basic_target_args - self.assertEqual( - [ - BuildSymbolInfo("target1", "Target1 docstring.", [], args), - BuildSymbolInfo("target2", "Target2 docstring.", [], args), - BuildSymbolInfo("target3", "Target3 docstring.", [], args), - ], - extracter.get_target_type_info(), - ) - - def test_get_object_info(self): - class Foo: - """Foo docstring.""" - - def __init__(self, bar, baz=42): - """ - :param bar: Bar details. - :param int baz: Baz details. - """ - - bfa = BuildFileAliases(targets={}, objects={"foo": Foo}, context_aware_object_factories={},) - extracter = BuildDictionaryInfoExtracter(bfa) - self.assertEqual( - [ - BuildSymbolInfo( - "foo", - "Foo docstring.", - [], - [ - FunctionArg("bar", "Bar details.", False, None), - FunctionArg("baz", "Baz details.", True, 42), - ], - ) - ], - extracter.get_object_info(), - ) - - def test_get_object_factory_info(self): - class Foo: - """Foo docstring.""" - - def __call__(self, bar, baz=42): - """ - :param bar: Bar details. - :param int baz: Baz details. - """ - - bfa = BuildFileAliases(targets={}, objects={}, context_aware_object_factories={"foo": Foo}) - extracter = BuildDictionaryInfoExtracter(bfa) - self.assertEqual( - [ - BuildSymbolInfo( - "foo", - "Foo docstring.", - [], - [ - FunctionArg("bar", "Bar details.", False, None), - FunctionArg("baz", "Baz details.", True, 42), - ], - ) - ], - extracter.get_object_factory_info(), - ) diff --git a/tests/python/pants_test/pantsd/test_pantsd_integration.py b/tests/python/pants_test/pantsd/test_pantsd_integration.py index ba41bde406b..66c5c571118 100644 --- a/tests/python/pants_test/pantsd/test_pantsd_integration.py +++ b/tests/python/pants_test/pantsd/test_pantsd_integration.py @@ -148,7 +148,7 @@ def test_pantsd_aligned_output(self) -> None: # Set for pytest output display. self.maxDiff = None - cmds = [["goals"], ["help"], ["targets"], ["roots"]] + cmds = [["goals"], ["help"], ["target-types"], ["roots"]] non_daemon_runs = [self.run_pants(cmd) for cmd in cmds]