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
« prev ^ index » next coverage.py v7.10.7, created at 2026-03-03 02:09 +0000
1"""Container, folder, and test information"""
3from __future__ import annotations
5from dataclasses import dataclass, field
6from typing import Any, Optional
8import yaml
9from jinja2 import BaseLoader, Environment
11from .project import CoreProjectMixin, NamingScheme
14@dataclass(frozen=True)
15class ContainerInfo:
16 """Configuration for a container to run for a directory
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 """
24 image: str = ""
25 tag: str = ""
26 cmd: str = ""
27 build: Optional[str] = None
29 @classmethod
30 def from_dict(cls, dictionary: dict[str, Optional[str]]) -> ContainerInfo:
31 """
32 Create a ContainerInfo object from a dictionary
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)
43 def __bool__(self) -> bool:
44 return bool(self.image and self.tag and self.cmd)
47@dataclass(frozen=True)
48class FolderInfo:
49 """Metadata about sources in a directory
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
55 :ivar extension: the file extension that is considered as source
56 :ivar NamingScheme naming: the naming scheme for files in the directory
57 """
59 extension: str
60 naming: str
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
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
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 }
86 @classmethod
87 def from_dict(cls, dictionary: dict[str, str]) -> FolderInfo:
88 """
89 Create a FileInfo from a dictionary
91 :param dictionary: the dictionary representing FileInfo
92 :return: a new FileInfo
93 """
94 return FolderInfo(dictionary["extension"], dictionary["naming"])
97@dataclass(frozen=True)
98class TestInfo:
99 """An object representation of a testinfo file
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
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 """
114 container_info: ContainerInfo
115 file_info: FolderInfo
116 language_display_name: str
117 notes: list[str] = field(default_factory=list)
119 __test__ = False # Indicate this is not a test
121 @classmethod
122 def from_dict(cls, dictionary: dict[str, Any], language: str) -> TestInfo:
123 """
124 Create a TestInfo object from a dictionary
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 )
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
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)
156 @property
157 def is_testable(self) -> bool:
158 """
159 Indicate if language is testable
161 :return: True if language is testable, False otherwise
162 """
164 return bool(self.container_info)
167LANGUAGE_TEXT_TO_SYMBOL = {"plus": "+", "sharp": "#", "star": "*"}
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 = ""
176 return separator.join(tokens).title()
179__all__ = ["ContainerInfo", "FolderInfo", "TestInfo"]