Skip to content

Commit abf878c

Browse files
committed
ref(backup): Convert InstanceID to NamedTuple
This is a bit unfortunate, as this requires the `model` to be passed as an (unparsed) `str`, rather than as an already-parsed `NormalizedModelName`, ultimately making the class easier to misuse. However, `InstanceID`s will now need to be sent over RPC, and no other solution I tried for this that retained the type-unwrapping constructor was satisfactory: - I considered using Pydantic's `BaseModel` or `dataclass` base classes, but opted against this, as I would like to avoid Pydantic types leaking out of the RPC-specific modules they mostly live in today. - I tried to convince Pydantic to properly encode/decode the existing native dataclass, but ran across pydantic/pydantic#2531, which won't allow custom encoding/decoding of arbitrary types in Pydantic v1. - I also messed with some other inheritance schemes, but in the end decided converting to a `NamedTuple` and dropping the type unwrapping was probably the best solution for now. Issue: getsentry/team-ospo#196
1 parent be1b748 commit abf878c

File tree

6 files changed

+100
-83
lines changed

6 files changed

+100
-83
lines changed

src/sentry/backup/findings.py

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
from __future__ import annotations
22

3+
from abc import ABC, abstractmethod
34
from copy import deepcopy
45
from dataclasses import dataclass
56
from enum import IntEnum, auto, unique
6-
from typing import Optional
7+
from typing import List, NamedTuple, Optional
78

8-
from sentry.backup.dependencies import NormalizedModelName
99
from sentry.utils import json
1010

1111

12-
@dataclass
13-
class InstanceID:
12+
class InstanceID(NamedTuple):
1413
"""Every entry in the generated backup JSON file should have a unique model+ordinal combination,
1514
which serves as its identifier."""
1615

@@ -19,21 +18,30 @@ class InstanceID:
1918
# The order that this model appeared in the JSON inputs. Because we validate that the same
2019
# number of models of each kind are present on both the left and right side when validating, we
2120
# can use the ordinal as a unique identifier.
22-
ordinal: int | None = None
23-
24-
def __init__(self, model: NormalizedModelName, ordinal: Optional[int] = None):
25-
self.model = str(model)
26-
self.ordinal = ordinal
27-
28-
def __hash__(self):
29-
return hash((self.model, self.ordinal))
21+
ordinal: Optional[int] = None
3022

3123
def pretty(self) -> str:
3224
out = f"InstanceID(model: {self.model!r}"
3325
if self.ordinal:
3426
out += f", ordinal: {self.ordinal}"
3527
return out + ")"
3628

29+
@classmethod
30+
def encode(_, id: InstanceID) -> dict:
31+
"""
32+
Encode to JSON as a dictionary.
33+
"""
34+
35+
return {"model": id.model, "ordinal": id.ordinal}
36+
37+
@classmethod
38+
def decode(cls, data: dict) -> InstanceID:
39+
"""
40+
Decode from a JSON dictionary.
41+
"""
42+
43+
return cls(model=data["model"], ordinal=data["ordinal"])
44+
3745

3846
class FindingKind(IntEnum):
3947
pass
@@ -132,21 +140,41 @@ class ComparatorFindingKind(FindingKind):
132140

133141

134142
@dataclass(frozen=True)
135-
class Finding:
143+
class Finding(ABC):
136144
"""
137-
A JSON serializable and user-reportable finding for an import/export operation.
145+
A JSON serializable and user-reportable finding for an import/export operation. Don't use this
146+
class directly - inherit from it, set a specific `kind` type, and define your own pretty
147+
printer!
138148
"""
139149

140150
on: InstanceID
141151

142152
# The original `pk` of the model in question, if one is specified in the `InstanceID`.
143-
left_pk: int | None = None
153+
left_pk: Optional[int] = None
144154

145155
# The post-import `pk` of the model in question, if one is specified in the `InstanceID`.
146-
right_pk: int | None = None
156+
right_pk: Optional[int] = None
147157

