Coverage for src/glotter_core/testinfo.py: 100%

64 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-03-03 02:09 +0000

1"""Container, folder, and test information""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass, field 

6from typing import Any, Optional 

7 

8import yaml 

9from jinja2 import BaseLoader, Environment 

10 

11from .project import CoreProjectMixin, NamingScheme 

12 

13 

14@dataclass(frozen=True) 

15class ContainerInfo: 

16 """Configuration for a container to run for a directory 

17 

18 :ivar image: the image to run 

19 :ivar tag: the tag of the image to run 

20 :ivar cmd: the command to run the source inside the container 

21 :ivar build: an optional command to run to build the source before running the command 

22 """ 

23 

24 image: str = "" 

25 tag: str = "" 

26 cmd: str = "" 

27 build: Optional[str] = None 

28 

29 @classmethod 

30 def from_dict(cls, dictionary: dict[str, Optional[str]]) -> ContainerInfo: 

31 """ 

32 Create a ContainerInfo object from a dictionary 

33 

34 :param dictionary: the dictionary representing ContainerInfo 

35 :return: a new ContainerInfo object 

36 """ 

37 image = dictionary.get("image", "") 

38 tag = dictionary.get("tag", "") 

39 cmd = dictionary.get("cmd", "") 

40 build = dictionary.get("build") 

41 return ContainerInfo(image=image, tag=tag, cmd=cmd, build=build) 

42 

43 def __bool__(self) -> bool: 

44 return bool(self.image and self.tag and self.cmd) 

45 

46 

47@dataclass(frozen=True) 

48class FolderInfo: 

49 """Metadata about sources in a directory 

50 

51 :param extension: the file extension that is considered as source 

52 :param str naming: string containing the naming scheme for files in the directory 

53 :raises: :exc:`ValueError` if invalid naming scheme 

54 

55 :ivar extension: the file extension that is considered as source 

56 :ivar NamingScheme naming: the naming scheme for files in the directory 

57 """ 

58 

59 extension: str 

60 naming: str 

61 

62 def __post_init__(self) -> None: 

63 try: 

64 object.__setattr__(self, "naming", NamingScheme[self.naming]) 

65 except KeyError as e: 

66 raise ValueError(f'Unknown naming scheme: "{self.naming}"') from e 

67 

68 def get_project_mappings( 

69 self, projects: dict[str, CoreProjectMixin], include_extension: bool = False 

70 ) -> dict[str, str]: 

71 """ 

72 Uses the naming scheme to generate the expected source names in the directory 

73 and create a mapping from project type to source name 

74 

75 :param project: dictionary whose key is a project type and whose value is 

76 information about the project 

77 :param include_extension: whether to include the extension in the source name 

78 :return: a dict where the key is a project type and the value is the source name 

79 """ 

80 extension = self.extension if include_extension else "" 

81 return { 

82 project_type: f"{project.get_project_name_by_scheme(self.naming)}{extension}" 

83 for project_type, project in projects.items() 

84 } 

85 

86 @classmethod 

87 def from_dict(cls, dictionary: dict[str, str]) -> FolderInfo: 

88 """ 

89 Create a FileInfo from a dictionary 

90 

91 :param dictionary: the dictionary representing FileInfo 

92 :return: a new FileInfo 

93 """ 

94 return FolderInfo(dictionary["extension"], dictionary["naming"]) 

95 

96 

97@dataclass(frozen=True) 

98class TestInfo: 

99 """An object representation of a testinfo file 

100 

101 :param container_info: ContainerInfo object 

102 :param file_info: FolderInfo object 

103 :param language_display_name: string indicating the display name of the 

104 language 

105 :param notes: a list of notes about the language 

106 

107 :ivar container_info: ContainerInfo object 

108 :ivar file_info: FolderInfo object 

109 :ivar language_display_name: string indicating the display name of the 

110 language 

111 :ivar notes: a list of notes about the language 

112 """ 

113 

114 container_info: ContainerInfo 

115 file_info: FolderInfo 

116 language_display_name: str 

117 notes: list[str] = field(default_factory=list) 

118 

119 __test__ = False # Indicate this is not a test 

120 

121 @classmethod 

122 def from_dict(cls, dictionary: dict[str, Any], language: str) -> TestInfo: 

123 """ 

124 Create a TestInfo object from a dictionary 

125 

126 :param dictionary: the dictionary representing a TestInfo object 

127 :param language: language of source object 

128 :return: a new TestInfo object 

129 """ 

130 language_display_name = dictionary.get( 

131 "language_display_name", _get_language_display_name(language) 

132 ) 

133 return TestInfo( 

134 container_info=ContainerInfo.from_dict(dictionary.get("container", {})), 

135 file_info=FolderInfo.from_dict(dictionary["folder"]), 

136 language_display_name=language_display_name, 

137 notes=dictionary.get("notes", []), 

138 ) 

139 

140 @classmethod 

141 def from_string(cls, string: str, source) -> TestInfo: 

142 """ 

143 Create a TestInfo from a string. Modify the string using Jinja2 templating. 

144 Then parse it as yaml 

145 

146 :param string: contents of a testinfo file 

147 :param source: a source object to use for jinja2 template parsing 

148 :param language: language of source 

149 :return: a new TestInfo 

150 """ 

151 template = Environment(loader=BaseLoader).from_string(string) 

152 template_string = template.render(source=source) 

153 info_yaml = yaml.safe_load(template_string) 

154 return cls.from_dict(info_yaml, source.language) 

155 

156 @property 

157 def is_testable(self) -> bool: 

158 """ 

159 Indicate if language is testable 

160 

161 :return: True if language is testable, False otherwise 

162 """ 

163 

164 return bool(self.container_info) 

165 

166 

167LANGUAGE_TEXT_TO_SYMBOL = {"plus": "+", "sharp": "#", "star": "*"} 

168 

169 

170def _get_language_display_name(language: str) -> str: 

171 tokens = [LANGUAGE_TEXT_TO_SYMBOL.get(token, token) for token in language.split("-")] 

172 separator = " " 

173 if any(token in LANGUAGE_TEXT_TO_SYMBOL.values() for token in tokens): 

174 separator = "" 

175 

176 return separator.join(tokens).title() 

177 

178 

179__all__ = ["ContainerInfo", "FolderInfo", "TestInfo"]