Coverage for navigaattori/layout.py: 84.80%
87 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-04 20:46:10 +00:00
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-04 20:46:10 +00:00
1import copy
2import pathlib
3from typing import Union, no_type_check
5import yaml
7from navigaattori import ENCODING, log
10@no_type_check
11class Layout:
12 """Represent the layout."""
14 def layout_has_content(self) -> None:
15 """Ensure we received a none empty top level layout file to bootstrap from."""
16 if not self.layout_path.is_file() or not self.layout_path.stat().st_size:
17 self.state_code = 1
18 self.state_message = f'layout ({self.layout_path}) is no file or empty'
19 log.error(self.state_message)
21 def log_assessed_layout(self) -> None:
22 """Log out the layout we found."""
23 log.info(f'reporting current layout starting from ({self.layout_path}) ...')
24 for key, aspect in self.layout.items():
25 if not isinstance(aspect, dict) or not aspect: 25 ↛ 26line 25 didn't jump to line 26, because the condition on line 25 was never true
26 log.info(f'- {key} -> {aspect}')
27 else:
28 log.info(f'- {key} =>')
29 for this, that in aspect.items():
30 if not isinstance(that, dict) or not that: 30 ↛ 31line 30 didn't jump to line 31, because the condition on line 30 was never true
31 log.info(f' + {this} -> {that}')
32 else:
33 log.info(f' + {this} =>')
34 for k, v in that.items():
35 if not isinstance(v, dict) or not v: 35 ↛ 38line 35 didn't jump to line 38, because the condition on line 35 was never false
36 log.info(f' * {k} -> {v}')
37 else:
38 log.info(f' * {k} =>')
39 for kf, vf in v.items():
40 log.info(f' - {kf} -> {vf}')
42 def load_layout(self) -> None:
43 """Load the layout data."""
44 with open(self.layout_path, 'rt', encoding=ENCODING) as handle:
45 data = yaml.safe_load(handle)
46 if not data:
47 self.state_code = 1
48 self.state_message = 'empty layout?'
49 log.error(f'layout failed to load any entry from ({self.layout_path})')
50 return
51 peeled = data.get('layout', []) if isinstance(data, dict) else []
52 if not peeled or 'layout' not in data:
53 self.state_code = 1
54 self.state_message = 'missing expected top level key document - no layout or wrong file?'
55 log.error(f'layout failed to load anything from ({self.layout_path})')
56 return
57 self.layout: dict[str, dict[str, dict[str, bool]]] = copy.deepcopy(data) # TODO(sthagen) belt and braces
58 log.info(f'layout successfully loaded from ({self.layout_path}):')
59 self.log_assessed_layout()
61 def verify_token_use(self) -> None:
62 """Verify layout uses only tokens from the liitos vocabulary."""
63 log.info(f'verifying layout starting from ({self.layout_path}) uses only tokens from the liitos vocabulary ...')
64 bad_tokens = []
65 common_tokens = sorted(self.layout['layout']['global'])
66 for token in common_tokens:
67 if token not in self.tokens: 67 ↛ 68line 67 didn't jump to line 68, because the condition on line 67 was never true
68 bad_tokens.append(token)
69 log.error(f'- unknown token ({token}) in layout')
71 if bad_tokens: 71 ↛ 72line 71 didn't jump to line 72, because the condition on line 71 was never true
72 badness = len(bad_tokens)
73 tok_sin_plu = 'token' if badness == 1 else 'tokens'
74 self.state_code = 1
75 self.state_message = (
76 f'found {badness} invalid {tok_sin_plu} {tuple(sorted(bad_tokens))}'
77 f' in layout loaded completely starting from ({self.layout_path})'
78 )
79 return
81 common_tokens_count = len(common_tokens)
82 tok_sin_plu = 'token' if common_tokens_count == 1 else 'tokens'
83 token_use = round(100.0 * common_tokens_count / len(self.tokens), 2)
84 log.info(f'layout successfully verified {common_tokens_count} {tok_sin_plu} ({token_use}% of vocabulary)')
86 def __init__(self, layout_path: Union[str, pathlib.Path], options: dict[str, bool]):
87 self._options = options
88 self.quiet: bool = self._options.get('quiet', False)
89 self.strict: bool = self._options.get('strict', False)
90 self.verbose: bool = self._options.get('verbose', False)
91 self.guess: bool = self._options.get('guess', False)
92 self.layout_path: pathlib.Path = pathlib.Path(layout_path)
93 # self.layout: dict[str, dict[str, dict[str, bool]]] = {}
94 self.state_code = 0
95 self.state_message = ''
97 self.layout_has_content()
99 self.tokens = ('has_approvals', 'has_changes', 'has_notices') # HACK A DID ACK
101 if not self.state_code:
102 self.load_layout()
104 if not self.state_code:
105 self.verify_token_use()
107 if not self.state_code:
108 log.info(f'layout from ({self.layout_path}) seems to be valid')
110 def is_valid(self) -> bool:
111 """Is the model valid?"""
112 return not self.state_code
114 def code_details(self) -> tuple[int, str]:
115 """Return an ordered pair of state code and message"""
116 return self.state_code, self.state_message
118 @no_type_check
119 def container(self):
120 """Return the layout."""
121 return copy.deepcopy(self.layout)