Skip to content

Commit f972e62

Browse files
committed
ReST rendering improved
1 parent 3336a03 commit f972e62

File tree

8 files changed

+326
-96
lines changed

8 files changed

+326
-96
lines changed

tmt/config/__init__.py

+42-9
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import functools
22
import os
33
from contextlib import suppress
4-
from typing import Optional, cast
4+
from typing import Optional, TypeVar, cast
55

66
import fmf
77
import fmf.utils
88

99
import tmt.utils
1010
from tmt._compat.pathlib import Path
11+
from tmt._compat.pydantic import ValidationError
1112
from tmt.config.models.link import LinkConfig
13+
from tmt.config.models.themes import Theme, ThemeConfig
14+
from tmt.container import MetadataContainer
15+
16+
MetadataContainerT = TypeVar('MetadataContainerT', bound='MetadataContainer')
1217

1318
# Config directory
1419
DEFAULT_CONFIG_DIR = Path('~/.config/tmt')
@@ -79,24 +84,52 @@ def last_run(self, workdir: Path) -> None:
7984
raise tmt.utils.GeneralError(f"Unable to save last run '{self.path}'.\n{error}")
8085

8186
@functools.cached_property
82-
def fmf_tree(self) -> fmf.Tree:
87+
def fmf_tree(self) -> Optional[fmf.Tree]:
8388
"""
8489
Return the configuration tree
8590
"""
8691

8792
try:
8893
return fmf.Tree(self.path)
89-
except fmf.utils.RootError as error:
90-
raise tmt.utils.MetadataError(f"Config tree not found in '{self.path}'.") from error
94+
except fmf.utils.RootError:
95+
self.logger.debug(f"Config tree not found in '{self.path}'.")
9196

92-
@property
97+
return None
98+
99+
def _parse_config_subtree(
100+
self, path: str, model: type[MetadataContainerT]
101+
) -> Optional[MetadataContainerT]:
102+
if self.fmf_tree is None:
103+
return None
104+
105+
subtree = cast(Optional[fmf.Tree], self.fmf_tree.find(path))
106+
107+
if not subtree:
108+
self.logger.debug(f"Config path '{path}' not found in '{self.path}'.")
109+
110+
return None
111+
112+
try:
113+
return model.parse_obj(subtree.data)
114+
115+
except ValidationError as error:
116+
raise tmt.utils.SpecificationError(
117+
f"Invalid configuration in '{subtree.name}'."
118+
) from error
119+
120+
@functools.cached_property
93121
def link(self) -> Optional[LinkConfig]:
94122
"""
95123
Return the link configuration, if present.
96124
"""
97125

98-
link_config = cast(Optional[fmf.Tree], self.fmf_tree.find('/link'))
99-
if not link_config:
100-
return None
126+
return self._parse_config_subtree('/link', LinkConfig)
127+
128+
@functools.cached_property
129+
def theme(self) -> Theme:
130+
theme_config = self._parse_config_subtree('/theme', ThemeConfig)
131+
132+
if theme_config is None:
133+
return ThemeConfig.get_default_theme()
101134

102-
return LinkConfig.from_fmf(link_config)
135+
return theme_config.get_active_theme()

