@redpanda-data/docs-extensions-and-macros 4.10.7 → 4.10.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "4.10.7",
3
+ "version": "4.10.8",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -22,7 +22,7 @@ const helpers = require('./helpers');
22
22
  * CLI Usage: node generate-handlebars-docs.js <input-file> <output-dir>
23
23
  */
24
24
 
25
- // Register all helpers
25
+ // Register helpers
26
26
  Object.entries(helpers).forEach(([name, fn]) => {
27
27
  if (typeof fn !== 'function') {
28
28
  console.error(`❌ Helper "${name}" is not a function`);
@@ -62,11 +62,6 @@ function getTemplatePath(defaultPath, envVar) {
62
62
 
63
63
  /**
64
64
  * Register Handlebars partials used to render property documentation.
65
- *
66
- * Registers:
67
- * - "property"
68
- * - "topic-property"
69
- * - "deprecated-property"
70
65
  */
71
66
  function registerPartials() {
72
67
  const templatesDir = path.join(__dirname, 'templates');
@@ -112,7 +107,7 @@ function registerPartials() {
112
107
  /**
113
108
  * Generate consolidated AsciiDoc partials for properties grouped by type.
114
109
  */
115
- function generatePropertyPartials(properties, partialsDir) {
110
+ function generatePropertyPartials(properties, partialsDir, onRender) {
116
111
  console.log(`📝 Generating consolidated property partials in ${partialsDir}…`);
117
112
 
118
113
  const propertyTemplate = handlebars.compile(
@@ -129,11 +124,24 @@ function generatePropertyPartials(properties, partialsDir) {
129
124
 
130
125
  Object.values(properties).forEach(prop => {
131
126
  if (!prop.name || !prop.config_scope) return;
132
- if (prop.config_scope === 'topic') propertyGroups.topic.push(prop);
133
- else if (prop.config_scope === 'broker') propertyGroups.broker.push(prop);
134
- else if (prop.config_scope === 'cluster') {
135
- if (isObjectStorageProperty(prop)) propertyGroups['object-storage'].push(prop);
136
- else propertyGroups.cluster.push(prop);
127
+
128
+ switch (prop.config_scope) {
129
+ case 'topic':
130
+ propertyGroups.topic.push(prop);
131
+ break;
132
+ case 'broker':
133
+ propertyGroups.broker.push(prop);
134
+ break;
135
+ case 'cluster':
136
+ if (isObjectStorageProperty(prop)) propertyGroups['object-storage'].push(prop);
137
+ else propertyGroups.cluster.push(prop);
138
+ break;
139
+ case 'object-storage':
140
+ propertyGroups['object-storage'].push(prop);
141
+ break;
142
+ default:
143
+ console.warn(`⚠️ Unknown config_scope: ${prop.config_scope} for ${prop.name}`);
144
+ break;
137
145
  }
138
146
  });
139
147
 
@@ -143,9 +151,16 @@ function generatePropertyPartials(properties, partialsDir) {
143
151
  if (props.length === 0) return;
144
152
  props.sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')));
145
153
  const selectedTemplate = type === 'topic' ? topicTemplate : propertyTemplate;
146
- const content = props.map(p => selectedTemplate(p)).join('\n');
154
+ const pieces = [];
155
+ props.forEach(p => {
156
+ if (typeof onRender === 'function') {
157
+ try { onRender(p.name); } catch (err) { /* swallow callback errors */ }
158
+ }
159
+ pieces.push(selectedTemplate(p));
160
+ });
161
+ const content = pieces.join('\n');
147
162
  const filename = `${type}-properties.adoc`;
148
- fs.writeFileSync(path.join(propertiesPartialsDir, filename), AUTOGEN_NOTICE + content, 'utf8');
163
+ fs.writeFileSync(path.join(propertiesPartialsDir, filename), AUTOGEN_NOTICE + content, 'utf8');
149
164
  console.log(`✅ Generated ${filename} (${props.length} properties)`);
150
165
  totalCount += props.length;
151
166
  });
@@ -178,19 +193,18 @@ function generateDeprecatedDocs(properties, outputDir) {
178
193
  clusterProperties: clusterProperties.length ? clusterProperties : null
179
194
  };
180
195
 
181
- const output = template(data);
182
196
  const outputPath = process.env.OUTPUT_PARTIALS_DIR
183
197
  ? path.join(process.env.OUTPUT_PARTIALS_DIR, 'deprecated', 'deprecated-properties.adoc')
184
198
  : path.join(outputDir, 'partials', 'deprecated', 'deprecated-properties.adoc');
185
199
 
186
200
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
187
- fs.writeFileSync(outputPath, AUTOGEN_NOTICE + output, 'utf8');
201
+ fs.writeFileSync(outputPath, AUTOGEN_NOTICE + template(data), 'utf8');
188
202
  console.log(`✅ Generated ${outputPath}`);
189
203
  return deprecatedProperties.length;
190
204
  }
191
205
 
192
206
  /**
193
- * Generate topic-property-mappings.adoc using the mappings template and topic properties.
207
+ * Generate topic-property-mappings.adoc
194
208
  */
195
209
  function generateTopicPropertyMappings(properties, partialsDir) {
196
210
  const templatesDir = path.join(__dirname, 'templates');
@@ -218,31 +232,42 @@ function generateTopicPropertyMappings(properties, partialsDir) {
218
232
  }
219
233
 
220
234
  /**
221
- * Generate error reports for missing descriptions and deprecated properties.
235
+ * Generate error reports for missing descriptions, deprecated, and undocumented properties.
222
236
  */
223
- function generateErrorReports(properties) {
237
+ function generateErrorReports(properties, documentedProperties = []) {
224
238
  const emptyDescriptions = [];
225
239
  const deprecatedProperties = [];
240
+ const allKeys = Object.keys(properties);
226
241
 
227
- Object.values(properties).forEach(p => {
228
- if (!p.description || !p.description.trim()) emptyDescriptions.push(p.name);
229
- if (p.is_deprecated) deprecatedProperties.push(p.name);
242
+ // Use documentedProperties array (property names that were rendered into partials)
243
+ const documentedSet = new Set(documentedProperties);
244
+ const undocumented = [];
245
+
246
+ Object.entries(properties).forEach(([key, p]) => {
247
+ const name = p.name || key;
248
+ if (!p.description || !p.description.trim()) emptyDescriptions.push(name);
249
+ if (p.is_deprecated) deprecatedProperties.push(name);
250
+ if (!documentedSet.has(name)) undocumented.push(name);
230
251
  });
231
252
 
232
- const total = Object.keys(properties).length;
253
+ const total = allKeys.length;
233
254
  const pctEmpty = total ? ((emptyDescriptions.length / total) * 100).toFixed(2) : '0.00';
234
255
  const pctDeprecated = total ? ((deprecatedProperties.length / total) * 100).toFixed(2) : '0.00';
235
- console.log(`Empty descriptions: ${emptyDescriptions.length} (${pctEmpty}%)`);
236
- console.log(`Deprecated: ${deprecatedProperties.length} (${pctDeprecated}%)`);
256
+ const pctUndocumented = total ? ((undocumented.length / total) * 100).toFixed(2) : '0.00';
257
+
258
+ console.log(`📉 Empty descriptions: ${emptyDescriptions.length} (${pctEmpty}%)`);
259
+ console.log(`🕸️ Deprecated: ${deprecatedProperties.length} (${pctDeprecated}%)`);
260
+ console.log(`🚫 Not documented: ${undocumented.length} (${pctUndocumented}%)`);
237
261
 
238
262
  return {
239
263
  empty_descriptions: emptyDescriptions.sort(),
240
- deprecated_properties: deprecatedProperties.sort()
264
+ deprecated_properties: deprecatedProperties.sort(),
265
+ undocumented_properties: undocumented.sort(),
241
266
  };
242
267
  }
243
268
 
244
269
  /**
245
- * Main generator — only supports partials and deprecated docs.
270
+ * Main generator
246
271
  */
247
272
  function generateAllDocs(inputFile, outputDir) {
248
273
  const data = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
@@ -252,13 +277,14 @@ function generateAllDocs(inputFile, outputDir) {
252
277
 
253
278
  let partialsCount = 0;
254
279
  let deprecatedCount = 0;
280
+ const documentedProps = []; // Track which property names were rendered
255
281
 
256
282
  if (process.env.GENERATE_PARTIALS === '1' && process.env.OUTPUT_PARTIALS_DIR) {
257
283
  console.log('📄 Generating property partials and deprecated docs...');
258
284
  deprecatedCount = generateDeprecatedDocs(properties, outputDir);
259
- partialsCount = generatePropertyPartials(properties, process.env.OUTPUT_PARTIALS_DIR);
260
285
 
261
- // Generate topic-property-mappings.adoc
286
+ // Generate property partials using the shared helper and collect names via callback
287
+ partialsCount = generatePropertyPartials(properties, process.env.OUTPUT_PARTIALS_DIR, name => documentedProps.push(name));
262
288
  try {
263
289
  generateTopicPropertyMappings(properties, process.env.OUTPUT_PARTIALS_DIR);
264
290
  } catch (err) {
@@ -268,24 +294,34 @@ function generateAllDocs(inputFile, outputDir) {
268
294
  console.log('📄 Skipping partial generation (set GENERATE_PARTIALS=1 and OUTPUT_PARTIALS_DIR to enable)');
269
295
  }
270
296
 
271
- const errors = generateErrorReports(properties);
272
- const inputData = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
273
- inputData.empty_descriptions = errors.empty_descriptions;
274
- inputData.deprecated_properties = errors.deprecated_properties;
275
- fs.writeFileSync(inputFile, JSON.stringify(inputData, null, 2), 'utf8');
297
+ const errors = generateErrorReports(properties, documentedProps);
276
298
 
277
- console.log('📊 Summary:');
278
- console.log(` Total properties: ${Object.keys(properties).length}`);
279
- console.log(` Total partials generated: ${partialsCount}`);
280
- console.log(` Deprecated properties: ${deprecatedCount}`);
299
+ const totalProperties = Object.keys(properties).length;
300
+ const notRendered = errors.undocumented_properties.length;
301
+ const pctRendered = totalProperties
302
+ ? ((partialsCount / totalProperties) * 100).toFixed(2)
303
+ : '0.00';
304
+
305
+ console.log('\n📊 Summary:');
306
+ console.log(` Total properties found: ${totalProperties}`);
307
+ console.log(` Property partials generated: ${partialsCount} (${pctRendered}% of total)`);
308
+ console.log(` Not documented: ${notRendered}`);
309
+ console.log(` Deprecated properties: ${deprecatedCount}`);
310
+
311
+ if (notRendered > 0) {
312
+ console.log('⚠️ Undocumented properties:\n ' + errors.undocumented_properties.join('\n '));
313
+ }
281
314
 
282
315
  return {
283
- totalProperties: Object.keys(properties).length,
284
- propertyPartials: partialsCount,
285
- deprecatedProperties: deprecatedCount
316
+ totalProperties,
317
+ generatedPartials: partialsCount,
318
+ undocumentedProperties: errors.undocumented_properties,
319
+ deprecatedProperties: deprecatedCount,
320
+ percentageRendered: pctRendered
286
321
  };
287
322
  }
288
323
 
324
+
289
325
  module.exports = {
290
326
  generateAllDocs,
291
327
  generateDeprecatedDocs,
@@ -0,0 +1,19 @@
1
+ // Returns 'cloud' if all topics are cloud-only, 'self-managed' if all are self-managed-only, else 'normal'
2
+ module.exports = function allTopicsConditional(related_topics) {
3
+ if (!Array.isArray(related_topics) || related_topics.length === 0) return null;
4
+ let allCloud = true;
5
+ let allSelfManaged = true;
6
+ for (const t of related_topics) {
7
+ if (typeof t !== 'string') {
8
+ allCloud = false;
9
+ allSelfManaged = false;
10
+ break;
11
+ }
12
+ const trimmed = t.trim();
13
+ if (!trimmed.startsWith('cloud-only:')) allCloud = false;
14
+ if (!trimmed.startsWith('self-managed-only:')) allSelfManaged = false;
15
+ }
16
+ if (allCloud) return 'cloud';
17
+ if (allSelfManaged) return 'self-managed';
18
+ return 'normal';
19
+ };
@@ -28,6 +28,10 @@ function processDefaults(inputString, suffix) {
28
28
  return inputString;
29
29
  }
30
30
 
31
+ // Remove C++ digit separators (apostrophes) from numbers/durations
32
+ // e.g. 30'000ms -> 30000ms, 1'500 -> 1500
33
+ inputString = inputString.replace(/(?<=\d)'(?=\d)/g, '');
34
+
31
35
  // Test for ip:port in vector: std::vector<net::unresolved_address>({{...}})
32
36
  const vectorMatch = inputString.match(/std::vector<net::unresolved_address>\(\{\{("([\d.]+)",\s*(\d+))\}\}\)/);
33
37
  if (vectorMatch) {
@@ -11,4 +11,6 @@ module.exports = {
11
11
  renderPropertyExample: require('./renderPropertyExample.js'),
12
12
  formatUnits: require('./formatUnits.js'),
13
13
  anchorName: require('./anchorName.js'),
14
+ parseRelatedTopic: require('./parseRelatedTopic.js'),
15
+ allTopicsConditional: require('./allTopicsConditional.js'),
14
16
  };
@@ -0,0 +1,13 @@
1
+ // Returns an object with type and value for a related topic
2
+ // type: 'cloud', 'self-managed', or 'normal'
3
+ module.exports = function parseRelatedTopic(topic) {
4
+ if (typeof topic !== 'string') return { type: 'normal', value: topic };
5
+ const trimmed = topic.trim();
6
+ if (trimmed.startsWith('cloud-only:')) {
7
+ return { type: 'cloud', value: trimmed.replace(/^cloud-only:/, '').trim() };
8
+ }
9
+ if (trimmed.startsWith('self-managed-only:')) {
10
+ return { type: 'self-managed', value: trimmed.replace(/^self-managed-only:/, '').trim() };
11
+ }
12
+ return { type: 'normal', value: trimmed };
13
+ };
@@ -1010,6 +1010,9 @@ def resolve_type_and_default(properties, definitions):
1010
1010
  processed (str): A string representing the JSON-ready value (for example: '"value"', 'null', '0', or the original input when no mapping applied).
1011
1011
  """
1012
1012
  arg_str = arg_str.strip()
1013
+ # Remove C++ digit separators (apostrophes) that may appear in numeric literals
1014
+ # Example: "30'000ms" -> "30000ms". Use conservative replace only between digits.
1015
+ arg_str = re.sub(r"(?<=\d)'(?=\d)", '', arg_str)
1013
1016
 
1014
1017
  # Handle std::nullopt -> null
1015
1018
  if arg_str == "std::nullopt":
@@ -1023,9 +1026,27 @@ def resolve_type_and_default(properties, definitions):
1023
1026
  resolved_value = resolve_cpp_function_call(function_name)
1024
1027
  if resolved_value is not None:
1025
1028
  return f'"{resolved_value}"'
1029
+
1030
+ # Handle std::chrono literals like std::chrono::minutes{5} -> "5min"
1031
+ chrono_match = re.match(r'std::chrono::([a-zA-Z]+)\s*\{\s*(\d+)\s*\}', arg_str)
1032
+ if chrono_match:
1033
+ unit = chrono_match.group(1)
1034
+ value = chrono_match.group(2)
1035
+ unit_map = {
1036
+ 'hours': 'h',
1037
+ 'minutes': 'min',
1038
+ 'seconds': 's',
1039
+ 'milliseconds': 'ms',
1040
+ 'microseconds': 'us',
1041
+ 'nanoseconds': 'ns'
1042
+ }
1043
+ short = unit_map.get(unit.lower(), unit)
1044
+ return f'"{value} {short}"'
1026
1045
 
1027
- # Handle enum-like patterns (such as fips_mode_flag::disabled -> "disabled")
1028
- enum_match = re.match(r'[a-zA-Z0-9_:]+::([a-zA-Z0-9_]+)', arg_str)
1046
+ # Handle enum-like patterns (such as fips_mode_flag::disabled -> "disabled").
1047
+ # Only treat bare 'X::Y' tokens as enums — do not match when the token
1048
+ # is followed by constructor braces/parentheses (e.g. std::chrono::minutes{5}).
1049
+ enum_match = re.match(r'[a-zA-Z0-9_:]+::([a-zA-Z0-9_]+)\s*$', arg_str)
1029
1050
  if enum_match:
1030
1051
  enum_value = enum_match.group(1)
1031
1052
  return f'"{enum_value}"'
@@ -23,6 +23,7 @@ endif::[]
23
23
  {{else}}
24
24
 
25
25
  No description available.
26
+
26
27
  {{/if}}
27
28
  {{#if is_enterprise}}
28
29
 
@@ -31,9 +32,11 @@ ifndef::env-cloud[]
31
32
  endif::[]
32
33
  {{/if}}
33
34
  {{#if cloud_byoc_only}}
35
+
34
36
  ifdef::env-cloud[]
35
37
  NOTE: This property is available only in Redpanda Cloud BYOC deployments.
36
38
  endif::[]
39
+
37
40
  {{/if}}
38
41
  {{#if units}}
39
42
 
@@ -50,6 +53,10 @@ endif::[]
50
53
  *Requires restart:* {{#if needs_restart}}Yes{{else}}No{{/if}}
51
54
  {{/if}}
52
55
  {{/if}}
56
+
57
+ ifndef::env-cloud[]
58
+ *Restored during xref:manage:whole-cluster-restore.adoc[Whole Cluster Restore]:* {{#if (ne gets_restored false)}}Yes{{else}}No{{/if}}
59
+ endif::[]
53
60
  {{#if visibility}}
54
61
 
55
62
  // tag::self-managed-only[]
@@ -89,13 +96,49 @@ endif::[]
89
96
  {{{renderPropertyExample this}}}
90
97
  {{/if}}
91
98
  {{#if related_topics}}
99
+ {{#with (allTopicsConditional related_topics) as |sectionType|}}
100
+
101
+ {{#if (eq sectionType "cloud")}}
102
+ ifdef::env-cloud[]
103
+ *Related topics:*
104
+
105
+ {{#each ../related_topics}}
106
+ {{#with (parseRelatedTopic this)}}
107
+ * {{{value}}}
108
+ {{/with}}
109
+ {{/each}}
110
+ endif::[]
111
+ {{else if (eq sectionType "self-managed")}}
112
+ ifndef::env-cloud[]
113
+ *Related topics:*
92
114
 
115
+ {{#each ../related_topics}}
116
+ {{#with (parseRelatedTopic this)}}
117
+ * {{{value}}}
118
+ {{/with}}
119
+ {{/each}}
120
+ endif::[]
121
+ {{else}}
93
122
  *Related topics:*
94
123
 
95
- {{#each related_topics}}
96
- * {{{this}}}
124
+ {{#each ../related_topics}}
125
+ {{#with (parseRelatedTopic this)}}
126
+ {{#if (eq type "cloud")}}
127
+ ifdef::env-cloud[]
128
+ * {{{value}}}
129
+ endif::[]
130
+ {{else if (eq type "self-managed")}}
131
+ ifndef::env-cloud[]
132
+ * {{{value}}}
133
+ endif::[]
134
+ {{else}}
135
+ * {{{value}}}
136
+ {{/if}}
137
+ {{/with}}
97
138
  {{/each}}
98
139
  {{/if}}
140
+ {{/with}}
141
+ {{/if}}
99
142
  {{#if aliases}}
100
143
 
101
144
  // tag::self-managed-only[]
@@ -74,17 +74,57 @@ endif::[]
74
74
  {{/if}}
75
75
 
76
76
  *Nullable:* {{#if nullable}}Yes{{else}}No{{/if}}
77
+
78
+ ifndef::env-cloud[]
79
+ *Restored during xref:manage:whole-cluster-restore.adoc[Whole Cluster Restore]:* {{#if (ne gets_restored false)}}Yes{{else}}No{{/if}}
80
+ endif::[]
77
81
  {{#if example}}
78
82
 
79
83
  {{{renderPropertyExample this}}}
80
84
  {{/if}}
81
85
  {{#if related_topics}}
86
+ {{#with (allTopicsConditional related_topics) as |sectionType|}}
82
87
 
88
+ {{#if (eq sectionType "cloud")}}
89
+ ifdef::env-cloud[]
83
90
  *Related topics:*
84
91
 
85
- {{#each related_topics}}
86
- * {{{this}}}
92
+ {{#each ../related_topics}}
93
+ {{#with (parseRelatedTopic this)}}
94
+ * {{{value}}}
95
+ {{/with}}
87
96
  {{/each}}
97
+ endif::[]
98
+ {{else if (eq sectionType "self-managed")}}
99
+ ifndef::env-cloud[]
100
+ *Related topics:*
101
+
102
+ {{#each ../related_topics}}
103
+ {{#with (parseRelatedTopic this)}}
104
+ * {{{value}}}
105
+ {{/with}}
106
+ {{/each}}
107
+ endif::[]
108
+ {{else}}
109
+ *Related topics:*
110
+
111
+ {{#each ../related_topics}}
112
+ {{#with (parseRelatedTopic this)}}
113
+ {{#if (eq type "cloud")}}
114
+ ifdef::env-cloud[]
115
+ * {{{value}}}
116
+ endif::[]
117
+ {{else if (eq type "self-managed")}}
118
+ ifndef::env-cloud[]
119
+ * {{{value}}}
120
+ endif::[]
121
+ {{else}}
122
+ * {{{value}}}
123
+ {{/if}}
124
+ {{/with}}
125
+ {{/each}}
126
+ {{/if}}
127
+ {{/with}}
88
128
  {{/if}}
89
129
  {{#if aliases}}
90
130
 
@@ -304,11 +304,29 @@ class TopicPropertyExtractor:
304
304
 
305
305
  def _determine_property_type(self, property_name: str) -> str:
306
306
  """Determine the type of a property based on its name and usage patterns"""
307
-
308
- # Type mapping based on property name patterns
307
+ # Explicit exceptions / overrides for properties whose name contains
308
+ # keywords that would otherwise map to boolean but which are actually
309
+ # string-valued (for example, a bucket name).
310
+ if property_name == "redpanda.remote.readreplica":
311
+ # This topic property contains the read-replica bucket identifier
312
+ # and should be treated as a string (not a boolean).
313
+ return "string"
314
+ # Explicit override: iceberg.delete is a boolean (whether to delete
315
+ # the corresponding Iceberg table when the topic is deleted).
316
+ if property_name == "redpanda.iceberg.delete":
317
+ return "boolean"
318
+
319
+ # Type mapping based on property name patterns (heuristic)
309
320
  if any(keyword in property_name for keyword in ["caching", "recovery", "read", "write", "delete"]):
310
- if property_name in ["write.caching", "redpanda.remote.recovery", "redpanda.remote.write",
311
- "redpanda.remote.read", "redpanda.remote.delete", "redpanda.remote.readreplica"]:
321
+ # Known boolean topic properties (keep list conservative)
322
+ boolean_props = [
323
+ "write.caching",
324
+ "redpanda.remote.recovery",
325
+ "redpanda.remote.write",
326
+ "redpanda.remote.read",
327
+ "redpanda.remote.delete",
328
+ ]
329
+ if property_name in boolean_props:
312
330
  return "boolean"
313
331
 
314
332
  elif any(suffix in property_name for suffix in [".bytes", ".ms", ".factor", ".lag.ms"]):
@@ -583,7 +601,8 @@ NOTE: All topic properties take effect immediately after being set.
583
601
  *Type:* {prop_type}
584
602
 
585
603
  """
586
- if acceptable_values:
604
+ # If the property type is boolean, never include an Accepted values section
605
+ if acceptable_values and str(prop_type).lower() not in ("boolean", "bool"):
587
606
  adoc_content += f"*Accepted values:* {acceptable_values}\n\n"
588
607
 
589
608
  adoc_content += "*Default:* null\n\n"
@@ -145,15 +145,19 @@ class IsArrayTransformer:
145
145
 
146
146
  class NeedsRestartTransformer:
147
147
  def accepts(self, info, file_pair):
148
- return True
149
-
148
+ # Only accept when the params blob exists and contains a needs_restart entry
149
+ return (
150
+ len(info.get("params", [])) > 2
151
+ and isinstance(info["params"][2].get("value"), dict)
152
+ and "needs_restart" in info["params"][2]["value"]
153
+ )
154
+
150
155
  def parse(self, property, info, file_pair):
151
- needs_restart = "yes"
152
- if len(info["params"]) > 2 and "needs_restart" in info["params"][2]["value"]:
153
- needs_restart = re.sub(
154
- r"^.*::", "", info["params"][2]["value"]["needs_restart"]
155
- )
156
- property["needs_restart"] = needs_restart != "no" # True by default, unless we find "no"
156
+ # We only get here if accepts(...) returned True, so the metadata blob is present
157
+ raw = info["params"][2]["value"]["needs_restart"]
158
+ flag = re.sub(r"^.*::", "", raw)
159
+ # Store as boolean; do not set any default when metadata is absent
160
+ property["needs_restart"] = (flag != "no")
157
161
 
158
162
  class GetsRestoredTransformer:
159
163
  def accepts(self, info, file_pair):