Skip to content

Commit

Permalink
chore(hybrid-cloud): Upgrade Pydantic to version 2.7 (#74770)
Browse files Browse the repository at this point in the history
  • Loading branch information
GabeVillalobos authored and roaga committed Jul 31, 2024
1 parent 0845b2d commit 6112dda
Show file tree
Hide file tree
Showing 12 changed files with 45 additions and 22 deletions.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ filterwarnings = [

# pytest has not yet implemented the replacement for this yet
"ignore:The --looponfail command line argument.*",

# Temporarily disable deprecation warnings for pydantic while we upgrade it
"ignore::DeprecationWarning:pydantic.*",
]
looponfailroots = ["src", "tests"]

Expand Down
2 changes: 1 addition & 1 deletion requirements-base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ python-rapidjson>=1.4
psutil>=5.9.2
psycopg2-binary>=2.9.9
PyJWT>=2.4.0
pydantic>=1.10.17,<2
pydantic>=2.5.0
python-dateutil>=2.9.0
pymemcache
python-u2flib-server>=5.0.0
Expand Down
4 changes: 3 additions & 1 deletion requirements-dev-frozen.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
--index-url https://pypi.devinfra.sentry.io/simple

amqp==5.2.0
annotated-types==0.7.0
anyio==3.7.1
asgiref==3.7.2
attrs==23.1.0
Expand Down Expand Up @@ -137,7 +138,8 @@ pyasn1-modules==0.2.4
pycodestyle==2.11.0
pycountry==17.5.14
pycparser==2.21
pydantic==1.10.17
pydantic==2.7.4
pydantic-core==2.18.4
pyflakes==3.2.0
pyjwt==2.4.0
pymemcache==4.0.0
Expand Down
4 changes: 3 additions & 1 deletion requirements-frozen.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
--index-url https://pypi.devinfra.sentry.io/simple

amqp==5.2.0
annotated-types==0.7.0
anyio==3.7.1
asgiref==3.7.2
attrs==23.1.0
Expand Down Expand Up @@ -96,7 +97,8 @@ pyasn1==0.4.5
pyasn1-modules==0.2.4
pycountry==17.5.14
pycparser==2.21
pydantic==1.10.17
pydantic==2.7.4
pydantic-core==2.18.4
pyjwt==2.4.0
pymemcache==4.0.0
pyparsing==3.0.9
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/autofix/utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import datetime
import enum
from typing import TypedDict

import orjson
import requests
from django.conf import settings
from pydantic import BaseModel
from typing_extensions import TypedDict

from sentry.integrations.utils.code_mapping import get_sorted_code_mapping_configs
from sentry.models.project import Project
Expand Down
10 changes: 6 additions & 4 deletions src/sentry/hybridcloud/rpc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import pydantic
from django.db import router, transaction
from django.db.models import Model
from pydantic import ConfigDict

from sentry.silo.base import SiloMode
from sentry.utils.env import in_test_environment
Expand Down Expand Up @@ -43,13 +44,14 @@ def __hash__(self) -> int:
class RpcModel(pydantic.BaseModel):
"""A serializable object that may be part of an RPC schema."""

class Config:
orm_mode = True
use_enum_values = True
# TODO(Hybrid-Cloud): Remove number coercion after pydantic V2 stabilized
model_config = ConfigDict(
from_attributes=True, use_enum_values=True, coerce_numbers_to_str=True
)

@classmethod
def get_field_names(cls) -> Iterable[str]:
return iter(cls.__fields__.keys())
return iter(cls.model_fields.keys())

@classmethod
def serialize_by_field_name(
Expand Down
6 changes: 5 additions & 1 deletion src/sentry/hybridcloud/rpc/sig.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import pydantic
from django.utils.functional import LazyObject
from pydantic import ConfigDict

from sentry.hybridcloud.rpc import ArgumentDict

Expand Down Expand Up @@ -81,7 +82,10 @@ def create_field(param: inspect.Parameter) -> tuple[Any, Any]:
if self.is_instance_method:
parameters = parameters[1:] # exclude `self` argument
field_definitions = {p.name: create_field(p) for p in parameters}
return pydantic.create_model(model_name, **field_definitions) # type: ignore[call-overload]

# TODO(Hybrid-Cloud): Remove number coercion after pydantic V2 stabilized
config = ConfigDict(coerce_numbers_to_str=True)
return pydantic.create_model(model_name, __config__=config, **field_definitions) # type: ignore[call-overload]

_RETURN_MODEL_ATTR = "value"

Expand Down
5 changes: 3 additions & 2 deletions src/sentry/types/region.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import sentry_sdk
from django.conf import settings
from django.http import HttpRequest
from pydantic import TypeAdapter
from pydantic.dataclasses import dataclass
from pydantic.tools import parse_obj_as

from sentry import options
from sentry.silo.base import SiloMode, SingleProcessSiloModeState, control_silo_function
Expand Down Expand Up @@ -151,7 +151,8 @@ def validate_all(self) -> None:
def _parse_raw_config(region_config: Any) -> Iterable[Region]:
if isinstance(region_config, (str, bytes)):
json_config_values = json.loads(region_config)
config_values = parse_obj_as(list[Region], json_config_values)
adapter = TypeAdapter(list[Region])
config_values = adapter.validate_python(json_config_values)
else:
config_values = region_config

Expand Down
2 changes: 1 addition & 1 deletion src/sentry/users/services/user/serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def serialize_generic_user(user: Any) -> RpcUser | None:
def _serialize_from_user_fields(user: User) -> dict[str, Any]:
args = {
field_name: getattr(user, field_name)
for field_name in RpcUserProfile.__fields__
for field_name in RpcUserProfile.model_fields
if hasattr(user, field_name)
}
args["pk"] = user.pk
Expand Down
9 changes: 8 additions & 1 deletion tests/sentry/api/endpoints/test_organization_sdk_updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest
from django.urls import reverse
from pydantic import PydanticDeprecatedSince20

from sentry.sdk_updates import SdkIndexState
from sentry.testutils.cases import APITestCase, SnubaTestCase
Expand Down Expand Up @@ -188,8 +189,14 @@ def test_unknown_version(self, mock_index_state):
update_suggestions = response.data
assert len(update_suggestions) == 0

# TODO(Gabe): Temporary kludge to allow this to pass while pydantic
# deprecation warnings are active.
filtered_warnings = [
info for info in warninfo if not isinstance(info.message, PydanticDeprecatedSince20)
]

# until it is turned into an error, we'll get a warning about parsing an invalid version
(warning,) = warninfo
(warning,) = filtered_warnings
assert isinstance(warning.message, DeprecationWarning)
(warn_msg,) = warning.message.args
assert (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import unittest
from unittest.mock import patch

import responses
Expand Down Expand Up @@ -111,7 +112,16 @@ def test_delete_install(self, record, run):
assert AuditLogEntry.objects.filter(
event=audit_log.get_event_id("SENTRY_APP_UNINSTALL")
).exists()
run.assert_called_once_with(install=self.orm_installation2, user=rpc_user, action="deleted")

# user is wrapped in a SimpleLazyObject, meaning we can't assert equality
# via the method parameter check. Manually retrieve and check it instead.
run.assert_called_once_with(
install=self.orm_installation2, user=unittest.mock.ANY, action="deleted"
)

mock_call_user = run.mock_calls[0].kwargs.get("user")
assert mock_call_user == rpc_user

record.assert_called_with(
"sentry_app.uninstalled",
user_id=self.user.id,
Expand Down
8 changes: 0 additions & 8 deletions tests/sentry/test_dependencies.py

This file was deleted.

0 comments on commit 6112dda

Please sign in to comment.