Make registry entries deepcopy-able, so console metric dumps work

Attribute, SpanName and MetricName are str subclasses whose __new__
requires the metadata arguments, so copy.deepcopy could not reconstruct
one - it falls back to cls.__new__(cls) and raises TypeError.

That broke a real path rather than a theoretical one. The SDK's
ConsoleMetricExporter renders data points through dataclasses.asdict(),
which deepcopies mappings, and both core and kit-based plugins pass
registry entries as metric attribute keys - so every console metrics
dump crashed, core's own points included. Found by datasette-paper's
dev harness running opentelemetry-instrument with console exporters.

__reduce__ collapses copies to a plain str, which is what an entry is
everywhere except the registry module itself: the description, values
and buckets describe the single registered instance, and nothing reads
them off a copy. Pickle is fixed by the same change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwU7BcTnAxUGSJYhBrQaY7
This commit is contained in:
Alex Garcia 2026-09-08 14:46:13 -07:00
commit bbc85b7e4f
2 changed files with 72 additions and 1 deletions

View file

@ -50,6 +50,20 @@ class Attribute(str):
self.values = frozenset(values) if values is not None else None
return self
def __reduce__(self):
# Copies and pickles collapse to a plain str. Without this, `copy` has
# to reconstruct a str subclass through `cls.__new__(cls)`, which these
# classes reject - their `__new__` requires the metadata arguments. It
# is not a theoretical problem: the SDK's ConsoleMetricExporter renders
# data points with `dataclasses.asdict()`, which deepcopies mappings,
# and registry entries are used as metric attribute keys - so every
# console metrics dump would crash. Collapsing is also the honest
# answer, not a workaround. On the wire and in a copy an entry *is*
# its string; the description, values and buckets describe the single
# registered instance in this module, and nothing reads them off a
# copy.
return (str, (str(self),))
def __repr__(self):
return f"Attribute({str(self)!r})"
@ -95,6 +109,10 @@ class SpanName(str):
self.kind = kind
return self
def __reduce__(self):
# See Attribute.__reduce__.
return (str, (str(self),))
def __repr__(self):
return f"SpanName({str(self)!r})"
@ -117,6 +135,10 @@ class MetricName(str):
self.buckets = tuple(buckets) if buckets is not None else None
return self
def __reduce__(self):
# See Attribute.__reduce__.
return (str, (str(self),))
def __repr__(self):
return f"MetricName({str(self)!r})"