Coverage for sphinx_ifelse/directives.py: 100%

100 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2025-03-26 16:18 +0000

1import copy 

2 

3from docutils import nodes 

4 

5from sphinx.util.docutils import SphinxDirective 

6 

7from sphinx.util import logging 

8 

9from sphinx_ifelse.utils import directive2location, remove_all_childs_of_types 

10 

11 

12logger = logging.getLogger(__name__) 

13 

14 

15class IfElseNode(nodes.General, nodes.Element): 

16 

17 def __init__(self, 

18 condition: str | None, 

19 evaluatedto: bool, 

20 previously_evaluatedtoTrue: bool, 

21 location): 

22 self.condition = condition 

23 self.evaluatedto = evaluatedto 

24 self.previously_evaluatedtoTrue = previously_evaluatedtoTrue 

25 self.location = location 

26 super().__init__() 

27 

28 def already_evaluatedtoTrue(self): 

29 return self.evaluatedto or self.previously_evaluatedtoTrue 

30 

31 

32class IfNode(IfElseNode): 

33 def __init__(self, 

34 condition: str = '', 

35 evaluatedto: bool = False, 

36 location = None): 

37 super().__init__(condition = condition, 

38 evaluatedto = evaluatedto, 

39 previously_evaluatedtoTrue = False, 

40 location = location) 

41 

42 

43class ElIfNode(IfElseNode): 

44 def __init__(self, 

45 condition: str = '', 

46 evaluatedto: bool = False, 

47 previously_evaluatedtoTrue:bool = False, 

48 location = None): 

49 super().__init__(condition = condition, 

50 evaluatedto = evaluatedto, 

51 previously_evaluatedtoTrue = previously_evaluatedtoTrue, 

52 location = location) 

53 

54 

55class ElseNode(IfElseNode): 

56 def __init__(self, 

57 previously_evaluatedtoTrue: bool = False, 

58 location = None): 

59 super().__init__(condition = None, 

60 evaluatedto = not previously_evaluatedtoTrue, 

61 previously_evaluatedtoTrue = previously_evaluatedtoTrue, 

62 location = location) 

63 

64 

65def process_ifelse_nodes(app, doctree, fromdocname): 

66 nodetypes = [IfNode, ElIfNode, ElseNode] 

67 remove_all_childs_of_types(doctree, nodetypes) 

68 return 

69 

70 

71class AbstractIfElseDirective(SphinxDirective): 

72 """ 

73 Abstract class for common if/else logic. 

74 """ 

75 

76 def evaluate_condition(self, condition:str)->bool: 

77 """ 

78 Determines if a previous sibling directive (if or elif) 

79 has already evaluated to True. 

80 

81 Returns: 

82 bool: True if a previous sibling directive evaluated to True, 

83 otherwise False. 

84 """ 

85 

86 env = self.state.document.settings.env 

87 app = env.app 

88 

89 class_name = self.__class__.__name__ 

90 

91 variants = app.config.ifelse_variants 

92 

93 # eval will change the globals variable, we have to avoid this, 

94 # so we create a deep copy 

95 variants_deep_copy = copy.deepcopy(variants) 

96 

97 try: 

98 proceed = eval(condition, variants_deep_copy) 

99 except Exception as err: 

100 logger.warning( 

101 f"{class_name}: exception while evaluating expression: {err}", 

102 type="ifelse", 

103 subtype=class_name, 

104 location=directive2location(self) 

105 ) 

106 proceed = True 

107 

108 return proceed 

109 

110 def fetch_already_evaluatedtoTrue(self)->bool: 

111 """ 

112 Fetches the result of a already evaluated condition. 

113 

114 Returns: 

115 bool: `True` if the condition was already evaluated to `True`, 

116 otherwise `False`. 

117 """ 

118 

119 class_name = self.__class__.__name__ 

120 

121 parent = self.state.parent 

122 previously_evaluatedtoTrue:bool = False 

123 

124 last_sibling = None 

125 

126 # find last none 'nodes.comment' 

127 for last_sibling in reversed(parent): 

128 if not isinstance(last_sibling, nodes.comment): 

129 break 

130 

131 if isinstance(last_sibling, IfNode) or isinstance(last_sibling, ElIfNode): 

132 previously_evaluatedtoTrue = last_sibling.already_evaluatedtoTrue() 

133 else: 

134 logger.warning( 

135 f"{class_name}: without a preceding IfDirective or ElIfDirective. "+ \ 

136 f"Maybe there is something wrong with the intendition.", 

137 type="ifelse", 

138 subtype=class_name, 

139 location=directive2location(self) 

140 ) 

141 previously_evaluatedtoTrue = False 

142 

143 return previously_evaluatedtoTrue 

144 

145 

146class IfDirective(AbstractIfElseDirective): 

147 """Directive to switch between alternative content in the documentation. 

148 """ 

149 

150 required_arguments = 1 

151 optional_arguments = 0 

152 final_argument_whitespace = True 

153 has_content = True 

154 

155 def run(self): 

156 condition = self.arguments[0] 

157 condition_evaluated_to = self.evaluate_condition(condition=condition) 

158 

159 selfnode = IfNode( 

160 condition=condition, 

161 evaluatedto=condition_evaluated_to, 

162 location=directive2location(self) 

163 ) 

164 

165 if condition_evaluated_to: 

166 parsed = self.parse_content_to_nodes(allow_section_headings=True) 

167 parsed.append(selfnode) 

168 return parsed 

169 else: 

170 return [selfnode] 

171 

172 

173class ElIfDirective(AbstractIfElseDirective): 

174 """Directive to switch between alternative content in the documentation. 

175 """ 

176 

177 required_arguments = 1 

178 optional_arguments = 0 

179 final_argument_whitespace = True 

180 has_content = True 

181 

182 def run(self): 

183 parent = self.state.parent 

184 env = self.state.document.settings.env 

185 app = env.app 

186 

187 condition = self.arguments[0] 

188 condition_evaluated_to = self.evaluate_condition(condition=condition) 

189 

190 previously_evaluatedtoTrue = self.fetch_already_evaluatedtoTrue() 

191 

192 selfnode = ElIfNode( 

193 condition=condition, 

194 evaluatedto=condition_evaluated_to, 

195 previously_evaluatedtoTrue = previously_evaluatedtoTrue, 

196 location=directive2location(self) 

197 ) 

198 

199 process_content = condition_evaluated_to and not previously_evaluatedtoTrue 

200 

201 if process_content: 

202 parsed = self.parse_content_to_nodes(allow_section_headings=True) 

203 parsed.append(selfnode) 

204 return parsed 

205 else: 

206 return [selfnode] 

207 

208 

209 

210class ElseDirective(AbstractIfElseDirective): 

211 """Directive to switch between alternative content in the documentation. 

212 """ 

213 

214 required_arguments = 0 

215 optional_arguments = 0 

216 final_argument_whitespace = False 

217 has_content = True 

218 

219 def run(self): 

220 previously_evaluatedtoTrue = self.fetch_already_evaluatedtoTrue() 

221 

222 selfnode = ElseNode( 

223 previously_evaluatedtoTrue = previously_evaluatedtoTrue, 

224 location=directive2location(self) 

225 ) 

226 

227 if not previously_evaluatedtoTrue: 

228 parsed = self.parse_content_to_nodes(allow_section_headings=True) 

229 parsed.append(selfnode) 

230 return parsed 

231 else: 

232 return [selfnode]