Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 198 additions & 5 deletions space_packet_parser/xtce/containers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Module with XTCE models related to SequenceContainers"""

import warnings
from dataclasses import dataclass, field
from typing import Any, Union

Expand All @@ -12,6 +13,171 @@
from space_packet_parser.xtce import comparisons, parameter_types, parameters


@dataclass
class ParameterRefEntry(common.Parseable, common.XmlObject):
"""<xtce:ParameterRefEntry>

Represents a reference to a parameter in an EntryList, with optional conditional inclusion
and repeat logic.

Parameters
----------
parameter_ref : str
Name reference to the Parameter
include_condition : Optional[comparisons.MatchCriteria]
Condition for inclusion. If None, parameter is always included.
repeat_entry : Optional[Any]
Repeat information. Not currently supported - will raise NotImplementedError during parse.
"""

parameter_ref: str
include_condition: Optional[comparisons.MatchCriteria] = None
repeat_entry: Optional[Any] = None

def parse(self, packet: spp.SpacePacket, parameter_lookup: dict[str, parameters.Parameter]) -> None:
"""Parse the parameter reference entry, handling conditional inclusion.

Parameters
----------
packet : spp.SpacePacket
The packet being parsed
parameter_lookup : dict[str, parameters.Parameter]
Dictionary to look up parameter objects by name

Raises
------
NotImplementedError
If RepeatEntry is specified
KeyError
If the referenced parameter is not found in parameter_lookup
"""
# Check if repeat_entry is specified
if self.repeat_entry is not None:
raise NotImplementedError("RepeatEntry is not currently supported in parsing")

# Evaluate include condition if it exists
if self.include_condition is not None:
if not self.include_condition.evaluate(packet):
# Condition is False, skip this parameter
return

# Parse the parameter
try:
parameter = parameter_lookup[self.parameter_ref]
except KeyError as err:
raise KeyError(
f"Parameter '{self.parameter_ref}' referenced in ParameterRefEntry not found in parameter lookup. "
f"Available parameters: {list(parameter_lookup.keys())}"
) from err
parameter.parse(packet)

@classmethod
def from_xml(
cls,
element: ElementTree.Element,
*,
tree: Optional[ElementTree.ElementTree] = None,
parameter_lookup: Optional[dict[str, parameters.Parameter]] = None,
parameter_type_lookup: Optional[dict[str, parameter_types.ParameterType]] = None,
container_lookup: Optional[dict[str, Any]] = None,
) -> "ParameterRefEntry":
"""Create a ParameterRefEntry from an XML element.

Parameters
----------
element : ElementTree.Element
The ParameterRefEntry XML element
tree : Optional[ElementTree.ElementTree]
Full XTCE tree
parameter_lookup : Optional[dict[str, parameters.Parameter]]
Ignored
parameter_type_lookup : Optional[dict[str, parameter_types.ParameterType]]
Ignored
container_lookup : Optional[dict[str, Any]]
Ignored

Returns
-------
: ParameterRefEntry
"""
parameter_ref = element.attrib["parameterRef"]

# Parse optional IncludeCondition
include_condition = None
if (include_cond_elem := element.find("IncludeCondition")) is not None:
# IncludeCondition contains a single MatchCriteria element (Comparison, ComparisonList, or BooleanExpression)
if (comparison_list_elem := include_cond_elem.find("ComparisonList")) is not None:
# ComparisonList contains multiple Comparison elements with AND semantics per XTCE spec
# Note: The existing restriction_criteria uses the same pattern (iterfind("*"))
# to iterate over Comparison elements in a ComparisonList
comparisons_list = [
comparisons.Comparison.from_xml(comp) for comp in comparison_list_elem.iterfind("*")
]
# LIMITATION: MatchCriteria interface expects a single object, but ComparisonList
# requires AND logic across multiple comparisons. For now, we only support single
# comparisons in ComparisonList. Full AND logic would require a new MatchCriteria
# subclass or modifying include_condition to accept a list of MatchCriteria.
# This follows the same limitation pattern used in restriction_criteria where
# multiple MatchCriteria are stored in a list on the SequenceContainer itself.
Comment on lines +116 to +121
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment describes a limitation but doesn't clearly state what the expected behavior is according to the XTCE spec. The comment should explicitly state that according to the XTCE specification, ComparisonList should have AND semantics across all comparisons, and link to the relevant section if possible.

Copilot uses AI. Check for mistakes.
if len(comparisons_list) == 1:
include_condition = comparisons_list[0]
else:
# For multiple comparisons, warn and use only the first one
warnings.warn(
f"ComparisonList with {len(comparisons_list)} comparisons in IncludeCondition "
f"for parameter '{parameter_ref}'. Only the first comparison will be evaluated. "
f"Full AND logic for ComparisonList in IncludeCondition requires architectural "
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The warning message should indicate the potential consequences of this limitation more clearly. Consider adding that this may result in incorrect packet parsing if the XTCE definition relies on multiple comparisons in the ComparisonList.

Suggested change
f"Full AND logic for ComparisonList in IncludeCondition requires architectural "
f"This may result in incorrect packet parsing if the XTCE definition relies on multiple comparisons "
f"being evaluated with AND logic. Full AND logic for ComparisonList in IncludeCondition requires architectural "

Copilot uses AI. Check for mistakes.
f"changes to support multiple MatchCriteria conditions.",
UserWarning,
)
include_condition = comparisons_list[0] if comparisons_list else None
elif (comparison_elem := include_cond_elem.find("Comparison")) is not None:
include_condition = comparisons.Comparison.from_xml(comparison_elem)
elif (bool_expr_elem := include_cond_elem.find("BooleanExpression")) is not None:
include_condition = comparisons.BooleanExpression.from_xml(bool_expr_elem)

# Parse optional RepeatEntry
repeat_entry = None
if element.find("RepeatEntry") is not None:
# We'll store a placeholder to indicate it was present
repeat_entry = True # Will cause NotImplementedError during parse

return cls(
parameter_ref=parameter_ref,
include_condition=include_condition,
repeat_entry=repeat_entry,
)

def to_xml(self, *, elmaker: ElementMaker) -> ElementTree.Element:
"""Create a ParameterRefEntry XML element.

