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

1import copy 

2import pathlib 

3from typing import Union, no_type_check 

4 

5import yaml 

6 

7from navigaattori import ENCODING, log 

8 

9 

10@no_type_check 

11class Layout: 

12 """Represent the layout.""" 

13 

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) 

20 

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}') 

41 

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() 

60 

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') 

70 

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 

80 

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)') 

85 

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 = '' 

96 

97 self.layout_has_content() 

98 

99 self.tokens = ('has_approvals', 'has_changes', 'has_notices') # HACK A DID ACK 

100 

101 if not self.state_code: 

102 self.load_layout() 

103 

104 if not self.state_code: 

105 self.verify_token_use() 

106 

107 if not self.state_code: 

108 log.info(f'layout from ({self.layout_path}) seems to be valid') 

109 

110 def is_valid(self) -> bool: 

111 """Is the model valid?""" 

112 return not self.state_code 

113 

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 

117 

118 @no_type_check 

119 def container(self): 

120 """Return the layout.""" 

121 return copy.deepcopy(self.layout)