Coverage for src / competitive_verifier / models / file.py: 100%

135 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-03-05 16:00 +0000

1import enum 

2import pathlib 

3from functools import cached_property 

4from logging import getLogger 

5from typing import TYPE_CHECKING, Any, NamedTuple 

6 

7from pydantic import BaseModel, Field 

8 

9from competitive_verifier.log import GitHubMessageParams 

10from competitive_verifier.util import to_relative 

11 

12from ._scc import SccGraph 

13from .path import ForcePosixPath, SortedPathSet 

14from .verification import Verification 

15 

16if TYPE_CHECKING: 

17 from _typeshed import StrPath 

18 

19 from .result import FileResult 

20logger = getLogger(__name__) 

21 

22_DependencyEdges = dict[pathlib.Path, set[pathlib.Path]] 

23 

24 

25class _DependencyGraph(NamedTuple): 

26 depends_on: _DependencyEdges 

27 required_by: _DependencyEdges 

28 verified_with: _DependencyEdges 

29 

30 

31class DocumentOutputMode(str, enum.Enum): 

32 visible = "visible" 

33 """The document will be output. (default) 

34 """ 

35 

36 hidden = "hidden" 

37 """The document will be output but will not linked from other pages. 

38 """ 

39 

40 no_index = "no-index" 

41 """The document will be output but will not linked from index page. 

42 """ 

43 

44 never = "never" 

45 """The document will be never output. 

46 """ 

47 

48 

49class AddtionalSource(BaseModel): 

50 name: str = Field( 

51 examples=["source_name"], 

52 description="The name of source file.", 

53 ) 

54 """The name of source file. 

55 """ 

56 path: ForcePosixPath = Field( 

57 description="The path source file.", 

58 examples=["relative_path_of_directory/file_name.cpp"], 

59 ) 

60 """The path source file. 

61 """ 

62 

63 

64class VerificationFile(BaseModel): 

65 dependencies: SortedPathSet = Field( 

66 default_factory=set[ForcePosixPath], 

67 description="The list of dependent files as paths relative to root.", 

68 ) 

69 """The list of dependent files as paths relative to root. 

70 """ 

71 verification: list[Verification] | Verification | None = Field( 

72 default_factory=list[Verification] 

73 ) 

74 document_attributes: dict[str, Any] = Field( 

75 default_factory=dict[str, Any], 

76 description="The attributes for documentation.", 

77 ) 

78 """The attributes for documentation. 

79 """ 

80 additonal_sources: list[AddtionalSource] = Field( 

81 default_factory=list[AddtionalSource], 

82 description="The addtional source paths.", 

83 examples=[ 

84 [ 

85 AddtionalSource( 

86 name="source_name", 

87 path=pathlib.Path("relative_path_of_directory/file_name.cpp"), 

88 ), 

89 ], 

90 ], 

91 ) 

92 """The addtional source paths 

93 """ 

94 

95 @property 

96 def title(self) -> str | None: 

97 """The document title specified as a attributes.""" 

98 d = self.document_attributes 

99 return d.get("TITLE") or d.get("document_title") 

100 

101 @property 

102 def display(self) -> DocumentOutputMode | None: 

103 """The document output mode as a attributes.""" 

104 d = self.document_attributes.get("DISPLAY") 

105 if not isinstance(d, str): 

106 return None 

107 try: 

108 return DocumentOutputMode[d.lower().replace("-", "_")] 

109 except KeyError: 

110 return None 

111 

112 @property 

113 def verification_list(self) -> list[Verification]: 

114 if self.verification is None: 

115 return [] 

116 if isinstance(self.verification, list): 

117 return self.verification 

118 return [self.verification] 

119 

120 def is_verification(self) -> bool: 

121 return bool(self.verification) 

122 

123 def is_lightweight_verification(self) -> bool: 

124 """If the effort required for verification is small, treat it as skippable.""" 

125 return self.is_verification() and all( 

126 v.is_lightweight for v in self.verification_list 

127 ) 

128 

129 

130class VerificationInput(BaseModel): 

131 files: dict[ForcePosixPath, VerificationFile] = Field( 

132 default_factory=dict[ForcePosixPath, VerificationFile], 

133 description="The key is relative path from the root.", 

134 ) 

