Skip to content

Isolate caller-supplied metadata with deepcopy - #65

Merged
pnorton-usgs merged 1 commit into
developmentfrom
fix/deepcopy-metadata-isolation
Aug 17, 2026
Merged

Isolate caller-supplied metadata with deepcopy#65
pnorton-usgs merged 1 commit into
developmentfrom
fix/deepcopy-metadata-isolation

Conversation

@pnorton-usgs

Copy link
Copy Markdown
Member

Summary

Prevents caller-supplied metadata from being mutated by Parameters instances. Related to #64.

Problem

Parameters.__init__ previously shared sub-dicts between self.__full_metadata and self.metadata. When bounded parameter resolution or dimension size changes mutated self.metadata, those changes leaked into __full_metadata. This caused create_subset() to pass already-resolved metadata (numeric maximum instead of dimension name) to child Parameters instances, crashing on bounded parameter add().

The same root cause also affected callers who reuse a single MetaData().metadata dict across multiple Parameters instances (e.g. pywatershed's DomainSubset tests).

Fix

Use two separate deepcopy calls in Parameters.__init__:

  1. __full_metadata — pristine copy, never mutated by the instance. Passed to create_subset() for building child Parameters instances.

  2. _working_metadata — separate working copy that self.metadata, self.__dimensions, and all Parameter instances are free to mutate (bounded resolution, dimension size changes, etc.).

Since the caller's dict is never passed into the internal machinery, shallow copies in leaf classes (Parameter.__init__, Dimension.__init__) are unnecessary — though harmless if also applied.

Testing

All 302 tests pass locally (Python 3.13).

Use two separate deep copies in Parameters.__init__:

1. __full_metadata: pristine copy, never mutated by the instance.
   Used by create_subset() to build child Parameters instances
   that need the original unresolved metadata (dimension names
   in bounded 'maximum' fields, original dimension sizes, etc.).

2. _working_metadata: separate working copy that self.metadata,
   self.__dimensions, and all Parameter instances are free to mutate
   (bounded resolution, dimension size changes, etc.).

This supersedes the need for shallow copies in Parameter.__init__
and Dimension.__init__ since the caller's dict is never passed into
the internal machinery. The approach also prevents create_subset()
from crashing due to mutated __full_metadata, which would happen
if __full_metadata and self.metadata shared sub-dicts.

Addresses the same root cause as PR #64 (caller-supplied metadata
being mutated across Parameters instances) but at a higher level,
providing stronger guarantees.
@pnorton-usgs
pnorton-usgs merged commit 59453bf into development Aug 17, 2026
7 checks passed
@pnorton-usgs
pnorton-usgs deleted the fix/deepcopy-metadata-isolation branch August 17, 2026 17:18
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.

1 participant