Coverage for glotter / project.py: 100%

72 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-05-10 16:13 +0000

1from typing import Annotated, ClassVar, Dict, List, Optional 

2 

3from glotter_core.project import AcronymScheme, CoreProjectMixin 

4from pydantic import BaseModel, Field, ValidationInfo, field_validator 

5 

6from glotter.auto_gen_test import AutoGenTest, AutoGenUseTests 

7from glotter.errors import ( 

8 InitErrorDetails, 

9 get_error_details, 

10 raise_simple_validation_error, 

11 raise_validation_errors, 

12 validate_str_dict, 

13 validate_str_list, 

14) 

15 

16 

17class Project(BaseModel, CoreProjectMixin): 

18 VALID_REGEX: ClassVar[str] = "^[0-9a-zA-Z]+$" 

19 

20 words: Annotated[ 

21 List[Annotated[str, Field(min_length=1, pattern=VALID_REGEX, strict=True)]], 

22 Field(min_length=1, strict=True), 

23 ] 

24 requires_parameters: bool = False 

25 strings: Dict[str, str] = {} 

26 acronyms: List[Annotated[str, Field(min_length=1, pattern=VALID_REGEX, strict=True)]] = [] 

27 acronym_scheme: AcronymScheme = AcronymScheme.two_letter_limit 

28 use_tests: Optional[AutoGenUseTests] = None 

29 repeat: Dict[str, int] = {} 

30 tests: Dict[str, AutoGenTest] = {} 

31 

32 @field_validator("acronyms", mode="before") 

33 @classmethod 

34 def get_acronym(cls, values): 

35 validate_str_list(cls, values) 

36 return [value.upper() for value in values] 

37 

38 @field_validator("strings", mode="before") 

39 @classmethod 

40 def validate_strings(cls, values): 

41 validate_str_dict(cls, values) 

42 return values 

43 

44 @field_validator("repeat", mode="before") 

45 @classmethod 

46 def validate_repeat(cls, values, info: ValidationInfo): 

47 errors = [] 

48 if not isinstance(values, dict): 

49 errors.append(get_error_details("Input should be a valid dictionary", (), values)) 

50 else: 

51 for key, value in values.items(): 

52 if not isinstance(key, str): 

53 errors.append(get_error_details("Key should be a valid string", (key,), key)) 

54 

55 if not isinstance(value, int): 

56 errors.append( 

57 get_error_details("Value should be a valid integer", (key,), value) 

58 ) 

59 elif value < 1: 

60 errors.append(get_error_details("Value should be at least 1", (key,), value)) 

61 

62 if errors: 

63 raise_validation_errors(cls, errors) 

64 

65 return values 

66 

67 @field_validator("tests", mode="before") 

68 @classmethod 

69 def get_tests(cls, value, info: ValidationInfo): 

70 if not isinstance(value, dict) or not all( 

71 isinstance(test, dict) for test in value.values() 

72 ): 

73 return value 

74 

75 if info.data.get("use_tests"): 

76 raise_simple_validation_error( 

77 cls, '"tests" and "use_tests" items are mutually exclusive', {"use_tests": "..."} 

78 ) 

79 

80 repeat = info.data.get("repeat") or {} 

81 _validate_test_keys(cls, value, repeat) 

82 

83 return { 

84 test_name: { 

85 **test, 

86 "requires_parameters": info.data.get("requires_parameters") or False, 

87 "strings": info.data.get("strings") or {}, 

88 "name": test_name, 

89 "repeat": repeat.get(test_name, 1), 

90 } 

91 for test_name, test in value.items() 

92 } 

93 

94 def set_tests( 

95 self, project: "Project", loc_prefix: Optional[tuple] = None 

96 ) -> List[InitErrorDetails]: 

97 """ 

98 If there is a "use_tests" item, then set the specified tests, renaming them 

99 according to the "use_tests" item. The "use_tests" item is then removed 

100 

101 :params tests: Project with tests to use 

102 :params loc_prefix: Optional prefix to apply to validation error locations 

103 """ 

104 

105 loc = loc_prefix or () 

106 errors = [] 

107 if self.use_tests: 

108 self.tests = {} 

109 for test_name_, test in project.tests.items(): 

110 test_name = test_name_.replace(self.use_tests.search, self.use_tests.replace) 

111 self.tests[test_name] = AutoGenTest( 

112 **test.model_dump(exclude={"name", "repeat"}), 

113 name=test_name, 

114 repeat=self.repeat.get(test_name, 1), 

115 ) 

116 

117 self.requires_parameters = project.requires_parameters 

118 self.use_tests = None 

119 errors += _validate_test_keys( 

120 self.__class__, self.tests, self.repeat, raise_exc=False, loc_prefix=loc 

121 ) 

122 

123 return errors 

124 

125 

126def _validate_test_keys( 

127 cls, tests, repeat, raise_exc: bool = True, loc_prefix: Optional[tuple] = None 

128) -> List[InitErrorDetails]: 

129 loc = loc_prefix or () 

130 errors = [] 

131 for test_name in repeat: 

132 if isinstance(test_name, str) and test_name not in tests: 

133 errors.append( 

134 get_error_details( 

135 f"Refers to a non-existent test name {test_name}", 

136 loc + ("repeat", test_name), 

137 test_name, 

138 ) 

139 ) 

140 

141 if raise_exc and errors: 

142 raise_validation_errors(cls, errors) 

143 

144 return errors