148158
reason: str = ""
149159

160+
def _pretty_inner(self) -> str:
161+
"""
162+
Pretty print only the fields on the shared `Finding` portion.
163+
"""
164+
165+
out = f"\n\ton: {self.on.pretty()}"
166+
if self.left_pk:
167+
out += f",\n\tleft_pk: {self.left_pk}"
168+
if self.right_pk:
169+
out += f",\n\tright_pk: {self.right_pk}"
170+
if self.reason:
171+
out += f",\n\treason: {self.reason}"
172+
return out
173+
174+
@abstractmethod
175+
def pretty(self) -> str:
176+
pass
177+
150178

151179
@dataclass(frozen=True)
152180
class ComparatorFinding(Finding):
@@ -157,20 +185,13 @@ class ComparatorFinding(Finding):
157185
kind: ComparatorFindingKind = ComparatorFindingKind.Unknown
158186

159187
def pretty(self) -> str:
160-
out = f"Finding(\n\tkind: {self.kind.name},\n\ton: {self.on.pretty()}"
161-
if self.left_pk:
162-
out += f",\n\tleft_pk: {self.left_pk}"
163-
if self.right_pk:
164-
out += f",\n\tright_pk: {self.right_pk}"
165-
if self.reason:
166-
out += f",\n\treason: {self.reason}"
167-
return out + "\n)"
188+
return f"ComparatorFinding(\n\tkind: {self.kind.name},{self._pretty_inner()}\n)"
168189

169190

170191
class ComparatorFindings:
171192
"""A wrapper type for a list of 'ComparatorFinding' which enables pretty-printing in asserts."""
172193

173-
def __init__(self, findings: list[ComparatorFinding]):
194+
def __init__(self, findings: List[ComparatorFinding]):
174195
self.findings = findings
175196

176197
def append(self, finding: ComparatorFinding) -> None:
@@ -179,7 +200,7 @@ def append(self, finding: ComparatorFinding) -> None:
179200
def empty(self) -> bool:
180201
return not self.findings
181202

182-
def extend(self, findings: list[ComparatorFinding]) -> None:
203+
def extend(self, findings: List[ComparatorFinding]) -> None:
183204
self.findings += findings
184205

185206
def pretty(self) -> str:
@@ -196,6 +217,4 @@ def default(self, obj):
196217
if isinstance(kind, FindingKind):
197218
d["kind"] = kind.name
198219
return d
199-
if isinstance(obj, InstanceID):
200-
return obj.__dict__
201220
return super().default(obj)

src/sentry/backup/validate.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def assign(self, obj: JSONData, side: Side) -> Tuple[int, list[ComparatorFinding
5252
findings.append(
5353
ComparatorFinding(
5454
kind=ComparatorFindingKind.UnorderedInput,
55-
on=InstanceID(model_name, self.next_ordinal),
55+
on=InstanceID(str(model_name), self.next_ordinal),
5656
left_pk=pk if side == Side.left else None,
5757
right_pk=pk if side == Side.right else None,
5858
reason=f"""instances not listed in ascending `pk` order; `pk` {pk} is less than or equal to {self.max_seen_pk} which precedes it""",
@@ -78,7 +78,7 @@ def build_model_map(
7878
counter = ordinal_counters[model_name]
7979
ordinal, found = counter.assign(model, side)
8080
findings.extend(found)
81-
id = InstanceID(model_name, ordinal)
81+
id = InstanceID(str(model_name), ordinal)
8282
model_map[id] = model
8383
return (model_map, ordinal_counters)
8484

@@ -109,7 +109,7 @@ def json_lines(obj: JSONData) -> list[str]:
109109
findings.append(
110110
ComparatorFinding(
111111
kind=ComparatorFindingKind.UnequalCounts,
112-
on=InstanceID(model_name),
112+
on=InstanceID(str(model_name)),
113113
reason=f"""counted {left_count} left entries and {right_count} right entries""",
114114
)
115115
)

0 commit comments

Comments
 (0)