Skip to content

Commit bd28a4e

Browse files
derek73claude
andcommitted
fix: restore CONSTANTS singleton identity across pickle/deepcopy (closes #169)
A default HumanName shares the module-level CONSTANTS singleton as its .C. pickle and copy.deepcopy cannot preserve object identity, so without custom hooks .C was serialized by value — causing has_own_config to flip to True and bloating every serialized default name with a full Constants copy. __getstate__ replaces .C with None as a sentinel when it is the shared singleton; __setstate__ restores the CONSTANTS reference from that sentinel. None is a safe sentinel because the constructor always replaces a None constants argument with a fresh Constants(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9939b1f commit bd28a4e

2 files changed

Lines changed: 37 additions & 0 deletions

File tree

nameparser/parser.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,17 @@ def __init__(
121121
# full_name setter triggers the parse
122122
self.full_name = full_name
123123

124+
def __getstate__(self) -> dict:
125+
state = self.__dict__.copy()
126+
if state.get('C') is CONSTANTS:
127+
state['C'] = None # sentinel: restore shared singleton on load
128+
return state
129+
130+
def __setstate__(self, state: dict) -> None:
131+
if state.get('C') is None:
132+
state['C'] = CONSTANTS
133+
self.__dict__.update(state)
134+
124135
def __iter__(self) -> Iterator[str]:
125136
return self
126137

tests/test_python_api.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,32 @@ def test_name_instance_deepcopy_isolates_instance_config(self) -> None:
9292
self.assertIn('chancellor', dup.C.titles)
9393
self.assertNotIn('marker', hn.C.titles)
9494

95+
def test_pickle_default_name_preserves_singleton_identity(self) -> None:
96+
"""A default HumanName must re-attach to CONSTANTS after a pickle round-trip.
97+
98+
Without __getstate__/__setstate__, pickle serializes .C by value, so the
99+
restored name gets a detached copy — has_own_config flips to True and
100+
every pickled default name carries a full Constants copy.
101+
"""
102+
hn = HumanName("John Doe")
103+
self.assertFalse(hn.has_own_config)
104+
self.assertIs(hn.C, CONSTANTS)
105+
106+
# Safe: round-tripping an object we just built, not untrusted data.
107+
restored = pickle.loads(pickle.dumps(hn))
108+
109+
self.assertIs(restored.C, CONSTANTS)
110+
self.assertFalse(restored.has_own_config)
111+
112+
def test_deepcopy_default_name_preserves_singleton_identity(self) -> None:
113+
"""copy.deepcopy of a default HumanName must re-attach to CONSTANTS."""
114+
hn = HumanName("John Doe")
115+
116+
dup = copy.deepcopy(hn)
117+
118+
self.assertIs(dup.C, CONSTANTS)
119+
self.assertFalse(dup.has_own_config)
120+
95121
def test_comparison(self) -> None:
96122
hn1 = HumanName("Doe-Ray, Dr. John P., CLU, CFP, LUTC")
97123
hn2 = HumanName("Dr. John P. Doe-Ray, CLU, CFP, LUTC")

0 commit comments

Comments
 (0)