Coverage for src/glotter_core/source.py: 100%
84 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"""Source information"""
3import os
4from dataclasses import dataclass, field
5from pathlib import Path
7import yaml
9from glotter_core.project import CoreProjectMixin, NamingScheme
10from glotter_core.testinfo import TestInfo
13@dataclass(frozen=True)
14class CoreSource:
15 """Metadata about a source file
17 :param filename: filename including extension
18 :param language: the language of the source
19 :param path: path to the file excluding name
20 :param str test_info: a string in yaml format containing testinfo for a directory
21 :param project_type: name of project for this source
23 :ivar filename: filename including extension
24 :ivar language: the language of the source
25 :ivar path: path to the file excluding name
26 :ivar TestInfo test_info: TestInfo object
27 :param project_type: name of project for this source
28 """
30 filename: str
31 language: str
32 path: str
33 test_info: str = field(repr=False)
34 project_type: str
36 def __post_init__(self) -> None:
37 object.__setattr__(self, "test_info", TestInfo.from_string(self.test_info, self))
39 @property
40 def full_path(self) -> str:
41 """Returns the full path to the source including filename and extension"""
42 return str(Path(self.path) / self.filename)
44 @property
45 def name(self) -> str:
46 """Returns the name of the source excluding the extension"""
47 return self.filename.split(".")[0]
49 @property
50 def extension(self) -> str:
51 """Returns the extension of the source"""
52 return "".join(Path(self.filename).suffixes)
55@dataclass
56class CoreLanguage:
57 """
58 Information about a language
60 :ivar sources: list of source objects
61 :ivar test_info: TestInfo object
62 :ivar path: Path to TestInfo object file
63 """
65 sources: list[CoreSource]
66 test_info: TestInfo
67 test_info_path: Path
70@dataclass
71class CoreSourceCategories:
72 """
73 Categories for sources
75 :ivar testable_by_project: dictionary whose key is the project type and
76 whose value is a list of testable source object
77 :ivar by_language: dictionary whose key is the language and whose
78 value is a CoreLanguage object
79 :ivar bad_sources: list of filenames that do not belong to a project
80 """
82 testable_by_project: dict[str, list[CoreSource]] = field(default_factory=dict)
83 by_language: dict[str, list[CoreLanguage]] = field(default_factory=dict)
84 bad_sources: list[str] = field(default_factory=list)
87_IGNORED_FILENAMES = {"untestable.yml", "testinfo.yml", "README.md"}
90def categorize_sources(
91 path: str, projects: dict[str, CoreProjectMixin], source_cls: type
92) -> CoreSourceCategories:
93 """
94 Categorize sources
96 :param path: path to source directory
97 :param projects: dictionary whose key is a project type and whose value is a
98 CoreProjectMixin object
99 :param source_cls: source object class
100 :return: CoreSourceCategories object containing information of the source
101 categories
102 """
104 categories = CoreSourceCategories()
105 categories.testable_by_project = {k: [] for k in projects}
106 orig_path = Path(path).resolve()
107 for root, _, files in os.walk(path):
108 current_path = Path(root).resolve()
109 test_info_string = ""
110 test_info_filename = ""
111 if "testinfo.yml" in files:
112 test_info_filename = "testinfo.yml"
113 test_info_string = Path(current_path, test_info_filename).read_text(encoding="utf-8")
114 elif "untestable.yml" in files:
115 test_info_filename = "untestable.yml"
116 test_info_string = _convert_untestable_to_testinfo(current_path, files, projects)
118 if test_info_string:
119 language = current_path.name
120 test_info = TestInfo.from_dict(yaml.safe_load(test_info_string), language)
121 folder_info = test_info.file_info
122 folder_project_names = folder_info.get_project_mappings(
123 projects, include_extension=True
124 )
125 sources = []
126 test_info_path = Path(current_path, test_info_filename)
127 for project_type, project_name in folder_project_names.items():
128 if project_name in files:
129 source = source_cls(
130 filename=project_name,
131 language=language,
132 path=str(current_path),
133 test_info=test_info_string,
134 project_type=project_type,
135 )
136 sources.append(source)
137 if source.test_info.is_testable:
138 categories.testable_by_project[project_type].append(source)
140 categories.by_language[language] = CoreLanguage(sources, test_info, test_info_path)
142 invalid_filenames = set(files) - (
143 set(folder_project_names.values()) | _IGNORED_FILENAMES
144 )
145 categories.bad_sources += [
146 str(current_path.relative_to(orig_path) / filename)
147 for filename in invalid_filenames
148 ]
150 return categories
153def _convert_untestable_to_testinfo(
154 current_path: Path, files: list[str], projects: dict[str, CoreProjectMixin]
155) -> str:
156 with Path(current_path, "untestable.yml").open(encoding="utf-8") as f:
157 untestable_data = yaml.safe_load(f)
159 notes = untestable_data[0]["reason"]
160 for filename in files:
161 if filename in _IGNORED_FILENAMES:
162 continue
164 base_filename = filename.split(".")[0]
165 extension = "".join(Path(filename).suffixes)
166 project_type = base_filename.lower().replace("-", "").replace("_", "")
167 if project_type in projects and len(projects[project_type].words) > 1:
168 for naming_scheme in NamingScheme:
169 expected_filename = (
170 projects[project_type].get_project_name_by_scheme(naming_scheme) + extension
171 )
172 if filename == expected_filename:
173 test_info_dict = {
174 "folder": {
175 "extension": extension,
176 "naming": naming_scheme.value,
177 },
178 "notes": [notes],
179 }
180 return yaml.dump(test_info_dict, sort_keys=False)
182 return ""
185__all__ = ["CoreLanguage", "CoreSource", "CoreSourceCategories", "categorize_sources"]