From 4d02859e8b3d3b701232f18a83233644ca09fcd5 Mon Sep 17 00:00:00 2001 From: Nathan Stender Date: Tue, 7 Apr 2026 03:26:00 -0400 Subject: [PATCH] fix: Use immutable copy pattern for WellItem result attachment Previously, WellItem._result was mutated after construction inside _create_measurement(), violating the intended immutability of the private field. This was a temporary workaround added during the PR #546 refactor when Result was changed from a public mutable field to a private field with property accessor. The root issue: WellItem and Result data come from different file sections and are parsed at different times, but calculated documents need access to result data through well items. Solution: Use dataclasses.replace() to create immutable copies of WellItem with _result properly set at construction time. This avoids mutation while maintaining the architectural separation between "wells without results" and "wells with results". Changes: - Added enrich_wells_with_results() that returns new Well/WellItem instances with results attached via dataclasses.replace() - Removed TODO and mutation from _create_measurement() - Parser now uses enriched_wells for both calculated documents and measurements All tests pass, no golden file changes. Co-Authored-By: Claude Opus 4.1 --- .../appbio_quantstudio_data_creator.py | 26 +++++++++++++++++-- .../appbio_quantstudio_parser.py | 8 ++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_data_creator.py b/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_data_creator.py index d8571c9ead..38883603c4 100644 --- a/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_data_creator.py +++ b/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_data_creator.py @@ -241,8 +241,6 @@ def _create_measurement( ) -> Measurement | None: if not result: return None - # TODO: temp workaround for cal doc result - well_item._result = result ( reporter_dye_data_cube, @@ -332,6 +330,30 @@ def get_well_item_results( ) +def enrich_wells_with_results( + wells: list[Well], + results_data: dict[int, dict[str, Result]], +) -> list[Well]: + """Return new Wells with results attached to well items via immutable copy. + + This creates new WellItem instances with _result properly set at construction time + (via dataclasses.replace), avoiding mutation of the private field after creation. + """ + from dataclasses import replace + + enriched_wells = [] + for well in wells: + enriched_items = [] + for well_item in well.items: + result = get_well_item_results(well_item, results_data) + if result: + enriched_items.append(replace(well_item, _result=result)) + else: + enriched_items.append(well_item) + enriched_wells.append(replace(well, items=enriched_items)) + return enriched_wells + + def get_well_item_amp_data( well_item: WellItem, amp_data: dict[int, dict[str, AmplificationData]] ) -> AmplificationData | None: diff --git a/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_parser.py b/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_parser.py index d0937e9e5a..12d0b6518d 100644 --- a/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_parser.py +++ b/src/allotropy/parsers/appbio_quantstudio/appbio_quantstudio_parser.py @@ -8,6 +8,7 @@ create_calculated_data, create_measurement_groups, create_metadata, + enrich_wells_with_results, ) from allotropy.parsers.appbio_quantstudio.appbio_quantstudio_reader import ( AppBioQuantStudioReader, @@ -41,8 +42,11 @@ def parse_data( results_data, result_metadata = Result.create(reader, header.experiment_type) melt_data = MeltCurveRawData.create(reader) + # Create immutable copies of wells with results attached + enriched_wells = enrich_wells_with_results(wells, results_data) + calculated_data_documents = iter_calculated_data_documents( - [well_item for well in wells for well_item in well.items], + [well_item for well in enriched_wells for well_item in well.items], header.experiment_type, result_metadata.reference_sample_description, result_metadata.reference_dna_description, @@ -52,7 +56,7 @@ def parse_data( metadata=create_metadata(header, original_file_path), measurement_groups=create_measurement_groups( header, - wells, + enriched_wells, amp_data, multi_data, results_data,