Skip to content

Commit 51b2dcd

Browse files
authored
Nicer looking command line output (#3079)
1 parent 8c5d253 commit 51b2dcd

3 files changed

Lines changed: 76 additions & 38 deletions

File tree

esmvalcore/_main.py

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ def show(
190190
"""
191191
import yaml
192192
from nested_lookup import nested_delete
193+
from rich.markdown import Markdown
193194
from rich.syntax import Syntax
194195

195196
from esmvalcore.config import CFG
@@ -203,7 +204,9 @@ def show(
203204
if filter
204205
else ""
205206
)
206-
self._console.print(f"# Current configuration{exclude_msg}:")
207+
self._console.print(
208+
Markdown(f"# Current configuration{exclude_msg}\n<br>"),
209+
)
207210
self._console.print(
208211
Syntax(
209212
yaml.safe_dump(cfg),
@@ -450,28 +453,48 @@ class Recipes:
450453
https://docs.esmvaltool.org/en/latest/recipes/index.html.
451454
"""
452455

453-
@staticmethod
454-
def list() -> None:
456+
def __init__(self) -> None:
457+
from rich.console import Console
458+
459+
self._console = Console(soft_wrap=True)
460+
461+
def list(self) -> None:
455462
"""List all installed recipes.
456463
457464
Show all installed recipes, grouped by folder.
458465
"""
466+
import yaml
467+
from rich.markdown import Markdown
468+
459469
from .config._diagnostics import DIAGNOSTICS
460-
from .config._logging import configure_logging
461470

462-
configure_logging(console_log_level="info")
463471
recipes_folder = DIAGNOSTICS.recipes
464-
logger.info("Showing recipes installed in %s", recipes_folder)
465-
print("# Installed recipes") # noqa: T201
466-
for recipe_root, _, files in sorted(os.walk(recipes_folder)):
467-
root = os.path.relpath(recipe_root, recipes_folder)
468-
if root == ".":
469-
root = ""
470-
if root:
471-
print(f"\n# {root.replace(os.sep, ' - ').title()}") # noqa: T201
472+
messages = [f"# ESMValTool recipes available in `{DIAGNOSTICS.path}`"]
473+
for recipe_root, _, files in sorted(recipes_folder.walk()):
474+
subdir = recipe_root.relative_to(recipes_folder).as_posix()
475+
if subdir == ".":
476+
subdir = ""
477+
if subdir:
478+
messages.append(
479+
f"\n## {subdir.replace(os.sep, ' - ').title()}",
480+
)
472481
for filename in sorted(files):
473-
if filename.endswith(".yml"):
474-
print(os.path.join(root, filename)) # noqa: T201
482+
recipe = recipe_root / filename
483+
if recipe.suffix == ".yml":
484+
title = (
485+
(
486+
yaml.safe_load(
487+
recipe.read_text(encoding="utf-8"),
488+
)
489+
or {}
490+
)
491+
.get("documentation", {})
492+
.get("title", "")
493+
)
494+
messages.append(
495+
f"- `{recipe.relative_to(recipes_folder)}`: {title}",
496+
)
497+
self._console.print(Markdown("\n".join(messages)))
475498

476499
@staticmethod
477500
def get(recipe: str) -> None:
@@ -502,8 +525,7 @@ def get(recipe: str) -> None:
502525
shutil.copy(installed_recipe, Path(recipe).name)
503526
logger.info("Recipe %s successfully copied", recipe)
504527

505-
@staticmethod
506-
def show(recipe: str) -> None:
528+
def show(self, recipe: str) -> None:
507529
"""Show the given recipe in console.
508530
509531
Use this command to see the contents of any installed recipe.
@@ -513,22 +535,27 @@ def show(recipe: str) -> None:
513535
recipe: str
514536
Name of the recipe to get, including any subdirectories.
515537
"""
538+
from rich.markdown import Markdown
539+
from rich.syntax import Syntax
540+
516541
from .config._diagnostics import DIAGNOSTICS
517-
from .config._logging import configure_logging
518542
from .exceptions import RecipeError
519543

520-
configure_logging(console_log_level="info")
521544
installed_recipe = DIAGNOSTICS.recipes / recipe
522545
if not installed_recipe.exists():
523546
msg = (
524547
f"Recipe {recipe} not found. To list all available recipes, "
525548
'execute "esmvaltool list"'
526549
)
527550
raise RecipeError(msg)
528-
msg = f"Recipe {recipe}"
529-
logger.info(msg)
530-
logger.info("=" * len(msg))
531-
print(installed_recipe.read_text(encoding="utf-8")) # noqa: T201
551+
self._console.print(Markdown(f"# Recipe `{recipe}`"))
552+
self._console.print(
553+
Syntax(
554+
installed_recipe.read_text(encoding="utf-8"),
555+
"yaml",
556+
background_color="default",
557+
),
558+
)
532559

533560

534561
class ESMValTool:

tests/integration/test_main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def test_config_show(
208208
with arguments("esmvaltool", "config", "show", "--filter=None"):
209209
run()
210210
stdout = capsys.readouterr().out
211-
expected_header = "# Current configuration:\n"
211+
expected_header = "Current configuration\n"
212212
assert expected_header in stdout
213213
cfg_txt = stdout.split(expected_header)[1]
214214
cfg = yaml.safe_load(cfg_txt)
@@ -222,7 +222,7 @@ def test_config_show_brief_by_default(capsys: pytest.CaptureFixture) -> None:
222222
run()
223223
stdout = capsys.readouterr().out
224224
expected_header = (
225-
"# Current configuration, excluding the keys 'extra_facets':\n"
225+
"Current configuration, excluding the keys 'extra_facets'\n"
226226
)
227227
assert expected_header in stdout
228228
# Check that the configuration that is listed by default is sufficiently

tests/unit/main/test_recipes.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
11
"""Test the `Recipe` class implementing the `esmvaltool recipes` command."""
22

3-
import textwrap
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
46

57
import pytest
8+
import yaml
69

710
import esmvalcore.config._diagnostics
811
from esmvalcore._main import Recipes
912
from esmvalcore.exceptions import RecipeError
1013

14+
if TYPE_CHECKING:
15+
from pathlib import Path
16+
17+
import pytest_mock
1118

12-
def test_list(mocker, tmp_path, capsys):
19+
20+
def test_list(
21+
mocker: pytest_mock.MockerFixture,
22+
tmp_path: Path,
23+
capsys: pytest.CaptureFixture,
24+
) -> None:
1325
"""Test the command `esmvaltool recipes list`."""
14-
recipe_dir = tmp_path
26+
recipe_dir = tmp_path / "recipes"
27+
recipe_dir.mkdir()
1528
recipe1 = recipe_dir / "recipe_test1.yml"
1629
recipe2 = recipe_dir / "subdir" / "recipe_test2.yml"
17-
recipe1.touch()
30+
recipe1.write_text(
31+
yaml.safe_dump({"documentation": {"title": "This is a test recipe"}}),
32+
encoding="utf-8",
33+
)
1834
recipe2.parent.mkdir()
1935
recipe2.touch()
2036

@@ -24,19 +40,14 @@ def test_list(mocker, tmp_path, capsys):
2440
create_autospec=True,
2541
)
2642
diagnostics.recipes = recipe_dir
43+
diagnostics.path = tmp_path
2744

2845
Recipes().list()
2946

3047
msg = capsys.readouterr().out
31-
expected = textwrap.dedent(f"""
32-
# Installed recipes
33-
{recipe1.relative_to(recipe_dir)}
34-
35-
# Subdir
36-
{recipe2.relative_to(recipe_dir)}
37-
""")
38-
print(msg)
39-
assert msg.endswith(expected)
48+
assert f"ESMValTool recipes available in {diagnostics.path}" in msg
49+
assert "\u2022 recipe_test1.yml: This is a test recipe" in msg
50+
assert "\u2022 subdir/recipe_test2.yml" in msg
4051

4152

4253
def test_show(mocker, tmp_path, capsys):

0 commit comments

Comments
 (0)