Coverage for src / competitive_verifier / documents / config.py: 85%
82 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-06-05 08:59 +0900
« prev ^ index » next coverage.py v7.13.1, created at 2026-06-05 08:59 +0900
1import os
2import pathlib
3from logging import getLogger
4from typing import Any
6import yaml
7from pydantic import AliasChoices, BaseModel, ConfigDict, Field
9from competitive_verifier import git, github
10from competitive_verifier.log import GitHubMessageParams
11from competitive_verifier.models import RelativeDirectoryPath, SortedPathSet
13from .path_sort import PathSortOrder
15_CONFIG_YML_PATH = "_config.yml"
17COMPETITIVE_VERIFY_DOCS_CONFIG_YML = "COMPETITIVE_VERIFY_DOCS_CONFIG_YML"
19DEFAULT_DOCS_GENERATOR_REPOSITORY = "NotLeonian/competitive-verifier"
20DEFAULT_DOCS_GENERATOR_REPOSITORY_URL = (
21 f"https://github.com/{DEFAULT_DOCS_GENERATOR_REPOSITORY}"
22)
23DEFAULT_DOCS_DESCRIPTION = (
24 "<small>This documentation is automatically generated by "
25 f'<a href="{DEFAULT_DOCS_GENERATOR_REPOSITORY_URL}">'
26 f"{DEFAULT_DOCS_GENERATOR_REPOSITORY}</a></small>"
27)
29logger = getLogger(__name__)
32class ConfigIcons(BaseModel):
33 model_config = ConfigDict(extra="allow", populate_by_name=True)
35 LIBRARY_ALL_AC: str = ":heavy_check_mark:"
36 LIBRARY_PARTIAL_AC: str = ":heavy_check_mark:"
37 LIBRARY_SOME_WA: str = ":question:"
38 LIBRARY_ALL_WA: str = ":x:"
39 LIBRARY_NO_TESTS: str = ":warning:"
40 TEST_ACCEPTED: str = ":heavy_check_mark:"
41 TEST_WRONG_ANSWER: str = ":x:"
42 TEST_WAITING_JUDGE: str = ":warning:"
45class ConfigYaml(BaseModel):
46 model_config = ConfigDict(extra="allow")
48 def merge(self, other: "ConfigYaml"):
49 return ConfigYaml.model_validate(
50 self.model_dump() | other.model_dump(),
51 )
53 def model_dump_yml(self):
54 d = self.model_dump(
55 mode="json",
56 by_alias=True,
57 exclude_none=True,
58 )
59 return yaml.safe_dump(d, encoding="utf-8", line_break="\n")
61 basedir: RelativeDirectoryPath | None = None
62 action_name: str | None = None
63 branch_name: str | None = None
64 workflow_name: str | None = None
66 exclude: list[str] | None = None
67 description: str = DEFAULT_DOCS_DESCRIPTION
69 plugins: list[str] = Field(
70 default_factory=lambda: [
71 "jemoji",
72 "jekyll-redirect-from",
73 "jekyll-remote-theme",
74 ]
75 )
77 theme: str = "jekyll-theme-minimal"
78 remote_theme: str | None = None
79 mathjax: str | int = 3
80 highlightjs_style: str = Field(
81 default="default",
82 serialization_alias="highlightjs-style",
83 )
84 filename_index: bool = Field(
85 default=False,
86 serialization_alias="filename-index",
87 )
88 path_sort: PathSortOrder | None = Field(
89 default=None,
90 validation_alias=AliasChoices("path-sort", "path_sort"),
91 serialization_alias="path-sort",
92 description=(
93 "Sort order of generated document links. "
94 "If omitted, lexicographic order is used for compatibility. "
95 "Use 'natural' to sort digit runs numerically."
96 ),
97 )
98 sass: dict[str, Any] = Field(default_factory=lambda: {"style": "compressed"})
99 icons: ConfigIcons = ConfigIcons()
100 consolidate: SortedPathSet | None = None
103def _load_user_render_config_yml(docs_dir: pathlib.Path) -> ConfigYaml | None:
104 env_config_yml = os.getenv(COMPETITIVE_VERIFY_DOCS_CONFIG_YML)
105 if env_config_yml: 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true
106 logger.info("Parse $COMPETITIVE_VERIFY_DOCS_CONFIG_YML: %s", env_config_yml)
107 try:
108 user_config_yml = yaml.safe_load(env_config_yml)
109 return ConfigYaml.model_validate(user_config_yml)
110 except Exception:
111 logger.exception("Failed to parse $COMPETITIVE_VERIFY_DOCS_CONFIG_YML")
113 user_config_yml_path = docs_dir / _CONFIG_YML_PATH
114 if user_config_yml_path.exists():
115 logger.info("Parse user _config.yml: %s", user_config_yml_path)
116 try:
117 user_config_yml = yaml.safe_load(user_config_yml_path.read_bytes())
118 return ConfigYaml.model_validate(user_config_yml)
119 except Exception:
120 logger.exception(
121 "Failed to parse _config.yml: %s",
122 user_config_yml_path,
123 extra={"github": GitHubMessageParams(file=user_config_yml_path)},
124 )
125 return None
128def load_config_yml(docs_dir: pathlib.Path) -> ConfigYaml:
129 logger.info("docs_dir=%s", docs_dir)
131 config_yml = _load_user_render_config_yml(docs_dir)
132 if not config_yml:
133 config_yml = ConfigYaml()
135 if not config_yml.action_name: 135 ↛ 137line 135 didn't jump to line 137 because the condition on line 135 was always true
136 config_yml.action_name = github.env.get_workflow_name()
137 if not config_yml.branch_name: 137 ↛ 139line 137 didn't jump to line 139 because the condition on line 137 was always true
138 config_yml.branch_name = github.env.get_ref_name()
139 if not config_yml.workflow_name: 139 ↛ 142line 139 didn't jump to line 142 because the condition on line 139 was always true
140 config_yml.workflow_name = github.env.get_workflow_filename()
142 if not config_yml.basedir: 142 ↛ 145line 142 didn't jump to line 145 because the condition on line 142 was always true
143 git_root = git.get_root_directory().resolve()
144 config_yml.basedir = pathlib.Path.cwd().resolve().relative_to(git_root)
145 return config_yml