Parameters
----------
elmaker : ElementMaker
Element factory with predefined namespace

Returns
-------
: ElementTree.Element
"""
entry = elmaker.ParameterRefEntry(parameterRef=self.parameter_ref)

if self.include_condition is not None:
include_cond = elmaker.IncludeCondition(self.include_condition.to_xml(elmaker=elmaker))
entry.append(include_cond)

if self.repeat_entry is not None:
# Placeholder for RepeatEntry serialization
# Since we don't fully parse it, we can't fully serialize it either
warnings.warn(
"RepeatEntry serialization is not fully implemented. "
"The RepeatEntry element will not be included in the XML output.",
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning is issued during serialization but doesn't indicate whether this could cause issues when the XML is read back. Consider adding information about whether round-tripping (parse XML -> serialize -> parse again) will preserve the RepeatEntry information or if it will be lost.

Suggested change
"The RepeatEntry element will not be included in the XML output.",
"The RepeatEntry element will not be included in the XML output. "
"If you parse XML containing RepeatEntry and then serialize it again, the RepeatEntry information will be lost.",

Copilot uses AI. Check for mistakes.
UserWarning,
)

return entry


@dataclass
class SequenceContainer(common.Parseable, common.XmlObject):
"""<xtce:SequenceContainer>
Expand Down Expand Up @@ -56,7 +222,11 @@ def parse(self, packet: spp.SpacePacket) -> None:
This could be recursive if the entry list contains SequenceContainers.
"""
for entry in self.entry_list:
entry.parse(packet=packet)
if isinstance(entry, ParameterRefEntry):
# ParameterRefEntry needs parameter_lookup to resolve references
entry.parse(packet=packet, parameter_lookup=self._parameter_lookup)
else:
entry.parse(packet=packet)

@classmethod
def from_xml(
Expand Down Expand Up @@ -111,8 +281,18 @@ def from_xml(
for entry in element.find("EntryList").iterfind("*"):
entry_tag_name = ElementTree.QName(entry).localname
if entry_tag_name == "ParameterRefEntry":
parameter_name = entry.attrib["parameterRef"]
entry_list.append(parameter_lookup[parameter_name]) # KeyError if parameter is not in the lookup
# Check if this ParameterRefEntry has IncludeCondition or RepeatEntry child elements
has_include_condition = entry.find("IncludeCondition") is not None
has_repeat_entry = entry.find("RepeatEntry") is not None

if has_include_condition or has_repeat_entry:
# Create a ParameterRefEntry object to handle conditional/repeated parsing
param_ref_entry = ParameterRefEntry.from_xml(entry, tree=tree)
entry_list.append(param_ref_entry)
else:
# No special handling needed, use the parameter directly for backward compatibility
parameter_name = entry.attrib["parameterRef"]
entry_list.append(parameter_lookup[parameter_name]) # KeyError if parameter is not in the lookup

elif entry_tag_name == "ContainerRefEntry":
# This container may not have been parsed yet. We need to parse it now so we might as well
Expand All @@ -129,6 +309,14 @@ def from_xml(
)
container_lookup[nested_container.name] = nested_container
entry_list.append(nested_container)
else:
warnings.warn(
f"Unrecognized entry type '{entry_tag_name}' in EntryList for container "
f"'{element.attrib['name']}'. Supported types: ParameterRefEntry, ContainerRefEntry. "
f"Skipping this entry.",
category=UserWarning,
)
continue

short_description = element.attrib.get("shortDescription", None)

Expand All @@ -137,7 +325,7 @@ def from_xml(
else:
long_description = None

return cls(
container = cls(
name=element.attrib["name"],
entry_list=entry_list,
base_container_name=base_container_name,
Expand All @@ -146,6 +334,9 @@ def from_xml(
short_description=short_description,
long_description=long_description,
)
# Store parameter lookup for use during parsing
container._parameter_lookup = parameter_lookup
return container

def to_xml(self, *, elmaker: ElementMaker) -> ElementTree.Element:
"""Create a SequenceContainer XML element
Expand Down Expand Up @@ -188,7 +379,9 @@ def to_xml(self, *, elmaker: ElementMaker) -> ElementTree.Element:

entry_list = em.EntryList()
for entry in self.entry_list:
if isinstance(entry, parameters.Parameter):
if isinstance(entry, ParameterRefEntry):
entry_element = entry.to_xml(elmaker=elmaker)
elif isinstance(entry, parameters.Parameter):
entry_element = em.ParameterRefEntry(parameterRef=entry.name)
elif isinstance(entry, SequenceContainer):
entry_element = em.ContainerRefEntry(containerRef=entry.name)
Expand Down
Loading
Loading