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
« 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
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
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]
35 if changes:
36 return parser_doc.replace(**changes)
37 return parser_doc
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
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 )
78 for attr_doc in assume_not_none(provided_attribute_docs):
79 attr_description = attr_doc.description
80 rendered_doc = []
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}`)"
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__}>"
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}")
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
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 ]
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
161 if isinstance(
162 unwrapped_parser, (DeclarativeMappingInputParser, DispatchingObjectParser)
163 ):
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:")
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 = " "
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:")
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 )
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("")
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 )
273 return "\n".join(r)