135 

136 def merge(self, other: "VerificationInput") -> "VerificationInput": 

137 return VerificationInput(files=self.files | other.files) 

138 

139 @classmethod 

140 def parse_file_relative(cls, path: "StrPath") -> "VerificationInput": 

141 impl = cls.model_validate_json(pathlib.Path(path).read_bytes()) 

142 new_files: dict[pathlib.Path, VerificationFile] = {} 

143 for p, f in impl.files.items(): 

144 rp = to_relative(p) 

145 if not rp: 

146 logger.warning( 

147 "Files in other directories are not subject to verification: %s", 

148 p, 

149 extra={"github": GitHubMessageParams()}, 

150 ) 

151 continue 

152 f.dependencies = {d for d in map(to_relative, f.dependencies) if d} 

153 new_files[rp] = f 

154 

155 impl.files = new_files 

156 return impl 

157 

158 def scc(self, *, reverse: bool = False) -> list[set[pathlib.Path]]: 

159 """Strongly Connected Component. 

160 

161 Args: 

162 reverse (bool): if True, libraries are ahead. otherwise, tests are ahead 

163 Returns: 

164 list[set[pathlib.Path]]: Strongly Connected Component result 

165 """ 

166 paths = list(self.files.keys()) 

167 vers_rev = {v: i for i, v in enumerate(paths)} 

168 g = SccGraph(len(paths)) 

169 for p, file in self.files.items(): 

170 for e in file.dependencies: 

171 t = vers_rev.get(e, -1) 

172 if t >= 0: 

173 if reverse: 

174 g.add_edge(t, vers_rev[p]) 

175 else: 

176 g.add_edge(vers_rev[p], t) 

177 return [{paths[ix] for ix in ls} for ls in g.scc()] 

178 

179 @cached_property 

180 def transitive_depends_on(self) -> _DependencyEdges: 

181 d: _DependencyEdges = {} 

182 g = self.scc(reverse=True) 

183 for group in g: 

184 result = group.copy() 

185 for p in group: 

186 for dep in self.files[p].dependencies: 

187 if dep not in result: 

188 resolved = d.get(dep) 

189 if resolved is not None: 

190 result.update(resolved) 

191 for p in group: 

192 d[p] = result 

193 

194 return d 

195 

196 @cached_property 

197 def _dependency_graph( 

198 self, 

199 ) -> _DependencyGraph: 

200 """Resolve dependency graphs. 

201 

202 Returns: Dependency graphs 

203 """ 

204 depends_on: _DependencyEdges = {} 

205 required_by: _DependencyEdges = {} 

206 verified_with: _DependencyEdges = {} 

207 

208 # initialize 

209 for path in self.files: 

210 depends_on[path] = set() 

211 required_by[path] = set() 

212 verified_with[path] = set() 

213 

214 # build the graph 

215 for src, vf in self.files.items(): 

216 for dst in vf.dependencies: 

217 if src == dst: 

218 continue 

219 if dst not in depends_on: # pragma: no cover 

220 logger.warning( 

221 "The file `%s` which is depended from `%s` is ignored " 

222 "because it's not listed as a source code file.", 

223 dst, 

224 src, 

225 extra={"github": GitHubMessageParams()}, 

226 ) 

227 continue 

228 

229 depends_on[src].add(dst) 

230 if vf.is_verification(): 

231 verified_with[dst].add(src) 

232 else: 

233 required_by[dst].add(src) 

234 return _DependencyGraph( 

235 depends_on=depends_on, 

236 required_by=required_by, 

237 verified_with=verified_with, 

238 ) 

239 

240 @property 

241 def depends_on(self) -> _DependencyEdges: 

242 return self._dependency_graph.depends_on 

243 

244 @property 

245 def required_by(self) -> _DependencyEdges: 

246 return self._dependency_graph.required_by 

247 

248 @property 

249 def verified_with(self) -> _DependencyEdges: 

250 return self._dependency_graph.verified_with 

251 

252 def filterd_files(self, files: dict[ForcePosixPath, "FileResult"]): 

253 for k, v in files.items(): 

254 if k in self.files: 

255 yield k, v