Coverage for src / competitive_verifier / documents / config.py: 84%

76 statements  

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

1import os 

2import pathlib 

3from logging import getLogger 

4from typing import Any 

5 

6import yaml 

7from pydantic import BaseModel, ConfigDict, Field 

8 

9from competitive_verifier import git, github 

10from competitive_verifier.log import GitHubMessageParams 

11from competitive_verifier.models import RelativeDirectoryPath, SortedPathSet 

12 

13_CONFIG_YML_PATH = "_config.yml" 

14 

15COMPETITIVE_VERIFY_DOCS_CONFIG_YML = "COMPETITIVE_VERIFY_DOCS_CONFIG_YML" 

16 

17logger = getLogger(__name__) 

18 

19 

20class ConfigIcons(BaseModel): 

21 model_config = ConfigDict(extra="allow") 

22 

23 LIBRARY_ALL_AC: str = ":heavy_check_mark:" 

24 LIBRARY_PARTIAL_AC: str = ":heavy_check_mark:" 

25 LIBRARY_SOME_WA: str = ":question:" 

26 LIBRARY_ALL_WA: str = ":x:" 

27 LIBRARY_NO_TESTS: str = ":warning:" 

28 TEST_ACCEPTED: str = ":heavy_check_mark:" 

29 TEST_WRONG_ANSWER: str = ":x:" 

30 TEST_WAITING_JUDGE: str = ":warning:" 

31 

32 

33class ConfigYaml(BaseModel): 

34 model_config = ConfigDict(extra="allow") 

35 

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

37 return ConfigYaml.model_validate( 

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

39 ) 

40 

41 def model_dump_yml(self): 

42 d = self.model_dump( 

43 mode="json", 

44 by_alias=True, 

45 exclude_none=True, 

46 ) 

47 return yaml.safe_dump(d, encoding="utf-8", line_break="\n") 

48 

49 basedir: RelativeDirectoryPath | None = None 

50 action_name: str | None = None 

51 branch_name: str | None = None 

52 workflow_name: str | None = None 

53 

54 exclude: list[str] | None = None 

55 description: str = ( 

56 "<small>This documentation is automatically generated by " 

57 '<a href="https://github.com/competitive-verifier/competitive-verifier">competitive-verifier/competitive-verifier</a>' 

58 "</small>" 

59 ) 

60 

61 plugins: list[str] = Field( 

62 default_factory=lambda: [ 

63 "jemoji", 

64 "jekyll-redirect-from", 

65 "jekyll-remote-theme", 

66 ] 

67 ) 

68 

69 theme: str = "jekyll-theme-minimal" 

70 mathjax: str | int = 3 

71 highlightjs_style: str = Field( 

72 default="default", 

73 serialization_alias="highlightjs-style", 

74 ) 

75 filename_index: bool = Field( 

76 default=False, 

77 serialization_alias="filename-index", 

78 ) 

79 sass: dict[str, Any] = Field(default_factory=lambda: {"style": "compressed"}) 

80 icons: ConfigIcons = ConfigIcons() 

81 consolidate: SortedPathSet | None = None 

82 

83 

84def _load_user_render_config_yml(docs_dir: pathlib.Path) -> ConfigYaml | None: 

85 env_config_yml = os.getenv(COMPETITIVE_VERIFY_DOCS_CONFIG_YML) 

86 if env_config_yml: 86 ↛ 87line 86 didn't jump to line 87 because the condition on line 86 was never true

87 logger.info("Parse $COMPETITIVE_VERIFY_DOCS_CONFIG_YML: %s", env_config_yml) 

88 try: 

89 user_config_yml = yaml.safe_load(env_config_yml) 

90 return ConfigYaml.model_validate(user_config_yml) 

91 except Exception: 

92 logger.exception("Failed to parse $COMPETITIVE_VERIFY_DOCS_CONFIG_YML") 

93 

94 user_config_yml_path = docs_dir / _CONFIG_YML_PATH 

95 if user_config_yml_path.exists(): 

96 logger.info("Parse user _config.yml: %s", user_config_yml_path) 

97 try: 

98 user_config_yml = yaml.safe_load(user_config_yml_path.read_bytes()) 

99 return ConfigYaml.model_validate(user_config_yml) 

100 except Exception: 

101 logger.exception( 

102 "Failed to parse _config.yml: %s", 

103 user_config_yml_path, 

104 extra={"github": GitHubMessageParams(file=user_config_yml_path)}, 

105 ) 

106 return None 

107 

108 

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

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

111 

112 config_yml = _load_user_render_config_yml(docs_dir) 

113 if not config_yml: 

114 config_yml = ConfigYaml() 

115 

116 if not config_yml.action_name: 116 ↛ 118line 116 didn't jump to line 118 because the condition on line 116 was always true

117 config_yml.action_name = github.env.get_workflow_name() 

118 if not config_yml.branch_name: 118 ↛ 120line 118 didn't jump to line 120 because the condition on line 118 was always true

119 config_yml.branch_name = github.env.get_ref_name() 

120 if not config_yml.workflow_name: 120 ↛ 123line 120 didn't jump to line 123 because the condition on line 120 was always true

121 config_yml.workflow_name = github.env.get_workflow_filename() 

122 

123 if not config_yml.basedir: 123 ↛ 126line 123 didn't jump to line 126 because the condition on line 123 was always true

124 git_root = git.get_root_directory().resolve() 

125 config_yml.basedir = pathlib.Path.cwd().resolve().relative_to(git_root) 

126 return config_yml