Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(hybrid-cloud): Upgrade Pydantic to version 2.7 #74770

Merged
merged 14 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pydantic>=2.5.0
pydantic>=2.5.0,<3

Could we pin to the 2.x range so that we don't accidentally bump a major?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's no risk to uncapping here -- in fact it's encouraged

this file isn't installed from -- it's used to generate the pinned file

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
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.

Loading