Coverage for src/debputy/manifest_parser/parser_doc.py: 79%

132 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-07 12:14 +0200

1import itertools 

2from typing import Optional, Iterable, Any, Tuple, Mapping, Sequence, FrozenSet 

3 

4from debputy import DEBPUTY_DOC_ROOT_DIR 

5from debputy.manifest_parser.declarative_parser import ( 

6 DeclarativeMappingInputParser, 

7 DeclarativeNonMappingInputParser, 

8 AttributeDescription, 

9) 

10from debputy.plugin.api.impl_types import ( 

11 DebputyPluginMetadata, 

12 DeclarativeInputParser, 

13 DispatchingObjectParser, 

14 ListWrappedDeclarativeInputParser, 

15 InPackageContextParser, 

16) 

17from debputy.plugin.api.spec import ( 

18 ParserDocumentation, 

19 reference_documentation, 

20 undocumented_attr, 

21) 

22from debputy.util import assume_not_none 

23 

24 

25def _provide_placeholder_parser_doc( 

26 parser_doc: Optional[ParserDocumentation], 

27 attributes: Iterable[str], 

28) -> ParserDocumentation: 

29 if parser_doc is None: 29 ↛ 30line 29 didn't jump to line 30, because the condition on line 29 was never true

30 parser_doc = reference_documentation() 

31 changes = {} 

32 if parser_doc.attribute_doc is None: 

33 changes["attribute_doc"] = [undocumented_attr(attr) for attr in attributes] 

34 

35 if changes: 

36 return parser_doc.replace(**changes) 

37 return parser_doc 

38 

39 

40def doc_args_for_parser_doc( 

41 rule_name: str, 

42 declarative_parser: DeclarativeInputParser[Any], 

43 plugin_metadata: DebputyPluginMetadata, 

44) -> Tuple[Mapping[str, str], ParserDocumentation]: 

45 attributes: Iterable[str] 

46 if isinstance(declarative_parser, DeclarativeMappingInputParser): 

47 attributes = declarative_parser.source_attributes.keys() 

48 else: 

49 attributes = [] 

50 doc_args = { 

51 "RULE_NAME": rule_name, 

52 "MANIFEST_FORMAT_DOC": f"{DEBPUTY_DOC_ROOT_DIR}/MANIFEST-FORMAT.md", 

53 "PLUGIN_NAME": plugin_metadata.plugin_name, 

54 } 

55 parser_doc = _provide_placeholder_parser_doc( 

56 declarative_parser.inline_reference_documentation, 

57 attributes, 

58 ) 

59 return doc_args, parser_doc 

60 

61 

62def render_attribute_doc( 

63 parser: Any, 

64 attributes: Mapping[str, "AttributeDescription"], 

65 required_attributes: FrozenSet[str], 

66 conditionally_required_attributes: FrozenSet[FrozenSet[str]], 

67 parser_doc: ParserDocumentation, 

68 doc_args: Mapping[str, str], 

69 *, 

70 rule_name: str = "<unset>", 

71 is_root_rule: bool = False, 

72 is_interactive: bool = False, 

73) -> Iterable[Tuple[FrozenSet[str], Sequence[str]]]: 

74 provided_attribute_docs = ( 

75 parser_doc.attribute_doc if parser_doc.attribute_doc is not None else [] 

76 ) 

77 

78 for attr_doc in assume_not_none(provided_attribute_docs): 

79 attr_description = attr_doc.description 

80 rendered_doc = [] 

81 

82 for parameter in sorted(attr_doc.attributes): 

83 parameter_details = attributes.get(parameter) 

84 if parameter_details is not None: 84 ↛ 88line 84 didn't jump to line 88, because the condition on line 84 was never false

85 source_name = parameter_details.source_attribute_name 

86 describe_type = parameter_details.type_validator.describe_type() 

87 else: 

88 assert isinstance(parser, DispatchingObjectParser) 

89 source_name = parameter 

90 subparser = parser.parser_for(source_name).parser 

91 if isinstance(subparser, InPackageContextParser): 

92 if is_interactive: 

93 describe_type = "PackageContext" 

94 else: 

95 rule_prefix = rule_name if not is_root_rule else "" 

96 describe_type = f"PackageContext (chains to `{rule_prefix}::{subparser.manifest_attribute_path_template}`)" 

97 

98 elif isinstance(subparser, DispatchingObjectParser): 

99 if is_interactive: 

100 describe_type = "Object" 

101 else: 

102 rule_prefix = rule_name if not is_root_rule else "" 

103 describe_type = f"Object (see `{rule_prefix}::{subparser.manifest_attribute_path_template}`)" 

104 elif isinstance(subparser, DeclarativeMappingInputParser): 

105 describe_type = "<Type definition not implemented yet>" # TODO: Derive from subparser 

106 elif isinstance(subparser, DeclarativeNonMappingInputParser): 

107 describe_type = ( 

108 subparser.alt_form_parser.type_validator.describe_type() 

109 ) 

110 else: 

111 describe_type = f"<Unknown: Non-introspectable subparser - {subparser.__class__.__name__}>" 

112 

113 if source_name in required_attributes: 

114 req_str = "required" 

115 elif any(source_name in s for s in conditionally_required_attributes): 

116 req_str = "conditional" 

117 else: 

118 req_str = "optional" 

119 rendered_doc.append(f"`{source_name}` ({req_str}): {describe_type}") 

120 

121 if attr_description: 121 ↛ 130line 121 didn't jump to line 130, because the condition on line 121 was never false

122 rendered_doc.append("") 

123 rendered_doc.extend( 

124 line 

125 for line in attr_description.format(**doc_args).splitlines( 

126 keepends=False 

127 ) 

128 ) 

129 rendered_doc.append("") 

