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

1import os 

2import pathlib 

3from logging import getLogger 

4from typing import Any 

5 

6import yaml 

7from pydantic import AliasChoices, BaseModel, ConfigDict, Field 

8 

9from competitive_verifier import git, github 

10from competitive_verifier.log import GitHubMessageParams 

11from competitive_verifier.models import RelativeDirectoryPath, SortedPathSet 

12 

13from .path_sort import PathSortOrder 

14 

15_CONFIG_YML_PATH = "_config.yml" 

16 

17COMPETITIVE_VERIFY_DOCS_CONFIG_YML = "COMPETITIVE_VERIFY_DOCS_CONFIG_YML" 

18 

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) 

28 

29logger = getLogger(__name__) 

30 

31 

32class ConfigIcons(BaseModel): 

33 model_config = ConfigDict(extra="allow", populate_by_name=True) 

34 

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:" 

43 

44 

45class ConfigYaml(BaseModel): 

46 model_config = ConfigDict(extra="allow") 

47 

48 def merge(self, other: "ConfigYaml"): 

49 return ConfigYaml.model_validate( 

50 self.model_dump() | other.model_dump(), 

51 ) 

52 

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") 

60 

61 basedir: RelativeDirectoryPath | None = None 

62 action_name: str | None = None 

63 branch_name: str | None = None 

64 workflow_name: str | None = None 

65 

66 exclude: list[str] | None = None 

67 description: str = DEFAULT_DOCS_DESCRIPTION 

68 

69 plugins: list[str] = Field( 

70 default_factory=lambda: [ 

71 "jemoji", 

72 "jekyll-redirect-from", 

73 "jekyll-remote-theme", 

74 ] 

75 ) 

76 

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 

101 

102 

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") 

112 

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 

126 

127 

128def load_config_yml(docs_dir: pathlib.Path) -> ConfigYaml: 

129 logger.info("docs_dir=%s", docs_dir) 

130 

131 config_yml = _load_user_render_config_yml(docs_dir) 

132 if not config_yml: 

133 config_yml = ConfigYaml() 

134 

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() 

141 

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