diff --git a/src/allotropy/parsers/cytiva_unicorn/reader/unicorn_zip_handler.py b/src/allotropy/parsers/cytiva_unicorn/reader/unicorn_zip_handler.py index 1ea205e800..a9bec798bd 100644 --- a/src/allotropy/parsers/cytiva_unicorn/reader/unicorn_zip_handler.py +++ b/src/allotropy/parsers/cytiva_unicorn/reader/unicorn_zip_handler.py @@ -65,7 +65,9 @@ def get_system_data(self) -> StrictXmlElement: system_data = self.get_zip_from_pattern("SystemData(.zip)?$") b_stream = system_data.get_file_from_pattern("^Xml$") raw_content = self.filter_xml_metadata(b_stream).read() - return StrictXmlElement.create_from_bytes(raw_content) + return StrictXmlElement.create_from_bytes( + raw_content, mark_read={"attr:FormatVersion", "attr:UNICORNVersion"} + ) def get_results(self) -> StrictXmlElement: b_stream = self.get_file_from_pattern("Result.xml$") @@ -77,12 +79,16 @@ def get_instrument_config_data(self) -> StrictXmlElement: instrument_config_data = self.get_zip_from_pattern(instrument_regex) b_stream = instrument_config_data.get_file_from_pattern("^Xml$") raw_content = self.filter_xml_metadata(b_stream).read() - return StrictXmlElement.create_from_bytes(raw_content) + return StrictXmlElement.create_from_bytes( + raw_content, mark_read={"attr:FormatVersion", "attr:UNICORNVersion"} + ) def get_evaluation_log(self) -> StrictXmlElement: b_stream = self.get_file_from_pattern("EvaluationLog.xml$") raw_content = self.filter_xml_metadata(b_stream).read() - return StrictXmlElement.create_from_bytes(raw_content) + return StrictXmlElement.create_from_bytes( + raw_content, mark_read={"attr:FormatVersion", "attr:UNICORNVersion"} + ) def get_chrom_1(self) -> StrictXmlElement: b_stream = self.get_file_from_pattern("Chrom.1.Xml$") @@ -93,4 +99,6 @@ def get_column_type_data(self) -> StrictXmlElement: column_type_data = self.get_zip_from_pattern("ColumnTypeData(.zip)?$") b_stream = column_type_data.get_file_from_pattern("^Xml$") raw_content = self.filter_xml_metadata(b_stream).read() - return StrictXmlElement.create_from_bytes(raw_content) + return StrictXmlElement.create_from_bytes( + raw_content, mark_read={"attr:FormatVersion", "attr:UNICORNVersion"} + ) diff --git a/src/allotropy/parsers/cytiva_unicorn/structure/measurement_group.py b/src/allotropy/parsers/cytiva_unicorn/structure/measurement_group.py index 2279889762..1ef61c98f6 100644 --- a/src/allotropy/parsers/cytiva_unicorn/structure/measurement_group.py +++ b/src/allotropy/parsers/cytiva_unicorn/structure/measurement_group.py @@ -62,34 +62,32 @@ def create_measurement_groups( FlowMeasurement.create_or_none(handler, elements, static_docs), TemperatureMeasurement.create_or_none(handler, elements, static_docs), ] + # Some curves may not be read due not matching any supported measurement type, mark these as read. + for element in elements: + element.mark_read("attr:CurveDataType") + + chrom_1.mark_read( + { + "element:TimeUnit", + "element:VolumeUnit", + "element:IsReadonly", + "attr:UNICORNVersion", + } + ) - measurement_group = [ + return [ MeasurementGroup( measurements=[measurement for measurement in measurements if measurement], fractions=create_fractions(event_curves) if event_curves else None, logs=create_logs(event_curves) if event_curves else None, - measurement_aggregate_custom_info={}, - ) - ] - - chrom_1.mark_read({"element:TimeUnit", "element:VolumeUnit", "element:IsReadonly"}) - custom_info = chrom_1.get_unread() - - if measurement_group[0].measurement_aggregate_custom_info is not None: - custom_info.pop("UNICORNVersion") - custom_info.update( - { + measurement_aggregate_custom_info={ "RunIndex": results.get_sub_text_or_none("RunIndex"), "RunType": results.get_sub_text_or_none("RunType"), "Name": results.get_sub_text_or_none("Name"), - } - ) - custom_info_sorted = dict(sorted(custom_info.items())) - measurement_group[0].measurement_aggregate_custom_info.update( - custom_info_sorted + **dict(sorted(chrom_1.get_unread().items())), + }, ) - - return measurement_group + ] def create_fractions(event_curves: StrictXmlElement) -> list[Fraction]: @@ -142,10 +140,12 @@ def create_logs(event_curves: StrictXmlElement) -> list[Log]: "Method", "Manual", ]: + event.get_unread() continue event_text = event.get_sub_text_or_none("EventText") if event_text is None: + event.get_unread() continue event_subtype = event.get_attr_or_none("EventSubType") diff --git a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/absorbance.py b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/absorbance.py index 6973a80be8..de7090b9cb 100644 --- a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/absorbance.py +++ b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/absorbance.py @@ -57,12 +57,16 @@ def create_or_none( elements: list[StrictXmlElement], static_docs: StaticDocs, ) -> UnicornMeasurement: + element = assert_not_none( + cls.filter_curve_or_none(elements, cls.get_curve_regex()), + "Unable to find curve data for absorbance measurement.", + ) measurement = cls.get_measurement( static_docs=static_docs, chromatogram_data_cube=assert_not_none( cls.get_data_cube_or_none( handler, - cls.filter_curve_or_none(elements, cls.get_curve_regex()), + element, DataCubeComponent( type_=FieldComponentDatatype.float, concept="absorbance", @@ -75,13 +79,13 @@ def create_or_none( DeviceControlDoc( device_type=DEVICE_TYPE, start_time=static_docs.start_time, - device_control_custom_info={}, + device_control_custom_info=cls.get_device_control_custom_info( + element + ), ) ], peaks=cls.get_peaks(handler), - ) - cls.add_custom_info( - measurement, cls.filter_curve_or_none(elements, cls.get_curve_regex()) + processed_data_custom_info=cls.get_processed_data_custom_info(element), ) return measurement @@ -154,6 +158,7 @@ def get_peaks_custom_info( @classmethod def get_peaks(cls, handler: UnicornZipHandler) -> list[Peak]: chrom_1 = handler.get_chrom_1() + chrom_1.get_unread() # chrom_1 data is read in create_measurement_groups peaks = chrom_1.recursive_find_or_none(["PeakTables", "PeakTable", "Peaks"]) output = [] for idx, peak in enumerate(peaks.findall("Peak") if peaks else [], start=1): diff --git a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/concentration.py b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/concentration.py index 9cc6dbd8b1..8d712df68e 100644 --- a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/concentration.py +++ b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/concentration.py @@ -28,6 +28,7 @@ def create_or_none( elements: list[StrictXmlElement], static_docs: StaticDocs, ) -> UnicornMeasurement | None: + element = cls.filter_curve_or_none(elements, r"^Conc B$") measurement = cls.get_measurement( static_docs=static_docs, device_control_docs=[ @@ -36,19 +37,19 @@ def create_or_none( start_time=static_docs.start_time, solvent_conc_data_cube=cls.get_data_cube_or_none( handler, - cls.filter_curve_or_none(elements, r"^Conc B$"), + element, DataCubeComponent( type_=FieldComponentDatatype.float, concept="solvent concentration", unit="%", ), ), - device_control_custom_info={}, + device_control_custom_info=cls.get_device_control_custom_info( + element + ), ), ], - ) - cls.add_custom_info( - measurement, cls.filter_curve_or_none(elements, r"^Conc B$") + processed_data_custom_info=cls.get_processed_data_custom_info(element), ) return measurement if cls.is_valid(cls.get_data_cubes(measurement)) else None diff --git a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/conductivity.py b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/conductivity.py index cc772885c3..7495fbd788 100644 --- a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/conductivity.py +++ b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/conductivity.py @@ -31,11 +31,12 @@ def create_or_none( elements: list[StrictXmlElement], static_docs: StaticDocs, ) -> UnicornMeasurement | None: + element = cls.filter_curve_or_none(elements, r"^Cond$") measurement = cls.get_measurement( static_docs=static_docs, chromatogram_data_cube=cls.get_data_cube_or_none( handler, - cls.filter_curve_or_none(elements, r"^Cond$"), + element, DataCubeComponent( type_=FieldComponentDatatype.float, concept="electric conductivity", @@ -56,11 +57,13 @@ def create_or_none( DeviceControlDoc( device_type=DEVICE_TYPE, start_time=static_docs.start_time, - device_control_custom_info={}, + device_control_custom_info=cls.get_device_control_custom_info( + element + ), ) ], + processed_data_custom_info=cls.get_processed_data_custom_info(element), ) - cls.add_custom_info(measurement, cls.filter_curve_or_none(elements, r"^Cond$")) return measurement if cls.is_valid(cls.get_data_cubes(measurement)) else None @classmethod diff --git a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/flow.py b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/flow.py index 57439410c8..3fe3ab40ac 100644 --- a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/flow.py +++ b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/flow.py @@ -36,7 +36,6 @@ def create_device_control( start_time=start_time, sample_flow_data_cube=sample_flow_data_cube, system_flow_data_cube=system_flow_data_cube, - device_control_custom_info={}, ) @classmethod diff --git a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/generic.py b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/generic.py index ddcf78dd6d..55da2416dc 100644 --- a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/generic.py +++ b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/generic.py @@ -1,6 +1,7 @@ from __future__ import annotations from re import search +from typing import Any from allotropy.allotrope.schema_mappers.adm.liquid_chromatography.benchling._2023._09.liquid_chromatography import ( DeviceControlDoc, @@ -28,64 +29,58 @@ class UnicornMeasurement(Measurement): @classmethod - def add_custom_info( - cls, measurement: Measurement, base_element: StrictXmlElement | None - ) -> None: - if base_element: - device_control_custom_info = { - "ColumnVolume": base_element.get_sub_text_or_none("ColumnVolume"), - "ScanInterval": base_element.get_sub_text_or_none("ScanInterval"), - } + def get_device_control_custom_info( + cls, base_element: StrictXmlElement | None + ) -> dict[str, Any]: + if not base_element: + return {} + return { + "ColumnVolume": base_element.get_sub_text_or_none("ColumnVolume"), + "ScanInterval": base_element.get_sub_text_or_none("ScanInterval"), + } - processed_data_custom_info = { - "AmplitudePrecision": base_element.get_sub_text_or_none( - "AmplitudePrecision" - ), - "AmplitudeUnit": base_element.get_sub_text_or_none("AmplitudeUnit"), - "ChromatogramID": base_element.get_sub_text_or_none("ChromatogramID"), - "ChromatogramName": base_element.get_sub_text_or_none( - "ChromatogramName" - ), - "ChromatogramStartTime": base_element.get_sub_text_or_none( - "ChromatogramStartTime" - ), - "ChromatogramStartTimeUtcOffsetMinutes": base_element.get_sub_text_or_none( - "ChromatogramStartTimeUtcOffsetMinutes" - ), - "ColumnVolumeUnitName": base_element.get_sub_text_or_none( - "ColumnVolumeUnitName" - ), - "CurveDataType": base_element.get_sub_text_or_none("CurveDataType"), - "CurveNumber": base_element.get_sub_text_or_none("CurveNumber"), - "DistanceBetweenPoints": base_element.get_sub_text_or_none( - "DistanceBetweenPoints" - ), - "DistancetoStartPoints": base_element.get_sub_text_or_none( - "DistancetoStartPoints" - ), - "IsExternal": base_element.get_sub_text_or_none("IsExternal"), - "IsOriginalData": base_element.get_sub_text_or_none("IsOriginalData"), - "IsReadOnly": base_element.get_sub_text_or_none("IsReadOnly"), - "IsoChroneType": base_element.get_sub_text_or_none("IsoChroneType"), - "MethodStartTime": base_element.get_sub_text_or_none("MethodStartTime"), - "MethodStartTimeUtcOffsetMinutes": base_element.get_sub_text_or_none( - "MethodStartTimeUtcOffsetMinutes" - ), - "TimeUnit": base_element.get_sub_text_or_none("TimeUnit"), - "VolumeUnit": base_element.get_sub_text_or_none("VolumeUnit"), - } - if ( - measurement.device_control_docs[0].device_control_custom_info - is not None - ): - measurement.device_control_docs[0].device_control_custom_info.update( - device_control_custom_info - ) - if measurement.processed_data_custom_info is not None: - processed_data_custom_info.update(base_element.get_unread()) - measurement.processed_data_custom_info.update( - processed_data_custom_info - ) + @classmethod + def get_processed_data_custom_info( + cls, base_element: StrictXmlElement | None + ) -> dict[str, Any]: + if not base_element: + return {} + return { + "AmplitudePrecision": base_element.get_sub_text_or_none( + "AmplitudePrecision" + ), + "AmplitudeUnit": base_element.get_sub_text_or_none("AmplitudeUnit"), + "ChromatogramID": base_element.get_sub_text_or_none("ChromatogramID"), + "ChromatogramName": base_element.get_sub_text_or_none("ChromatogramName"), + "ChromatogramStartTime": base_element.get_sub_text_or_none( + "ChromatogramStartTime" + ), + "ChromatogramStartTimeUtcOffsetMinutes": base_element.get_sub_text_or_none( + "ChromatogramStartTimeUtcOffsetMinutes" + ), + "ColumnVolumeUnitName": base_element.get_sub_text_or_none( + "ColumnVolumeUnitName" + ), + "CurveDataType": base_element.get_attr_or_none("CurveDataType"), + "CurveNumber": base_element.get_sub_text_or_none("CurveNumber"), + "DistanceBetweenPoints": base_element.get_sub_text_or_none( + "DistanceBetweenPoints" + ), + "DistancetoStartPoints": base_element.get_sub_text_or_none( + "DistancetoStartPoints" + ), + "IsExternal": base_element.get_sub_text_or_none("IsExternal"), + "IsOriginalData": base_element.get_sub_text_or_none("IsOriginalData"), + "IsReadOnly": base_element.get_sub_text_or_none("IsReadOnly"), + "IsoChroneType": base_element.get_sub_text_or_none("IsoChroneType"), + "MethodStartTime": base_element.get_sub_text_or_none("MethodStartTime"), + "MethodStartTimeUtcOffsetMinutes": base_element.get_sub_text_or_none( + "MethodStartTimeUtcOffsetMinutes" + ), + "TimeUnit": base_element.get_sub_text_or_none("TimeUnit"), + "VolumeUnit": base_element.get_sub_text_or_none("VolumeUnit"), + **base_element.get_unread(), + } @classmethod def filter_curve_or_none( @@ -141,6 +136,7 @@ def get_measurement( processed_data_chromatogram_data_cube: DataCube | None = None, derived_column_pressure_data_cube: DataCube | None = None, peaks: list[Peak] | None = None, + processed_data_custom_info: dict[str, Any] | None = None, ) -> UnicornMeasurement: return UnicornMeasurement( measurement_identifier=random_uuid_str(), @@ -166,7 +162,7 @@ def get_measurement( "sample_volume_3": static_docs.sample_volume_3, }, peaks=peaks, - processed_data_custom_info={}, + processed_data_custom_info=processed_data_custom_info, ) @classmethod diff --git a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/ph.py b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/ph.py index 12f7bbe0ff..24facd459a 100644 --- a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/ph.py +++ b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/ph.py @@ -28,11 +28,12 @@ def create_or_none( elements: list[StrictXmlElement], static_docs: StaticDocs, ) -> UnicornMeasurement | None: + element = cls.filter_curve_or_none(elements, r"^pH$") measurement = cls.get_measurement( static_docs=static_docs, chromatogram_data_cube=cls.get_data_cube_or_none( handler, - cls.filter_curve_or_none(elements, r"^pH$"), + element, DataCubeComponent( type_=FieldComponentDatatype.float, concept="pH", @@ -43,11 +44,13 @@ def create_or_none( DeviceControlDoc( device_type=DEVICE_TYPE, start_time=static_docs.start_time, - device_control_custom_info={}, + device_control_custom_info=cls.get_device_control_custom_info( + element + ), ) ], + processed_data_custom_info=cls.get_processed_data_custom_info(element), ) - cls.add_custom_info(measurement, cls.filter_curve_or_none(elements, r"^pH$")) return measurement if cls.is_valid(cls.get_data_cubes(measurement)) else None @classmethod diff --git a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/pressure.py b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/pressure.py index d7805f99bd..1754a5d9aa 100644 --- a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/pressure.py +++ b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/pressure.py @@ -28,11 +28,12 @@ def create_or_none( elements: list[StrictXmlElement], static_docs: StaticDocs, ) -> UnicornMeasurement | None: + element = cls.filter_curve_or_none(elements, r"^DeltaC pressure$") measurement = cls.get_measurement( static_docs=static_docs, derived_column_pressure_data_cube=cls.get_data_cube_or_none( handler, - cls.filter_curve_or_none(elements, r"^DeltaC pressure$"), + element, DataCubeComponent( type_=FieldComponentDatatype.float, concept="delta column pressure", @@ -79,12 +80,12 @@ def create_or_none( unit="MPa", ), ), - device_control_custom_info={}, + device_control_custom_info=cls.get_device_control_custom_info( + element + ), ), ], - ) - cls.add_custom_info( - measurement, cls.filter_curve_or_none(elements, r"^DeltaC pressure$") + processed_data_custom_info=cls.get_processed_data_custom_info(element), ) return measurement if cls.is_valid(cls.get_data_cubes(measurement)) else None diff --git a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/temperature.py b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/temperature.py index 18474c977c..04bef1d9e5 100644 --- a/src/allotropy/parsers/cytiva_unicorn/structure/measurements/temperature.py +++ b/src/allotropy/parsers/cytiva_unicorn/structure/measurements/temperature.py @@ -28,6 +28,7 @@ def create_or_none( elements: list[StrictXmlElement], static_docs: StaticDocs, ) -> UnicornMeasurement | None: + element = cls.filter_curve_or_none(elements, r"^Cond temp$") measurement = cls.get_measurement( static_docs=static_docs, device_control_docs=[ @@ -36,19 +37,19 @@ def create_or_none( start_time=static_docs.start_time, temperature_profile_data_cube=cls.get_data_cube_or_none( handler, - cls.filter_curve_or_none(elements, r"^Cond temp$"), + element, DataCubeComponent( type_=FieldComponentDatatype.float, concept="temperature", unit="degC", ), ), - device_control_custom_info={}, + device_control_custom_info=cls.get_device_control_custom_info( + element + ), ), ], - ) - cls.add_custom_info( - measurement, cls.filter_curve_or_none(elements, r"^Cond temp$") + processed_data_custom_info=cls.get_processed_data_custom_info(element), ) return measurement if cls.is_valid(cls.get_data_cubes(measurement)) else None diff --git a/src/allotropy/parsers/cytiva_unicorn/structure/metadata.py b/src/allotropy/parsers/cytiva_unicorn/structure/metadata.py index f89e88cd35..3dd82e9eab 100644 --- a/src/allotropy/parsers/cytiva_unicorn/structure/metadata.py +++ b/src/allotropy/parsers/cytiva_unicorn/structure/metadata.py @@ -29,18 +29,19 @@ def create_metadata( ) -> Metadata: system_data = handler.get_system_data() instrument_config_data = handler.get_instrument_config_data() - instrument_config = system_data.recursive_find( ["System", "InstrumentConfiguration"] ) - + instrument_config.mark_read( + {"attr:FormatVersion", "attr:UNICORNVersion", "attr:Version"} + ) system_name = results.find_or_none("SystemName") firmware_version = instrument_config_data.find_or_none("FirmwareVersion") results.mark_read( {"element:RunIndex", "element:RunType", "element:BatchId", "element:Name"} ) - metadata = Metadata( + return Metadata( asset_management_identifier=instrument_config.get_attr("Description"), product_manufacturer="Cytiva Life Sciences", device_identifier=system_name.get_text_or_none() if system_name else None, @@ -50,13 +51,9 @@ def create_metadata( analyst=get_audit_trail_entry_user(handler), file_name=Path(file_path).name, unc_path=file_path, - data_system_custom_info={}, software_version=results.get_attr("UNICORNVersion"), device_system_custom_info={ "SystemTypeName": results.get_sub_text_or_none("SystemTypeName") }, + data_system_custom_info=results.get_unread(), ) - - if metadata.data_system_custom_info is not None: - metadata.data_system_custom_info.update(results.get_unread()) - return metadata diff --git a/src/allotropy/parsers/cytiva_unicorn/structure/static_docs.py b/src/allotropy/parsers/cytiva_unicorn/structure/static_docs.py index cf119e8d36..93b0e9c6b1 100644 --- a/src/allotropy/parsers/cytiva_unicorn/structure/static_docs.py +++ b/src/allotropy/parsers/cytiva_unicorn/structure/static_docs.py @@ -127,7 +127,12 @@ def create( void_volume = chromatogram_analysis_settings.recursive_find_or_none( ["IntegrationSettings", "ColumnProperties", "ColumnVolume"] ) + chromatogram_analysis_settings.mark_read( + {"attr:FormatVersion", "attr:UNICORNVersion"} + ) + # CurveDataType is read in add_custom_info + curve.mark_read("attr:CurveDataType") return StaticDocs( chromatography_serial_num=( article_number.get_text_or_none() diff --git a/src/allotropy/parsers/utils/strict_xml_element.py b/src/allotropy/parsers/utils/strict_xml_element.py index ab8b8e8a8c..934c33d40a 100644 --- a/src/allotropy/parsers/utils/strict_xml_element.py +++ b/src/allotropy/parsers/utils/strict_xml_element.py @@ -14,24 +14,30 @@ class StrictXmlElement: @classmethod - def create_from_bytes(cls, data: bytes) -> StrictXmlElement: - return StrictXmlElement(fromstring(data)) + def create_from_bytes( + cls, data: bytes, mark_read: set[str] | None = None + ) -> StrictXmlElement: + return StrictXmlElement(fromstring(data), mark_read=mark_read) def __init__( - self, element: ElementTree.Element, namespaces: dict[str, str] | None = None + self, + element: ElementTree.Element, + namespaces: dict[str, str] | None = None, + mark_read: set[str] | None = None, ): self.element = element self.namespaces = namespaces or {} self.read_keys: set[str] = set() self.errored = False self.creation_stack = traceback.extract_stack() + self.mark_read(mark_read or set()) def __del__(self) -> None: if self.errored: return # NOTE: this will be turned on by default when all callers have been updated to pass the warning. # Only consider attributes as available keys, not child elements - attribute_keys = set() + attribute_keys: set[str] = set() # Add attributes with "attr:" prefix for attr_name in self.element.attrib.keys(): @@ -44,10 +50,10 @@ def __del__(self) -> None: attribute_keys.add(f"ns_attr:{namespace_key}:{field_name}") # Filter out attributes that have been read in either form (attr: or ns_attr:) - unread_keys = set() + unread_keys: set[str] = set() for key in attribute_keys: if key.startswith("attr:"): - attr_name = key[5:] # Remove "attr:" prefix + attr_name = key.removeprefix("attr:") # Check if this is a namespaced attribute that has been read via ns_attr is_namespaced_and_read = False @@ -100,7 +106,7 @@ def __del__(self) -> None: ) warnings.warn( - f"StrictXmlElement went out of scope without reading all keys{creation_info}, unread: {sorted(unread_keys)}.", + f"StrictXmlElement '{self.element.tag}' went out of scope without reading all keys{creation_info}, unread: {sorted(unread_keys)}.", stacklevel=2, )