From 465f75afec59b77ddf36903eefe1e059b4961a17 Mon Sep 17 00:00:00 2001 From: jakeross Date: Fri, 26 Jun 2026 14:54:57 -0600 Subject: [PATCH] Make Record pickle-safe (fix RecursionError via IO manager) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record.__getattr__ read self._payload, so during unpickling — when pickle probes getattr(obj, "__setstate__") before __dict__ is restored — it recursed on the missing _payload until RecursionError. This broke the GCS pickle IO manager whenever a Record (or a payload nesting one) crossed between assets. Guard __getattr__: dunder lookups raise AttributeError (so pickle falls back to default state restore) and _payload is read via __dict__ to avoid re-entry. Verified bare and nested Records round-trip through pickle; 263 tests pass. Co-Authored-By: Claude Opus 4.8 --- backend/record.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/backend/record.py b/backend/record.py index b9a913f..43b9424 100644 --- a/backend/record.py +++ b/backend/record.py @@ -79,7 +79,17 @@ def _get_sigfig_formatted_value(self, attr): return v def __getattr__(self, attr): - v = self._payload.get(attr) + # Guard against recursion when the instance has no _payload yet — e.g. + # during unpickling, when pickle probes getattr(obj, "__setstate__") + # before __dict__ is restored. Reading via __dict__ (not self._payload) + # avoids re-entering __getattr__, and dunder lookups raise cleanly so + # pickle falls back to its default state restore. + if attr.startswith("__") and attr.endswith("__"): + raise AttributeError(attr) + payload = self.__dict__.get("_payload") + if payload is None: + raise AttributeError(attr) + v = payload.get(attr) if v is None and self.defaults: v = self.defaults.get(attr) return v