Coverage for src/pullapprove/checklists.py: 66%

38 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-01-26 14:10 -0600

1""" 

2Configuration models for approval checklists. 

3 

4A simpler alternative to forms for compliance sign-off before approval. 

5Each item is a checkbox with configurable requirement and notes settings. 

6""" 

7 

8from __future__ import annotations 

9 

10import hashlib 

11import json 

12from enum import Enum 

13from typing import Any 

14 

15from pydantic import ( 

16 BaseModel, 

17 ConfigDict, 

18 Field, 

19) 

20 

21 

22class NotesOption(str, Enum): 

23 """Controls when/if notes are shown/required for a checklist item.""" 

24 

25 DISABLED = "disabled" # No notes field 

26 OPTIONAL = "optional" # Notes field available but not required 

27 REQUIRED = "required" # Notes always required 

28 REQUIRED_IF_CHECKED = "required_if_checked" # Notes required when item is checked 

29 REQUIRED_IF_UNCHECKED = "required_if_unchecked" # Notes required when not checked 

30 

31 

32class ChecklistItem(BaseModel): 

33 """A single checklist item. 

34 

35 Settings: 

36 - required: Whether the item must be checked (default: True) 

37 - notes: Controls when/if notes are shown/required (default: "optional") 

38 - help: Optional help text displayed below the label 

39 

40 Notes options: 

41 - "disabled": No notes field 

42 - "optional": Notes field available but not required (default) 

43 - "required": Notes always required 

44 - "required_if_checked": Notes required when item is checked 

45 - "required_if_unchecked": Notes required when item is not checked 

46 """ 

47 

48 model_config = ConfigDict(extra="forbid") 

49 

50 label: str = Field(min_length=1) 

51 required: bool = True 

52 notes: NotesOption = NotesOption.OPTIONAL 

53 help: str = "" 

54 

55 # Runtime values (not part of config, populated during submission) 

56 checked: bool = False 

57 notes_value: str = "" 

58 

59 def notes_required_for_state(self, checked: bool) -> bool: 

60 """Check if notes are required given the checked state.""" 

61 if self.notes == NotesOption.REQUIRED: 

62 return True 

63 if self.notes == NotesOption.REQUIRED_IF_CHECKED and checked: 

64 return True 

65 if self.notes == NotesOption.REQUIRED_IF_UNCHECKED and not checked: 

66 return True 

67 return False 

68 

69 

70class Checklist(BaseModel): 

71 """Configuration for approval checklists.""" 

72 

73 model_config = ConfigDict(extra="forbid") 

74 

75 items: list[ChecklistItem] = Field(min_length=1) 

76 

77 def compute_hash(self) -> str: 

78 """Compute deterministic hash of checklist definition. 

79 

80 Only includes semantic attributes that define the checklist: 

81 - label, required, notes, help 

82 Excludes runtime values: 

83 - checked, notes_value 

84 """ 

85 serialized: list[dict[str, Any]] = [] 

86 for item in self.items: 

87 semantic: dict[str, Any] = { 

88 "label": item.label, 

89 "required": item.required, 

90 "notes": item.notes.value, 

91 "help": item.help, 

92 } 

93 serialized.append(semantic) 

94 canonical = json.dumps(serialized, sort_keys=True) 

95 return hashlib.sha256(canonical.encode()).hexdigest()