130 yield attr_doc.attributes, rendered_doc 

131 

132 

133def render_rule( 

134 rule_name: str, 

135 declarative_parser: DeclarativeInputParser[Any], 

136 plugin_metadata: DebputyPluginMetadata, 

137 *, 

138 is_root_rule: bool = False, 

139) -> str: 

140 doc_args, parser_doc = doc_args_for_parser_doc( 

141 "the manifest root" if is_root_rule else rule_name, 

142 declarative_parser, 

143 plugin_metadata, 

144 ) 

145 t = assume_not_none(parser_doc.title).format(**doc_args) 

146 r = [ 

147 t, 

148 "=" * len(t), 

149 "", 

150 assume_not_none(parser_doc.description).format(**doc_args).rstrip(), 

151 "", 

152 ] 

153 

154 alt_form_parser = getattr(declarative_parser, "alt_form_parser", None) 

155 is_list_wrapped = False 

156 unwrapped_parser = declarative_parser 

157 if isinstance(declarative_parser, ListWrappedDeclarativeInputParser): 

158 is_list_wrapped = True 

159 unwrapped_parser = declarative_parser.delegate 

160 

161 if isinstance( 

162 unwrapped_parser, (DeclarativeMappingInputParser, DispatchingObjectParser) 

163 ): 

164 

165 if isinstance(unwrapped_parser, DeclarativeMappingInputParser): 165 ↛ 171line 165 didn't jump to line 171, because the condition on line 165 was never false

166 attributes = unwrapped_parser.source_attributes 

167 required = unwrapped_parser.input_time_required_parameters 

168 conditionally_required = unwrapped_parser.at_least_one_of 

169 mutually_exclusive = unwrapped_parser.mutually_exclusive_attributes 

170 else: 

171 attributes = {} 

172 required = frozenset() 

173 conditionally_required = frozenset() 

174 mutually_exclusive = frozenset() 

175 if is_list_wrapped: 

176 r.append("List where each element has the following attributes:") 

177 else: 

178 r.append("Attributes:") 

179 

180 rendered_attr_doc = render_attribute_doc( 

181 unwrapped_parser, 

182 attributes, 

183 required, 

184 conditionally_required, 

185 parser_doc, 

186 doc_args, 

187 is_root_rule=is_root_rule, 

188 rule_name=rule_name, 

189 is_interactive=False, 

190 ) 

191 for _, rendered_doc in rendered_attr_doc: 

192 prefix = " - " 

193 for line in rendered_doc: 

194 if line: 

195 r.append(f"{prefix}{line}") 

196 else: 

197 r.append("") 

198 prefix = " " 

199 

200 if ( 

201 bool(conditionally_required) 

202 or bool(mutually_exclusive) 

203 or any(pd.conflicting_attributes for pd in attributes.values()) 

204 ): 

205 r.append("") 

206 if is_list_wrapped: 

207 r.append( 

208 "This rule enforces the following restrictions on each element in the list:" 

209 ) 

210 else: 

211 r.append("This rule enforces the following restrictions:") 

212 

213 if conditionally_required or mutually_exclusive: 213 ↛ 231line 213 didn't jump to line 231, because the condition on line 213 was never false

214 all_groups = set( 

215 itertools.chain(conditionally_required, mutually_exclusive) 

216 ) 

217 for g in all_groups: 

218 anames = "`, `".join(g) 

219 is_mx = g in mutually_exclusive 

220 is_cr = g in conditionally_required 

221 if is_mx and is_cr: 

222 r.append(f" - The rule must use exactly one of: `{anames}`") 

223 elif is_cr: 223 ↛ 224line 223 didn't jump to line 224, because the condition on line 223 was never true

224 r.append(f" - The rule must use at least one of: `{anames}`") 

225 else: 

226 assert is_mx 

227 r.append( 

228 f" - The following attributes are mutually exclusive: `{anames}`" 

229 ) 

230 

231 if mutually_exclusive or any( 231 ↛ exit,   231 ↛ 2482 missed branches: 1) line 231 didn't run the generator expression on line 231, 2) line 231 didn't jump to line 248, because the condition on line 231 was never false

232 pd.conflicting_attributes for pd in attributes.values() 

233 ): 

234 for parameter, parameter_details in sorted(attributes.items()): 

235 source_name = parameter_details.source_attribute_name 

236 conflicts = set(parameter_details.conflicting_attributes) 

237 for mx in mutually_exclusive: 

238 if parameter in mx and mx not in conditionally_required: 238 ↛ 239line 238 didn't jump to line 239, because the condition on line 238 was never true

239 conflicts |= mx 

240 if conflicts: 

241 conflicts.discard(parameter) 

242 cnames = "`, `".join( 

243 attributes[a].source_attribute_name for a in conflicts 

244 ) 

245 r.append( 

246 f" - The attribute `{source_name}` cannot be used with any of: `{cnames}`" 

247 ) 

248 r.append("") 

249 if alt_form_parser is not None: 

250 # FIXME: Mapping[str, Any] ends here, which is ironic given the headline. 

251 r.append( 

252 f"Non-mapping format: {alt_form_parser.type_validator.describe_type()}" 

253 ) 

254 alt_parser_desc = parser_doc.alt_parser_description 

255 if alt_parser_desc: 

256 r.extend( 

257 f" {line}" 

258 for line in alt_parser_desc.format(**doc_args).splitlines( 

259 keepends=False 

260 ) 

261 ) 

262 r.append("") 

263 

264 if declarative_parser.reference_documentation_url is not None: 

265 r.append( 

266 f"Reference documentation: {declarative_parser.reference_documentation_url}" 

267 ) 

268 else: 

269 r.append( 

270 "Reference documentation: No reference documentation link provided by the plugin" 

271 ) 

272 

273 return "\n".join(r)