Coverage for navigaattori/changes.py: 100.00%

65 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 Changes: 

12 """Represent a list of changes (tuples of author, [date], issue, [revision], and summary) in sequence.""" 

13 

14 def changes_have_content(self) -> None: 

15 """Ensure we received a folder to bootstrap.""" 

16 if not self.changes_path.is_file() or not self.changes_path.stat().st_size: 

17 self.state_code = 1 

18 self.state_message = f'changes ({self.changes_path}) is no file or empty' 

19 log.error(self.state_message) 

20 

21 def load_changes(self) -> None: 

22 """Load the sequence of changes tuples (author, [date], issue, [revision], and summary).""" 

23 with open(self.changes_path, 'rt', encoding=ENCODING) as handle: 

24 data = yaml.safe_load(handle) 

25 if not data: 

26 self.state_code = 1 

27 self.state_message = 'empty changes?' 

28 log.error(f'changes sequence failed to load any entry from ({self.changes_path})') 

29 return 

30 peeled = data.get('changes', []) if isinstance(data, dict) else [] 

31 if not peeled or 'changes' not in data: 

32 self.state_code = 1 

33 self.state_message = 'no changes or wrong file?' 

34 log.error(f'changes sequence failed to load anything from ({self.changes_path})') 

35 return 

36 for entry in peeled: 

37 if not isinstance(entry, dict) or 'author' not in entry or 'summary' not in entry: 

38 self.state_code = 1 

39 self.state_message = ( 

40 f'no map or one of the required keys (author, summary) missing in {entry}' 

41 f' of changes read from ({self.changes_path})?' 

42 ) 

43 log.error(f'changes sequence failed to load entry ({entry}) from ({self.changes_path})') 

44 return 

45 

46 self.changes_sequence = [] 

47 for entry in peeled: 

48 change = { 

49 'author': entry.get('author', 'AUTHOR-MISSING'), 

50 'date': entry.get('date', ''), 

51 'issue': entry.get('issue', ''), 

52 'revision': entry.get('revision', ''), 

53 'summary': entry.get('summary', 'SUMMARY-MISSING'), 

54 } 

55 self.changes_sequence.append(change) 

56 self.changes_count = len(self.changes_sequence) 

57 changes_sin_plu = 'change' if self.changes_count == 1 else 'changes' 

58 log.info(f'changes sequence loaded {self.changes_count} {changes_sin_plu} from ({self.changes_path}):') 

59 for entry in self.changes_sequence: 

60 log.info(f'- {entry}') 

61 log.info(f'changes sequence successfully loaded from ({self.changes_path}):') 

62 

63 def __init__(self, changes_path: Union[str, pathlib.Path], options: dict[str, bool]): 

64 self._options = options 

65 self.quiet: bool = self._options.get('quiet', False) 

66 self.strict: bool = self._options.get('strict', False) 

67 self.verbose: bool = self._options.get('verbose', False) 

68 self.guess: bool = self._options.get('guess', False) 

69 self.changes_path: pathlib.Path = pathlib.Path(changes_path) 

70 self.changes_sequence = [] 

71 self.changes_count = len(self.changes_sequence) 

72 self.state_code = 0 

73 self.state_message = '' 

74 

75 self.changes_have_content() 

76 

77 if not self.state_code: 

78 self.load_changes() 

79 

80 if not self.state_code: 

81 log.info(f'sequence of changes from ({self.changes_path}) is valid') 

82 

83 def is_valid(self) -> bool: 

84 """Is the model valid?""" 

85 return not self.state_code 

86 

87 def code_details(self) -> tuple[int, str]: 

88 """Return an ordered pair of state code and message""" 

89 return self.state_code, self.state_message 

90 

91 @no_type_check 

92 def container(self): 

93 """Return the changes sequence.""" 

94 return copy.deepcopy(self.changes_sequence)