3from enum
import Enum, IntEnum
7from AthenaCommon.Logging
import logging
9genSettingsLog = logging.getLogger(
"GeneratorSettingsSemantics")
14 Which duplicate setting survives after layers are sorted by precedence.
16 Layers are resolved in increasing precedence order: BASE, then TUNE, then
17 USER. A duplicate is either a parsed command with the same normalized key,
18 or an unparsed command with the same normalized full text.
20 FIRST keeps the first duplicate encountered. This preserves lower-precedence
21 defaults when later layers repeat the same setting.
23 LAST keeps the last duplicate encountered. This is the normal generator
24 behavior: tune settings override base settings, and user settings override
33 How one command string is understood during merging.
35 Each command is parsed with the separators from GeneratorSettingsLayer.
36 Commands that contain one of those separators are treated as parsed
37 key/value commands, e.g. "Main:timesAllowErrors = 500". These can override
40 Commands without any configured separator are unparsed commands. They have
41 no key, so they can only be deduplicated when their normalized full text is
44 PARSED_COMMAND =
"parsed_command"
45 UNPARSED_COMMAND =
"unparsed_command"
50 Precedence levels for generator settings layers.
51 Lower precedence layers are overridden by higher ones during merging.
60 A layer contains all generator settings from a single source,
61 e.g. a user fragment, tune, etc.
63 Normal assignments still use a plain sequence of strings. Layers are only
64 needed by configuration fragments that want to describe where commands came
65 from and how they should be ordered during CA merging.
73 keep=GeneratorSettingsKeep.LAST,
75 if isinstance(values, str):
76 raise TypeError(
"GeneratorSettingsLayer values must be a sequence")
89 if isinstance(separators, str):
90 separators = [separators]
103 Stored property value used by GeneratorSettingsSemantics.
105 It keeps the unresolved layers rather than a pre-merged list so that
106 CA merges remain associative: base.merge(user) and user.merge(base)
107 yield the same result.
114 for layer
in layers
or ():
115 if not isinstance(layer, GeneratorSettingsLayer):
116 layer_type =
type(layer).__name__
118 f
"GeneratorSettingsValue layers must be "
119 f
"GeneratorSettingsLayer instances, got {layer_type}"
131 layer.report_context,
141 def resolve(self, report_context=None, emit_report=False):
143 Resolve all layers to the final settings list.
145 If emit_report is True, print duplicate/conflict warnings once using
146 report_context (or a layer-provided report_context if available).
152 if settings_layer.report_context:
153 report_context = settings_layer.report_context
164 Return the resolved list as a string.
165 ComponentAccumulator.gatherProps() converts stored property values
166 to strings before handing them to Gaudi.
168 return str(self.
resolve(emit_report=
True))
171 """Return the final command list plus the duplicate/conflict report."""
173 separators = layers[0].separators
if layers
else (
"=",)
174 keep = layers[0].keep
if layers
else GeneratorSettingsKeep.LAST
175 precedences_seen = {}
178 layer.separators == separators
179 and layer.keep == keep
181 if layer.precedence
in precedences_seen:
182 previous_layer = precedences_seen[layer.precedence]
184 f
"cannot merge generator settings with duplicate "
185 f
"precedence {layer.precedence.name}: "
186 f
"{previous_layer.source} and {layer.source}"
188 precedences_seen[layer.precedence] = layer
192 f
"{entry.source}: separators={entry.separators}, "
193 f
"keep={entry.keep.value}"
197 f
"cannot merge generator settings with different parsing "
205 [record[
"original_setting"]
for record
in kept_records],
212 Semantics for generator settings properties.
214 Gaudi sees the property as a sequence of strings, but CA merging receives
215 GeneratorSettingsLayer objects so fragments can be combined by precedence
216 before the final list is handed to the C++ component.
218 __handled_types__ = [re.compile(
r"^GeneratorSettings<.*>$")]
223 super().
__init__(cpp_type, valueSem=getSemanticsFor(
"std::string"))
226 """Convert user input into the internal unresolved layer container."""
227 if isinstance(assigned_value, GeneratorSettingsValue):
228 return assigned_value
229 if isinstance(assigned_value, GeneratorSettingsLayer):
231 if isinstance(assigned_value, str):
232 raise TypeError(
"generator settings must be assigned from a sequence")
235 source=self.name
or "<unknown>",
236 values=tuple(assigned_value
or ()),
237 precedence=GeneratorSettingsPrecedence.USER,
238 report_context=self.name,
243 """Store C++ defaults as the lowest-precedence command layer."""
244 if not default_commands:
246 if isinstance(default_commands, str):
247 raise TypeError(
"generator settings must be assigned from a sequence")
250 source=self.name
or "<default>",
251 values=tuple(default_commands
or ()),
252 precedence=GeneratorSettingsPrecedence.BASE,
253 report_context=self.name,
257 def merge(self, current_value, incoming_value):
258 """Merge two unresolved layer sets during CA merging."""
259 current_settings = self.
store(current_value)
260 incoming_settings = self.
store(incoming_value)
262 current_settings.layers + incoming_settings.layers
267 Return the final plain command list.
268 This is the point where all layers are resolved.
269 Print duplicate and conflict warnings here.
271 settings_value = self.
store(stored_value)
272 return settings_value.resolve(report_context=self.name, emit_report=
True)
276 """Apply precedence before deduplication."""
280 int(layer.precedence),
289 Convert raw command strings into normalized records.
290 Parsed commands are deduplicated by key. Commands that cannot be parsed as
291 key/value records are deduplicated by their normalized full text.
295 for raw_setting
in layer.values:
299 "source_name": layer.source,
300 "record_kind": GeneratorSettingsRecord.UNPARSED_COMMAND,
301 "normalized_key":
None,
302 "normalized_value":
None,
303 "original_setting": raw_setting,
305 GeneratorSettingsRecord.UNPARSED_COMMAND,
313 "source_name": layer.source,
314 "record_kind": GeneratorSettingsRecord.PARSED_COMMAND,
315 "normalized_key": normalized_key,
317 "original_setting": raw_setting,
319 GeneratorSettingsRecord.PARSED_COMMAND,
327 text = str(setting_text).
strip()
328 for separator
in separators:
329 if separator
in text:
330 key_text, value_text = text.split(separator, 1)
331 return key_text.strip(), value_text.strip()
336 text = str(value).
strip()
337 return " ".join(text.split())
341 """Keep the first or last record for each normalized setting key."""
342 keep_last = keep == GeneratorSettingsKeep.LAST
343 records_to_scan = reversed(records)
if keep_last
else records
347 first_seen_by_signature = {}
348 for record
in records_to_scan:
349 signature = record[
"dedup_signature"]
350 if signature
in first_seen_by_signature:
351 kept_record = first_seen_by_signature[signature]
352 removed_record = dict(record)
353 removed_record[
"duplicate_of_source_name"] = kept_record[
"source_name"]
354 removed_record[
"kept_normalized_value"] = kept_record[
"normalized_value"]
355 removed_record[
"kept_original_setting"] = kept_record[
"original_setting"]
356 removed_records.append(removed_record)
359 first_seen_by_signature[signature] = record
360 kept_records.append(record)
363 kept_records.reverse()
364 removed_records.reverse()
365 return kept_records, removed_records
369 """Collect duplicate and conflict information for logging/tests."""
370 removed_duplicates = [
372 "source": record[
"source_name"],
373 "duplicate_of_source": record.get(
"duplicate_of_source_name"),
374 "setting": record[
"original_setting"],
375 "kept_setting": record.get(
"kept_original_setting"),
376 "normalized_value": record[
"normalized_value"],
377 "kept_normalized_value": record.get(
"kept_normalized_value"),
379 for record
in removed_records
382 removed_identical = [
384 "source": record[
"source"],
385 "duplicate_of_source": record.get(
"duplicate_of_source"),
386 "setting": record[
"setting"],
387 "kept_setting": record.get(
"kept_setting"),
389 for record
in removed_duplicates
390 if record[
"normalized_value"] == record.get(
"kept_normalized_value")
392 duplicates_in_source = {}
393 duplicates_across_sources = {}
395 for record
in removed_identical:
396 source_name = record.get(
"source",
"<unknown>")
397 duplicate_of_source = record.get(
"duplicate_of_source",
"<unknown>")
398 if duplicate_of_source == source_name:
399 duplicates_in_source[source_name] = (
400 duplicates_in_source.get(source_name, 0) + 1
404 source_pair = (source_name, duplicate_of_source)
405 duplicates_across_sources[source_pair] = (
406 duplicates_across_sources.get(source_pair, 0) + 1
412 "removed_duplicates": removed_duplicates,
413 "removed_identical": removed_identical,
414 "conflicting_reassignments": [
416 for record
in removed_duplicates
417 if record[
"normalized_value"] != record.get(
"kept_normalized_value")
419 "removed_overridden": [
421 for record
in removed_duplicates
422 if record[
"normalized_value"] != record.get(
"kept_normalized_value")
424 "conflict_details": conflict_details,
426 "duplicates_in_source": duplicates_in_source,
427 "duplicates_across_sources": [
429 "source": source_name,
430 "duplicate_of_source": duplicate_of_source,
431 "count": duplicate_count,
433 for (source_name, duplicate_of_source), duplicate_count
434 in sorted(duplicates_across_sources.items())
441 Build one reporting entry per conflicting parsed key.
443 The source and value order follows the original record order, and the kept
444 value is marked explicitly in the value list.
446 kept_value_by_key = {}
447 for record
in kept_records:
448 if record[
"record_kind"] != GeneratorSettingsRecord.PARSED_COMMAND:
450 kept_value_by_key[record[
"normalized_key"]] = record[
"normalized_value"]
453 for record
in records:
454 if record[
"record_kind"] != GeneratorSettingsRecord.PARSED_COMMAND:
457 key = record[
"normalized_key"]
458 source_name = record[
"source_name"]
459 value = record[
"normalized_value"]
461 key_entry = values_by_key.setdefault(
468 "source_to_values": {},
471 if source_name
not in key_entry[
"source_set"]:
472 key_entry[
"source_set"].
add(source_name)
473 key_entry[
"sources"].append(source_name)
474 if value
not in key_entry[
"value_set"]:
475 key_entry[
"value_set"].
add(value)
476 key_entry[
"values"].append(value)
477 key_entry[
"source_to_values"].setdefault(source_name,
set()).
add(value)
479 conflict_details = []
480 for key, key_entry
in values_by_key.items():
481 merged_values =
set()
482 for values
in key_entry[
"source_to_values"].values():
483 merged_values.update(values)
484 if len(merged_values) <= 1
or len(key_entry[
"source_to_values"]) <= 1:
487 kept_value = kept_value_by_key.get(key)
489 for value
in key_entry[
"values"]:
490 if value == kept_value:
491 marked_values.append(f
"{value} (kept)")
493 marked_values.append(value)
495 conflict_details.append({
497 "sources": key_entry[
"sources"],
498 "values": marked_values,
500 return conflict_details
504 """Find keys assigned to multiple values within or across sources."""
505 values_by_key_and_source = {}
506 for record
in records:
507 if record[
"record_kind"] != GeneratorSettingsRecord.PARSED_COMMAND:
510 key = record[
"normalized_key"]
511 source_name = record[
"source_name"]
512 value = record[
"normalized_value"]
513 values_by_key_and_source.setdefault(key, {}).setdefault(
519 for key, source_to_values
in values_by_key_and_source.items():
520 for source_name, values
in source_to_values.items():
523 "type":
"intra_source_conflict",
525 "source": source_name,
526 "values": sorted(values),
529 merged_values =
set()
530 for values
in source_to_values.values():
531 merged_values.update(values)
532 if len(merged_values) > 1
and len(source_to_values) > 1:
534 "type":
"inter_source_conflict",
536 "sources": sorted(source_to_values.keys()),
537 "values": sorted(merged_values),
543 """Print warnings from the structured report."""
544 issue_prefix =
"Potential issue with generator settings"
546 duplicates = report.get(
"removed_identical", [])
547 conflict_details = report.get(
"conflict_details", [])
548 if not duplicates
and not conflict_details:
551 genSettingsLog.warning(
552 f
"{issue_prefix} [{context}]: found {len(duplicates)} duplicate "
553 f
"setting(s) across sources and {len(conflict_details)} conflicting "
560 record.get(
"source",
""),
561 record.get(
"duplicate_of_source",
""),
562 record.get(
"setting",
""),
565 source_name = entry.get(
"source",
"<unknown>")
566 duplicate_of_source = entry.get(
"duplicate_of_source",
"<unknown>")
567 setting = entry.get(
"setting",
"<unknown>")
568 source_list = [source_name]
569 if duplicate_of_source != source_name:
570 source_list.append(duplicate_of_source)
571 genSettingsLog.warning(
572 f
"{issue_prefix} [{context}]: duplicate setting from sources "
573 f
"[{', '.join(source_list)}]: {setting}"
576 for entry
in conflict_details:
577 key = entry.get(
"key",
"<unknown>")
578 sources =
", ".join(entry.get(
"sources", []))
579 values =
", ".join(entry.get(
"values", []))
580 genSettingsLog.warning(
581 f
"{issue_prefix} [{context}]: conflicting setting '{key}' across "
582 f
"sources [{sources}] -> [{values}]"
__init__(self, source, values, precedence, separators="=", keep=GeneratorSettingsKeep.LAST, report_context=None)
opt_value(self, stored_value)
store(self, assigned_value)
default(self, default_commands)
__init__(self, layers=None)
resolve(self, report_context=None, emit_report=False)
bool add(const std::string &hname, TKey *tobj)
_deduplicate_records(records, keep)
_build_conflict_details(records, kept_records)
_build_report(records, kept_records, removed_records)
_parse_assignment(setting_text, separators)
_log_report(context, report)
_build_records(layers, separators)