Skip to content

[SYNPY-1844] When merging dataclasses with nested dataclasses the merge function is now recrursive.#1412

Merged
andrewelamb merged 8 commits into
developfrom
SYNPY-1844
Jun 25, 2026
Merged

[SYNPY-1844] When merging dataclasses with nested dataclasses the merge function is now recrursive.#1412
andrewelamb merged 8 commits into
developfrom
SYNPY-1844

Conversation

@andrewelamb

@andrewelamb andrewelamb commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Problem:

  • merge_dataclass_entities in synapseclient/core/utils.py did not correctly handle entities containing nested dataclass fields (e.g. a CurationTask whose task_properties field is itself a dataclass).
  • When both the source and destination held a nested dataclass on the same field, the old code unconditionally overwrote the destination's nested dataclass with the source's, bypassing the "destination wins on conflict" rule that applies to all scalar fields.
  • This meant that user-supplied changes to nested dataclass fields were silently discarded during a store.

Solution:

  • Made merge_dataclass_entities recursive: when both source and destination hold a dataclass instance on the same field, the function now recurses into that nested dataclass and applies the same merge rules field-by-field, rather than blindly replacing the destination value.
  • Added a guard for the case where the destination's field is None or not a dataclass — in that case the source value is used directly (existing gap-fill behavior).
  • Also updated the type annotations for source, destination, and the return value to use a TypeVar (T) instead of the previous hard-coded Union of specific model names. This correctly expresses that both arguments must be the same type and the return is that same type, and removes the need to update the annotation whenever a new model is added.
  • Removed the now-unnecessary TYPE_CHECKING import of the specific model names that existed solely to support the old Union annotation.

Testing:

  • Added a new TestMergeDataclassEntities test class in tests/unit/synapseclient/core/unit_test_utils.py covering:
    • Basic scalar gap-fill and conflict resolution
    • Annotations dict merging
    • Columns dict merging (adding new columns, recursively merging existing ones)
    • Items list merging (appending only new ids)
    • fields_to_ignore and fields_to_preserve_from_source behavior
    • Nested dataclass field: preserved when source is None, destination wins on conflict, sub-fields recurse-merge correctly, and source wins when destination field holds a non-dataclass value

@andrewelamb andrewelamb requested a review from a team as a code owner June 22, 2026 20:13
@andrewelamb andrewelamb marked this pull request as draft June 22, 2026 20:13
@andrewelamb andrewelamb changed the title when merging nested dataclasses together, the fucntion is now recursive [SYNPY-1844] When merging dataclasses with nested dataclasses the merge functionis now recrursive. Jun 22, 2026
@andrewelamb andrewelamb marked this pull request as ready for review June 22, 2026 20:23
@andrewelamb andrewelamb changed the title [SYNPY-1844] When merging dataclasses with nested dataclasses the merge functionis now recrursive. [SYNPY-1844] When merging dataclasses with nested dataclasses the merge function is now recrursive. Jun 22, 2026
@thomasyu888 thomasyu888 requested a review from BryanFauble June 22, 2026 23:01
Comment thread synapseclient/core/utils.py Outdated
del incoming_object[key]


T = TypeVar("_T")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What does this mean? Does it mean any type is allowed? I think this is different from the original: typing.Union[ "Project", "Folder", "File", "Table", "Column", "Evaluation" ]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's correct! This should work with any dataclass, not just the ones currently listed. It was already being called with CurationTask. There's just no dataclass type that I'm aware of.

In fact, it might work with any Python object that has the getattr and setattr methods.

@linglp linglp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM! I verified the fix worked on my end.

Comment thread synapseclient/core/utils.py Outdated
Comment thread tests/unit/synapseclient/core/unit_test_utils.py

@BryanFauble BryanFauble left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's add in that additional test case, otherwise LGTM!

@andrewelamb andrewelamb merged commit ba79ecc into develop Jun 25, 2026
17 of 23 checks passed
@andrewelamb andrewelamb deleted the SYNPY-1844 branch June 25, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants