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
« 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
7from pydantic import BaseModel, Field
9from competitive_verifier.log import GitHubMessageParams
10from competitive_verifier.util import to_relative
12from ._scc import SccGraph
13from .path import ForcePosixPath, SortedPathSet
14from .verification import Verification
16if TYPE_CHECKING:
17 from _typeshed import StrPath
19 from .result import FileResult
20logger = getLogger(__name__)
22_DependencyEdges = dict[pathlib.Path, set[pathlib.Path]]
25class _DependencyGraph(NamedTuple):
26 depends_on: _DependencyEdges
27 required_by: _DependencyEdges
28 verified_with: _DependencyEdges
31class DocumentOutputMode(str, enum.Enum):
32 visible = "visible"
33 """The document will be output. (default)
34 """
36 hidden = "hidden"
37 """The document will be output but will not linked from other pages.
38 """
40 no_index = "no-index"
41 """The document will be output but will not linked from index page.
42 """
44 never = "never"
45 """The document will be never output.
46 """
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 """
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 """
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")
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
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]
120 def is_verification(self) -> bool:
121 return bool(self.verification)
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 )
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 )
136 def merge(self, other: "VerificationInput") -> "VerificationInput":
137 return VerificationInput(files=self.files | other.files)
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
155 impl.files = new_files
156 return impl
158 def scc(self, *, reverse: bool = False) -> list[set[pathlib.Path]]:
159 """Strongly Connected Component.
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()]
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
194 return d
196 @cached_property
197 def _dependency_graph(
198 self,
199 ) -> _DependencyGraph:
200 """Resolve dependency graphs.
202 Returns: Dependency graphs
203 """
204 depends_on: _DependencyEdges = {}
205 required_by: _DependencyEdges = {}
206 verified_with: _DependencyEdges = {}
208 # initialize
209 for path in self.files:
210 depends_on[path] = set()
211 required_by[path] = set()
212 verified_with[path] = set()
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
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 )
240 @property
241 def depends_on(self) -> _DependencyEdges:
242 return self._dependency_graph.depends_on
244 @property
245 def required_by(self) -> _DependencyEdges:
246 return self._dependency_graph.required_by
248 @property
249 def verified_with(self) -> _DependencyEdges:
250 return self._dependency_graph.verified_with
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