Coverage for glotter / project.py: 100%
72 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-05-10 16:13 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-05-10 16:13 +0000
1from typing import Annotated, ClassVar, Dict, List, Optional
3from glotter_core.project import AcronymScheme, CoreProjectMixin
4from pydantic import BaseModel, Field, ValidationInfo, field_validator
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)
17class Project(BaseModel, CoreProjectMixin):
18 VALID_REGEX: ClassVar[str] = "^[0-9a-zA-Z]+$"
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] = {}
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]
38 @field_validator("strings", mode="before")
39 @classmethod
40 def validate_strings(cls, values):
41 validate_str_dict(cls, values)
42 return values
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))
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))
62 if errors:
63 raise_validation_errors(cls, errors)
65 return values
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
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 )
80 repeat = info.data.get("repeat") or {}
81 _validate_test_keys(cls, value, repeat)
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 }
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
101 :params tests: Project with tests to use
102 :params loc_prefix: Optional prefix to apply to validation error locations
103 """
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 )
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 )
123 return errors
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 )
141 if raise_exc and errors:
142 raise_validation_errors(cls, errors)
144 return errors