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

Pydantic v2 support #304

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def read(filename):

extras_require = {
"marshmallow": ["marshmallow>=2.15.0"],
"pydantic:python_version >= '3.6'": ["pydantic>=1.6.1"],
"pydantic:python_version >= '3.6'": ["pydantic>=2.0.0"],
"aiohttp:python_version <= '3.4'": [],
"aiohttp:python_version >= '3.4'": "aiohttp>=2.3.0",
"twisted:python_version != '3.3' and python_version != '3.4'": "twisted>=17.1.0",
Expand Down
22 changes: 16 additions & 6 deletions tests/integration/test_retry_aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,27 @@
from uplink.clients import io

from . import test_retry
import sys


@pytest.mark.asyncio
async def test_retry_with_asyncio(mock_client, mock_response):
import asyncio
# Check the Python version and define coroutine accordingly
if sys.version_info >= (3, 5):
# For Python 3.5 and newer
Copy link
Owner

Choose a reason for hiding this comment

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

non-blocking: I would expect this to throw a SyntaxError for Python 2.7. But, we are way beyond EOL now, and should drop 2.7 support IMO.

async def coroutine():
return mock_response
elif (3, 3) <= sys.version_info < (3, 5):
Copy link
Owner

Choose a reason for hiding this comment

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

Might be safe to drop this. I don't believe we officially support Python versions below 3.5

# For Python 3.3 and 3.4
import asyncio
@asyncio.coroutine
def coroutine():
yield
return mock_response
else:
# Not applicable for Python 2
return

@asyncio.coroutine
def coroutine():
return mock_response

# Setup
mock_response.with_json({"id": 123, "name": "prkumar"})
mock_client.with_side_effect([Exception, coroutine()])
mock_client.with_io(io.AsyncioStrategy())
Expand Down
89 changes: 36 additions & 53 deletions tests/unit/test_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def test_convert(self):

class TestStandardConverter(object):
def test_create_response_body_converter_with_converter(
self, converter_mock
self, converter_mock
):
# Setup
factory = standard.StandardConverter()
Expand Down Expand Up @@ -83,7 +83,7 @@ class TestConverterFactoryRegistry(object):
backend = converters.ConverterFactoryRegistry._converter_factory_registry

def test_init_args_are_passed_to_factory(
self, converter_factory_mock, converter_mock
self, converter_factory_mock, converter_mock
):
args = ("arg1", "arg2")
kwargs = {"arg3": "arg3"}
Expand Down Expand Up @@ -178,7 +178,7 @@ def test_init_without_marshmallow(self):

@for_marshmallow_2_and_3
def test_create_request_body_converter(
self, mocker, schema_mock_and_argument, is_marshmallow_3
self, mocker, schema_mock_and_argument, is_marshmallow_3
):
# Setup
schema_mock, argument = schema_mock_and_argument
Expand Down Expand Up @@ -210,7 +210,7 @@ def test_create_request_body_converter_without_schema(self):

@for_marshmallow_2_and_3
def test_create_response_body_converter(
self, mocker, schema_mock_and_argument, is_marshmallow_3
self, mocker, schema_mock_and_argument, is_marshmallow_3
):
# Setup
schema_mock, argument = schema_mock_and_argument
Expand Down Expand Up @@ -242,7 +242,7 @@ def test_create_response_body_converter(
c.convert(data)

def test_create_response_body_converter_with_unsupported_response(
self, schema_mock_and_argument
self, schema_mock_and_argument
):
# Setup
schema_mock, argument = schema_mock_and_argument
Expand Down Expand Up @@ -474,15 +474,6 @@ def test_dict_converter(self):
sys.version_info < (3, 6), reason="requires python3.6 or higher"
)
class TestPydanticConverter(object):
@pytest.fixture
def pydantic_model_mock(self, mocker):
class Model(pydantic.BaseModel):
def __new__(cls, *args, **kwargs):
return model

model = mocker.Mock(spec=Model)
return model, Model

def test_init_without_pydantic(self, mocker):
mocker.patch.object(
converters.PydanticConverter,
Expand All @@ -494,30 +485,29 @@ def test_init_without_pydantic(self, mocker):
with pytest.raises(ImportError):
converters.PydanticConverter()

def test_create_request_body_converter(self, pydantic_model_mock):
def test_create_request_body_converter(self):
class Model(pydantic.BaseModel):
id: int = 0

expected_result = {"id": 0}
request_body = {}

model_mock, model = pydantic_model_mock
model_mock.dict.return_value = expected_result
request_body = {}

converter = converters.PydanticConverter()
request_converter = converter.create_request_body_converter(model)
request_converter = converter.create_request_body_converter(Model)

result = request_converter.convert(request_body)

assert result == expected_result
model_mock.dict.assert_called_once()
model_mock.dict.assert_called_once()

def test_convert_complex_model(self):
from json import loads
from datetime import datetime

class ComplexModel(pydantic.BaseModel):
when = datetime.utcnow() # type: datetime
where = "http://example.com" # type: pydantic.AnyUrl
some = [1] # type: typing.List[int]
when: datetime = datetime.utcnow()
where: pydantic.AnyUrl = "http://example.com"
some: list[int] = [1]

model = ComplexModel()
request_body = {}
Expand All @@ -532,23 +522,20 @@ class ComplexModel(pydantic.BaseModel):

assert result == expected_result

def test_create_request_body_converter_with_original_model(
self, pydantic_model_mock
):
def test_create_request_body_converter_with_original_model(self):
expected_result = {"id": 0}

model_mock, model = pydantic_model_mock
model_mock.dict.return_value = expected_result
class Model(pydantic.BaseModel):
id: int = 0

request_body = model()
request_body = Model()

converter = converters.PydanticConverter()
request_converter = converter.create_request_body_converter(model)
request_converter = converter.create_request_body_converter(Model)

result = request_converter.convert(request_body)

assert result == expected_result
model_mock.dict.assert_called_once()

def test_create_request_body_converter_without_schema(self, mocker):
expected_result = None
Expand All @@ -558,44 +545,37 @@ def test_create_request_body_converter_without_schema(self, mocker):

assert result is expected_result

def test_create_response_body_converter(self, mocker, pydantic_model_mock):
expected_result = "data"
model_mock, model = pydantic_model_mock
def test_create_response_body_converter(self, mocker):
class Model(pydantic.BaseModel):
id: int

parse_obj_mock = mocker.patch.object(
model, "parse_obj", return_value=expected_result
)
expected_result = Model(id=1)

response = mocker.Mock(spec=["json"])
response.json.return_value = {}
response.json.return_value = {"id": 1}

converter = converters.PydanticConverter()
c = converter.create_response_body_converter(model)
c = converter.create_response_body_converter(Model)

result = c.convert(response)

response.json.assert_called_once()
parse_obj_mock.assert_called_once_with(response.json())
assert result == expected_result

def test_create_response_body_converter_invalid_response(
self, mocker, pydantic_model_mock
self, mocker
):
data = {"quick": "fox"}
_, model = pydantic_model_mock
data = {"id": "qwe"} # Not int

parse_obj_mock = mocker.patch.object(
model, "parse_obj", side_effect=pydantic.ValidationError([], model)
)
class Model(pydantic.BaseModel):
id: int

converter = converters.PydanticConverter()
c = converter.create_response_body_converter(model)
c = converter.create_response_body_converter(Model)

with pytest.raises(pydantic.ValidationError):
c.convert(data)

parse_obj_mock.assert_called_once_with(data)

def test_create_response_body_converter_without_schema(self):
expected_result = None
converter = converters.PydanticConverter()
Expand All @@ -604,12 +584,15 @@ def test_create_response_body_converter_without_schema(self):

assert result is expected_result

def test_create_string_converter(self, pydantic_model_mock):
def test_create_string_converter(self):
expected_result = None
_, model = pydantic_model_mock

class Model(pydantic.BaseModel):
id: int

converter = converters.PydanticConverter()

c = converter.create_string_converter(model, None)
c = converter.create_string_converter(Model, None)

assert c is expected_result

Expand Down
4 changes: 2 additions & 2 deletions uplink/converters/pydantic_.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


def _encode_pydantic(obj):
from pydantic.json import pydantic_encoder
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that here is better to auto-detect the version of pydantic that is installed and import the correct function so we keep backward compatibility.

Copy link
Author

Choose a reason for hiding this comment

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

Hm, good point. Pydantic claims they will stop supporting V1 when V3 is released (somewhere this year), so I am not sure if V1 support gonna be needed.
What I missed is explicitly requiring a pydantic>2 for uplink - that's fixed now

from pydantic_core import to_jsonable_python

# json atoms
if isinstance(obj, (str, int, float, bool)) or obj is None:
Expand All @@ -22,7 +22,7 @@ def _encode_pydantic(obj):
return [_encode_pydantic(i) for i in obj]

# pydantic types
return _encode_pydantic(pydantic_encoder(obj))
return _encode_pydantic(to_jsonable_python(obj))


class _PydanticRequestBody(Converter):
Expand Down