tmt/config/models/themes.py

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from typing import Any, Optional, Union
2+
3+
import click
4+
5+
import tmt.utils
6+
from tmt._compat.pydantic import ValidationError
7+
from tmt._compat.typing import TypeAlias
8+
from tmt.container import MetadataContainer
9+
10+
Color: TypeAlias = Union[int, tuple[int, int, int], str, None]
11+
12+
13+
class Style(MetadataContainer):
14+
"""
15+
A collection of parameters accepted by :py:func:`click.style`.
16+
"""
17+
18+
fg: Optional[Color] = None
19+
bg: Optional[Color] = None
20+
bold: Optional[bool] = None
21+
dim: Optional[bool] = None
22+
underline: Optional[bool] = None
23+
italic: Optional[bool] = None
24+
blink: Optional[bool] = None
25+
strikethrough: Optional[bool] = None
26+
27+
def apply(self, text: str) -> str:
28+
"""
29+
Apply this style to a given string.
30+
"""
31+
32+
return click.style(text, **self.dict())
33+
34+
35+
_DEFAULT_STYLE = Style()
36+
37+
38+
class Theme(MetadataContainer):
39+
"""
40+
A collection of items tmt uses to colorize various tokens of its CLI.
41+
"""
42+
43+
restructuredtext_text: Style = _DEFAULT_STYLE
44+
45+
restructuredtext_literal: Style = _DEFAULT_STYLE
46+
restructuredtext_emphasis: Style = _DEFAULT_STYLE
47+
restructuredtext_strong: Style = _DEFAULT_STYLE
48+
49+
restructuredtext_literalblock: Style = _DEFAULT_STYLE
50+
restructuredtext_literalblock_yaml: Style = _DEFAULT_STYLE
51+
restructuredtext_literalblock_shell: Style = _DEFAULT_STYLE
52+
53+
restructuredtext_admonition_note: Style = _DEFAULT_STYLE
54+
restructuredtext_admonition_warning: Style = _DEFAULT_STYLE
55+
56+
def to_spec(self) -> dict[str, Any]:
57+
return {key.replace('_', '-'): value for key, value in self.dict().items()}
58+
59+
def to_minimal_spec(self) -> dict[str, Any]:
60+
spec: dict[str, Any] = {}
61+
62+
for theme_key, style in self.to_spec().items():
63+
style_spec = {
64+
style_key: value for style_key, value in style.items() if value is not None
65+
}
66+
67+
if style_spec:
68+
spec[theme_key.replace('_', '-')] = style_spec
69+
70+
return spec
71+
72+
@classmethod
73+
def from_spec(cls: type['Theme'], data: Any) -> 'Theme':
74+
try:
75+
return Theme.parse_obj(data)
76+
77+
except ValidationError as error:
78+
raise tmt.utils.SpecificationError("Invalid theme configuration.") from error
79+
80+
@classmethod
81+
def from_file(cls: type['Theme'], path: tmt.utils.Path) -> 'Theme':
82+
return Theme.from_spec(tmt.utils.yaml_to_dict(path.read_text()))
83+
84+
85+
class ThemeConfig(MetadataContainer):
86+
active_theme: str = 'default'
87+
88+
@classmethod
89+
def load_theme(cls, theme_name: str) -> Theme:
90+
try:
91+
return Theme.from_file(
92+
tmt.utils.resource_files(tmt.utils.Path('config/themes') / f'{theme_name}.yaml')
93+
)
94+
95+
except FileNotFoundError as exc:
96+
raise tmt.utils.GeneralError(f"No such theme '{theme_name}'.") from exc
97+
98+
@classmethod
99+
def get_default_theme(cls) -> Theme:
100+
return cls.load_theme('default')
101+
102+
def get_active_theme(self) -> Theme:
103+
return self.load_theme(self.active_theme)

tmt/config/themes/default.yaml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# restructuredtext-text:
2+
3+
restructuredtext-literal:
4+
fg: green
5+
restructuredtext-emphasis:
6+
fg: blue
7+
restructuredtext-strong:
8+
fg: blue
9+
bold: true
10+
11+
restructuredtext-literalblock:
12+
fg: green
13+
restructuredtext-literalblock-yaml:
14+
fg: cyan
15+
restructuredtext-literalblock-shell:
16+
fg: yellow
17+
18+
restructuredtext-admonition-note:
19+
fg: blue
20+
bold: true
21+
restructuredtext-admonition-warning:
22+
fg: yellow
23+
bold: true

tmt/config/themes/plain.yaml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# restructuredtext-text:
2+
3+
# restructuredtext-literal:
4+
# restructuredtext-emphasis:
5+
# restructuredtext-strong:
6+
7+
# restructuredtext-literalblock:
8+
# restructuredtext-literalblock-yaml:
9+
# restructuredtext-literalblock-shell:
10+
11+
# restructuredtext-admonition-note:
12+
# restructuredtext-admonition-warning:

tmt/steps/prepare/ansible.py

-2
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,6 @@ class PrepareAnsible(tmt.steps.prepare.PreparePlugin[PrepareAnsibleData]):
8282
# https://tmt.readthedocs.io/en/stable/contribute.html#docs
8383
#
8484
"""
85-
Prepare guest using Ansible.
86-
8785
Run Ansible playbooks against the guest, by running
8886
``ansible-playbook`` for all given playbooks.
8987

tmt/trying.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,8 @@ def get_default_plans(self, run: tmt.base.Run) -> list[Plan]:
197197
# Check user config for custom default plans. Search for all
198198
# plans starting with the default user plan name (there might be
199199
# more than just one).
200-
try:
201-
config_tree = tmt.config.Config().fmf_tree
200+
config_tree = tmt.config.Config().fmf_tree
201+
if config_tree is not None:
202202
plan_name = re.escape(USER_PLAN_NAME)
203203
# cast: once fmf is properly annotated, cast() would not be needed.
204204
# pyright isn't able to infer the type.
@@ -209,8 +209,6 @@ def get_default_plans(self, run: tmt.base.Run) -> list[Plan]:
209209
self.tree.tree.update(plan_dict)
210210
self.debug("Use the default user plan config.")
211211
return self.tree.plans(names=[f"^{plan_name}"], run=run)
212-
except MetadataError:
213-
self.debug("User config tree not found.")
214212

215213
# Use the default plan template otherwise
216214
plan_name = re.escape(tmt.templates.DEFAULT_PLAN_NAME)

tmt/utils/jira.py

-2
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,6 @@ def from_issue_url(
9999
link_config = tmt.config.Config().link
100100
except tmt.utils.SpecificationError:
101101
raise
102-
except tmt.utils.MetadataError:
103-
return None
104102
if not link_config:
105103
return None
106104

0 commit comments

Comments
 (0)