diff --git a/hab/cli.py b/hab/cli.py index e69e1cc..579ae43 100644 --- a/hab/cli.py +++ b/hab/cli.py @@ -645,29 +645,30 @@ def dump(settings, uri, env, env_config, report_type, flat, verbosity, format_ty resolver = settings.resolver if report_type in ("uris", "versions", "forest"): - # Allow the user to disable truncation of versions with verbosity flag - truncate = None if verbosity else 3 + from .parsers.format_parser import FormatParser - def echo_line(line): - if line.strip() == line: - click.echo(f"{Fore.GREEN}{line}{Fore.RESET}") - else: - click.echo(line) + formatter = FormatParser(verbosity, color=True) + # Allow the user to disable truncation of versions with verbosity flag + truncate = None if verbosity > 1 else 3 if report_type in ("uris", "forest"): click.echo(f'{Fore.YELLOW}{" URIs ".center(50, "-")}{Fore.RESET}') # Filter out any URI's hidden by the requested verbosity level with utils.verbosity_filter(resolver, verbosity): - for line in resolver.dump_forest(resolver.configs): - echo_line(line) + for line in resolver.dump_forest( + resolver.configs, fmt=formatter.format + ): + click.echo(line) if report_type in ("versions", "forest"): click.echo(f'{Fore.YELLOW}{" Versions ".center(50, "-")}{Fore.RESET}') + for line in resolver.dump_forest( resolver.distros, attr="name", + fmt=formatter.format, truncate=truncate, ): - echo_line(line) + click.echo(line) elif report_type == "all-uris": # Combines all non-placeholder URI's into a single json document and display. # This can be used to compare changes to configs when editing them in bulk. diff --git a/hab/parsers/format_parser.py b/hab/parsers/format_parser.py new file mode 100644 index 0000000..6f07e33 --- /dev/null +++ b/hab/parsers/format_parser.py @@ -0,0 +1,44 @@ +from colorama import Fore + + +class FormatParser: + """Used to format HabBase objects into a readable string. + + Parameters: + top_level (str): Formatting applied to name if this is a top level item. + This is applied to the name value before passed to simple/verbose. + simple (str): A str.format string used to generate the non-verbose output. + verbose (str): A str.format string used to generate the verbose output. + + All of the format strings are provided the kwargs `pre`, `name` and `filename` + + Args: + verbosity (int): Controls the complexity of the output. If zero then + `self.simple` is used, otherwise `self.verbose` is uses. + color (bool, optional): Enables adding color control characters for readability. + """ + + def __init__(self, verbosity, color=True): + self.color = color + self.verbosity = verbosity + + # Configure the format strings + self.simple = "{pre}{name}" + if self.color: + self.verbose = f'{{pre}}{{name}}: {Fore.YELLOW}"{{filename}}"{Fore.RESET}' + self.top_level = f"{Fore.GREEN}{{name}}{Fore.RESET}" + else: + self.verbose = '{pre}{name}: "{filename}"' + self.top_level = "{name}" + + def format(self, parser, attr, pre): + """Format the output of Config or Distro for printing based on verbosity.""" + name = getattr(parser, attr) + if not pre: + name = self.top_level.format(pre=pre, name=name, filename=parser.filename) + + if not self.verbosity or not parser.filename: + fmt = self.simple + else: + fmt = self.verbose + return fmt.format(pre=pre, name=name, filename=parser.filename) diff --git a/hab/resolver.py b/hab/resolver.py index 8d968fe..bc7bf72 100644 --- a/hab/resolver.py +++ b/hab/resolver.py @@ -203,7 +203,9 @@ def dump_forest( attr (str, optional): The name of the attribute to display for each node. If None is passed, the anytree object is returned un-modified. fmt (str, optional): str.format string to control the display of - each node in the forest. Accepts (pre, attr) keys. + each node in the forest. Accepts (pre, attr) keys. If a callable + is passed then it will be called passing (parser, attr=attr, pre=pre) + and should return the text for that hab.parsers instance. style (anytree.render.AbstractStyle, optional): Controls how anytree renders the branch information. If not set, defaults to a custom style that intents all children(recursively) to the same depth. @@ -235,13 +237,13 @@ def sort_forest(items): for row in anytree.RenderTree( forest[tree_name], style=style, childiter=sort_forest ): - cfg = row.node + parser = row.node # Process inheritance for this config to ensure the correct value - cfg._collect_values(cfg, ["min_verbosity"]) + parser._collect_values(parser, ["min_verbosity"]) # Check if this row should be shown based on verbosity settings. - min_verbosity = {"min_verbosity": cfg.min_verbosity} - if not cfg.check_min_verbosity(min_verbosity): + min_verbosity = {"min_verbosity": parser.min_verbosity} + if not parser.check_min_verbosity(min_verbosity): # TODO: If a parent is hidden but not a child, fix rendering # so the child's indent is reduced correctly. continue @@ -254,7 +256,10 @@ def sort_forest(items): pre = row.pre if limit_pre: pre = row.pre[: len(indent)] - yield fmt.format(pre=pre, attr=getattr(cfg, attr)) + if isinstance(fmt, str): + yield fmt.format(pre=pre, attr=getattr(parser, attr)) + else: + yield fmt(parser, attr=attr, pre=pre) def find_distro(self, requirement): """Returns the DistroVersion matching the requirement or None""" diff --git a/tests/test_format_parser.py b/tests/test_format_parser.py new file mode 100644 index 0000000..32e0131 --- /dev/null +++ b/tests/test_format_parser.py @@ -0,0 +1,98 @@ +import pytest +from colorama import Fore + +from hab.parsers.format_parser import FormatParser + +# Short access to the required colorama color codes for formatting +cg = Fore.GREEN +cr = Fore.RESET +cy = Fore.YELLOW + + +@pytest.mark.parametrize( + "uri,pre,color,zero,one", + ( + # Top level URI placeholder is not indented + ("app", "", False, "app", "app"), + ("app", "", True, f"{cg}app{cr}", f"{cg}app{cr}"), + # Child URI is defined, The URI is not turned green and is indented + ("app/aliased", " ", False, " app/aliased", ' app/aliased: "{filename}"'), + ( + "app/aliased", + " ", + True, + " app/aliased", + f' app/aliased: {cy}"{{filename}}"{cr}', + ), + # Grand-child URI is defined, The URI is not turned green and is indented + ( + "app/aliased/mod", + " ", + False, + " app/aliased/mod", + ' app/aliased/mod: "{filename}"', + ), + ( + "app/aliased/mod", + " ", + True, + " app/aliased/mod", + f' app/aliased/mod: {cy}"{{filename}}"{cr}', + ), + # Top level URI is defined so not a placeholder (not indented) + ("project_a", "", False, "project_a", 'project_a: "{filename}"'), + ( + "project_a", + "", + True, + f"{cg}project_a{cr}", + f'{cg}project_a{cr}: {cy}"{{filename}}"{cr}', + ), + # Parent and child URI is not defined, The URI is not turned green and no filename + ("app/houdini", " ", False, " app/houdini", " app/houdini"), + ( + "app/houdini", + " ", + True, + " app/houdini", + " app/houdini", + ), + ), +) +def test_format_parser_uri(config_root, uncached_resolver, uri, pre, color, zero, one): + """Test various uses of `hab.parsers.format_parser.FormatParser`.""" + cfg = uncached_resolver.closest_config(uri) + + # Test verbosity set to zero + formatter = FormatParser(0, color=color) + result = formatter.format(cfg, "uri", pre) + assert result == zero + + # Test verbosity of one + formatter = FormatParser(1, color=color) + result = formatter.format(cfg, "uri", pre) + assert result == one.format(filename=cfg.filename) + + +def test_dump_forest_callable(uncached_resolver, config_root): + """Check that Resolver.dump_forest handles passing a callable to fmt.""" + formatter = FormatParser(1, color=False) + result = [] + for line in uncached_resolver.dump_forest( + uncached_resolver.distros, attr="name", fmt=formatter.format, truncate=3 + ): + result.append(line) + + result = "\n".join(result) + + check = [ + "aliased", + f''' aliased==2.0: "{config_root / 'distros/aliased/2.0/.hab.json'}"''', + "aliased_mod", + f''' aliased_mod==1.0: "{config_root / 'distros/aliased_mod/1.0/.hab.json'}"''', + "aliased_verbosity", + f" aliased_verbosity==1.0: " + f'''"{config_root / 'distros/aliased_verbosity/1.0/.hab.json'}"''', + ] + check = "\n".join(check) + assert result.startswith(check)