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
« 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 Changes:
12 """Represent a list of changes (tuples of author, [date], issue, [revision], and summary) in sequence."""
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)
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
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}):')
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 = ''
75 self.changes_have_content()
77 if not self.state_code:
78 self.load_changes()
80 if not self.state_code:
81 log.info(f'sequence of changes from ({self.changes_path}) is valid')
83 def is_valid(self) -> bool:
84 """Is the model valid?"""
85 return not self.state_code
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
91 @no_type_check
92 def container(self):
93 """Return the changes sequence."""
94 return copy.deepcopy(self.changes_sequence)