Skip to content

fix: preserve Omitted singleton across copy, deepcopy and pickle - #17

Merged
goduni merged 2 commits into
goduni:masterfrom
abhinyaay:fix/omitted-copy-pickle-singleton
Sep 15, 2026
Merged

goduni merged 2 commits into
goduni:masterfrom
abhinyaay:fix/omitted-copy-pickle-singleton

Conversation

@abhinyaay

Copy link
Copy Markdown
Contributor

Description

Omitted is a singleton enforced by SingletonMeta.__call__, but copy.copy, copy.deepcopy and pickle build new instances through cls.__new__(cls), bypassing the metaclass. Since Omitted defined no __reduce__ or copy hooks, a copied or unpickled sentinel compared unequal to the original:

import copy, pickle
from unihttp.omitted import Omitted

o = Omitted()
copy.copy(o) is o                    # was False
copy.deepcopy(o) == o                # was False
pickle.loads(pickle.dumps(o)) == o   # was False

This silently broke equality for any object with an Omitted-valued field once it round-tripped through deepcopy/pickle (Celery, multiprocessing, caches, dataclasses.asdict, ...), even though the objects looked identical via repr.

The fix keeps the singleton invariant across all three paths so Omitted always resolves back to the one instance:

  • __copy__ / __deepcopy__ return self
  • __reduce__ returns (type(self), ()), so unpickling routes back through SingletonMeta

With the singleton preserved, identity-based equality holds automatically, so no __eq__/__hash__ change is needed. The serializers keep working unchanged since they rely on isinstance.

Reported downstream in K1rL3s/maxo#241.

Testing

uv run pytest --cov src on Python 3.13: 414 passed, 4 skipped, 100% coverage. just lint (ruff check + format) and just static-analysis (mypy strict) both clean.

Type of change

  • Bug fix (a non-breaking change that resolves an issue)

Checklist

  • My code adheres to the style guidelines of this project (just lint shows no errors)
  • I have conducted a self-review of my own code
  • My changes do not generate any new warnings
  • I have added tests to validate the effectiveness of my fix or the functionality of my new feature
  • I have ensured that static analysis tests are passing by running just static-analysis

SingletonMeta enforces the singleton via __call__, but copy, deepcopy and pickle recreate the instance through __new__, bypassing the metaclass. This broke equality for any object with an Omitted-valued field after a copy or pickle round-trip. Add __copy__, __deepcopy__ and __reduce__ so Omitted always resolves back to the singleton.
Regression tests for the singleton fix: copy/deepcopy/pickle identity and equality, dataclass equality round-trips, and dataclasses.asdict().
@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (a45033d) to head (fa59258).
⚠️ Report is 8 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff            @@
##            master       #17   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           33        33           
  Lines         1328      1334    +6     
=========================================
+ Hits          1328      1334    +6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@goduni
goduni merged commit 849c934 into goduni:master Sep 15, 2026
7 checks passed
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.

2 participants