Skip to content

Commit 012d3f1

Browse files
ref: improve typing of sentry.db.models (#72963)
<!-- Describe your PR here. -->
1 parent 5d711ca commit 012d3f1

File tree

5 files changed

+85
-68
lines changed

5 files changed

+85
-68
lines changed

pyproject.toml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,6 @@ module = [
235235
"sentry.auth.system",
236236
"sentry.auth.view",
237237
"sentry.db.mixin",
238-
"sentry.db.models.paranoia",
239-
"sentry.db.models.utils",
240238
"sentry.db.postgres.base",
241239
"sentry.db.router",
242240
"sentry.digests.notifications",
@@ -519,11 +517,9 @@ module = [
519517
"sentry.api.helpers.source_map_helper",
520518
"sentry.buffer.*",
521519
"sentry.build.*",
522-
"sentry.db.models.manager",
523-
"sentry.db.models.manager.base",
524-
"sentry.db.models.manager.base_query_set",
525-
"sentry.db.models.manager.types",
526-
"sentry.db.models.query",
520+
"sentry.db.models.manager.*",
521+
"sentry.db.models.paranoia",
522+
"sentry.db.models.utils",
527523
"sentry.eventstore.reprocessing.redis",
528524
"sentry.eventtypes.error",
529525
"sentry.grouping.component",

src/sentry/db/exceptions.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,2 @@
11
class QueryError(Exception):
22
pass
3-
4-
5-
class CannotResolveExpression(Exception):
6-
pass

src/sentry/db/models/paranoia.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,21 @@
88
from sentry.db.models.manager.types import M
99

1010

11+
def _bogus_delete_return_value() -> tuple[int, dict[str, int]]:
12+
# django'd delete returns (# deleted, dict[model name, # deleted])
13+
# but we never use this value (and aren't actually deleting!) so...
14+
return (0, {})
15+
16+
1117
class ParanoidQuerySet(BaseQuerySet[M]):
1218
"""
1319
Prevents objects from being hard-deleted. Instead, sets the
1420
``date_deleted``, effectively soft-deleting the object.
1521
"""
1622

17-
def delete(self) -> None:
23+
def delete(self) -> tuple[int, dict[str, int]]:
1824
self.update(date_deleted=timezone.now())
25+
return _bogus_delete_return_value()
1926

2027

2128
class ParanoidManager(BaseManager[M]):
@@ -35,7 +42,10 @@ class Meta:
3542
objects: ClassVar[ParanoidManager[Self]] = ParanoidManager()
3643
with_deleted: ClassVar[BaseManager[Self]] = BaseManager()
3744

38-
def delete(self) -> None:
39-
self.update(date_deleted=timezone.now())
45+
def delete(
46+
self, using: str | None = None, keep_parents: bool = False
47+
) -> tuple[int, dict[str, int]]:
48+
self.update(using=using, date_deleted=timezone.now())
49+
return _bogus_delete_return_value()
4050

4151
__repr__ = sane_repr("id")

src/sentry/db/models/query.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
from __future__ import annotations
22

33
import itertools
4+
import operator
45
from functools import reduce
56
from typing import TYPE_CHECKING, Any, Literal
67

78
from django.db import IntegrityError, router, transaction
8-
from django.db.models import Model, Q
9-
from django.db.models.expressions import CombinedExpression
9+
from django.db.models import F, Model, Q
10+
from django.db.models.expressions import BaseExpression, CombinedExpression, Value
1011
from django.db.models.fields import Field
1112
from django.db.models.signals import post_save
1213

13-
from .utils import resolve_combined_expression
14-
1514
if TYPE_CHECKING:
1615
from sentry.db.models.base import BaseModel
1716

@@ -21,6 +20,47 @@
2120
"update_or_create",
2221
)
2322

23+
COMBINED_EXPRESSION_CALLBACKS = {
24+
CombinedExpression.ADD: operator.add,
25+
CombinedExpression.SUB: operator.sub,
26+
CombinedExpression.MUL: operator.mul,
27+
CombinedExpression.DIV: operator.floordiv,
28+
CombinedExpression.MOD: operator.mod,
29+
CombinedExpression.BITAND: operator.and_,
30+
CombinedExpression.BITOR: operator.or_,
31+
}
32+
33+
34+
class CannotResolveExpression(Exception):
35+
pass
36+
37+
38+
def resolve_combined_expression(instance: Model, node: CombinedExpression) -> BaseExpression:
39+
def _resolve(instance: Model, node: BaseExpression | F) -> BaseExpression:
40+
if isinstance(node, Value):
41+
return node.value
42+
if isinstance(node, F):
43+
return getattr(instance, node.name)
44+
if isinstance(node, CombinedExpression):
45+
return resolve_combined_expression(instance, node)
46+
return node
47+
48+
if isinstance(node, Value):
49+
return node.value
50+
if not isinstance(node, CombinedExpression):
51+
raise CannotResolveExpression
52+
op = COMBINED_EXPRESSION_CALLBACKS.get(node.connector, None)
53+
if not op:
54+
raise CannotResolveExpression
55+
if hasattr(node, "children"):
56+
children = node.children
57+
else:
58+
children = [node.lhs, node.rhs]
59+
runner = _resolve(instance, children[0])
60+
for n in children[1:]:
61+
runner = op(runner, _resolve(instance, n))
62+
return runner
63+
2464

2565
def _get_field(model: type[BaseModel], key: str) -> Field[object, object]:
2666
field = model._meta.get_field(key)

src/sentry/db/models/utils.py

Lines changed: 25 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,19 @@
11
from __future__ import annotations
22

3-
import operator
43
from collections.abc import Container
5-
from typing import Any
4+
from typing import TYPE_CHECKING, Any, Generic, Self, TypeVar, overload
65
from uuid import uuid4
76

8-
from django.db.models import F, Field, Model
9-
from django.db.models.expressions import BaseExpression, CombinedExpression, Value
7+
from django.db.models import Field, Model
108
from django.utils.crypto import get_random_string
119
from django.utils.text import slugify
1210

13-
from sentry.db.exceptions import CannotResolveExpression
14-
15-
COMBINED_EXPRESSION_CALLBACKS = {
16-
CombinedExpression.ADD: operator.add,
17-
CombinedExpression.SUB: operator.sub,
18-
CombinedExpression.MUL: operator.mul,
19-
CombinedExpression.DIV: operator.floordiv,
20-
CombinedExpression.MOD: operator.mod,
21-
CombinedExpression.BITAND: operator.and_,
22-
CombinedExpression.BITOR: operator.or_,
23-
}
24-
25-
26-
def resolve_combined_expression(instance: Model, node: BaseExpression) -> BaseExpression:
27-
def _resolve(instance: Model, node: BaseExpression | F) -> BaseExpression:
28-
if isinstance(node, Value):
29-
return node.value
30-
if isinstance(node, F):
31-
return getattr(instance, node.name)
32-
if isinstance(node, CombinedExpression):
33-
return resolve_combined_expression(instance, node)
34-
return node
35-
36-
if isinstance(node, Value):
37-
return node.value
38-
if not hasattr(node, "connector"):
39-
raise CannotResolveExpression
40-
op = COMBINED_EXPRESSION_CALLBACKS.get(node.connector, None)
41-
if not op:
42-
raise CannotResolveExpression
43-
if hasattr(node, "children"):
44-
children = node.children
45-
else:
46-
children = [node.lhs, node.rhs]
47-
runner = _resolve(instance, children[0])
48-
for n in children[1:]:
49-
runner = op(runner, _resolve(instance, n))
50-
return runner
11+
if TYPE_CHECKING:
12+
from sentry.db.models.base import Model as SentryModel
5113

5214

5315
def unique_db_instance(
54-
inst: Model,
16+
inst: SentryModel,
5517
base_value: str,
5618
reserved: Container[str] = (),
5719
max_length: int = 30,
@@ -62,7 +24,7 @@ def unique_db_instance(
6224
if base_value is not None:
6325
base_value = base_value.strip()
6426
if base_value in reserved:
65-
base_value = None
27+
base_value = ""
6628

6729
if not base_value:
6830
base_value = uuid4().hex[:12]
@@ -105,7 +67,7 @@ def unique_db_instance(
10567

10668

10769
def slugify_instance(
108-
inst: Model,
70+
inst: SentryModel,
10971
label: str,
11072
reserved: Container[str] = (),
11173
max_length: int = 30,
@@ -119,20 +81,33 @@ def slugify_instance(
11981
return unique_db_instance(inst, value, reserved, max_length, field_name, *args, **kwargs)
12082

12183

122-
class Creator:
84+
# matches django-stubs for Field
85+
_ST = TypeVar("_ST", contravariant=True)
86+
_GT = TypeVar("_GT", covariant=True)
87+
88+
89+
class Creator(Generic[_ST, _GT]):
12390
"""
12491
A descriptor that invokes `to_python` when attributes are set.
12592
This provides backwards compatibility for fields that used to use
12693
SubfieldBase which will be removed in Django1.10
12794
"""
12895

129-
def __init__(self, field: Field):
96+
def __init__(self, field: Field[_ST, _GT]) -> None:
13097
self.field = field
13198

132-
def __get__(self, obj: Model, type: Any = None) -> Any:
133-
if obj is None:
99+
@overload
100+
def __get__(self, inst: Model, owner: type[Any]) -> Any:
101+
...
102+
103+
@overload
104+
def __get__(self, inst: None, owner: type[Any]) -> Self:
105+
...
106+
107+
def __get__(self, inst: Model | None, owner: type[Any]) -> Self | Any:
108+
if inst is None:
134109
return self
135-
return obj.__dict__[self.field.name]
110+
return inst.__dict__[self.field.name]
136111

137112
def __set__(self, obj: Model, value: Any) -> None:
138113
obj.__dict__[self.field.name] = self.field.to_python(value)

0 commit comments

Comments
 (0)