Skip to content
Merged
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
8 changes: 8 additions & 0 deletions jsonrpclib/jsonclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,14 @@ def dump(
elif utils.is_enum(obj):
# Add parameters for enumerations
return_obj["__jsonclass__"].append([obj.value])
elif utils.is_pydantic(obj):
# Found a Pydantic class, drop to JSON
if hasattr(obj, "model_dump"):
# v2 Pydantic
return_obj["__jsonclass__"].append(obj.model_dump())
else:
# v1 Pydantic
return_obj["__jsonclass__"].append(obj.dict())
else:
# Otherwise, try to figure it out
# Obviously, we can't assume to know anything about the
Expand Down
15 changes: 15 additions & 0 deletions jsonrpclib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ def from_bytes(data):
return data
return str(data)

def is_pydantic(obj):
"""
Checks if the given objet is Pydantic model instance
"""
return False


else:
# Python 3
Expand All @@ -92,6 +98,15 @@ def from_bytes(data):
return data
return str(data, "UTF-8")

def is_pydantic(obj):
"""
Checks if the given objet is Pydantic model instance
"""
return any(
base.__name__ == "BaseModel"
and base.__module__.startswith("pydantic")
for base in obj.__class__.__mro__
)

# ------------------------------------------------------------------------------
# Enumerations
Expand Down
34 changes: 33 additions & 1 deletion run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,45 @@ run_lib_tests() {
return $rc
}

python_supports_pydantic() {
if [ -z "$UV" ]
then
# uv requires Python 3.8+, supported by Pydantic
return 1
fi

python -c 'import sys; exit(sys.version_info[:2] >= (3, 7)' >/dev/null 2>&1
if [ $? -eq 0 ]
then
return 1
fi

python3 -c 'import sys; exit(sys.version_info[:2] >= (3, 7)' >/dev/null 2>&1
if [ $? -eq 0 ]
then
return 1
else
return 0
fi
}

echo "Installing dependencies..."
run_pip_install pytest coverage || exit 1
export COVERAGE_PROCESS_START=".coveragerc"

if python_supports_pydantic
then
echo "Try installing pydantic..."
run_pip_install pydantic
EXTRA_ARGS=()
else
echo "Ignoring Pydantic tests"
EXTRA_ARGS=("--ignore" "tests/test_pydantic.py")
fi

echo "Initial tests..."
export JSONRPCLIB_TEST_EXPECTED_LIB=json
run_coverage run -m pytest || exit 1
run_coverage run -m pytest "${EXTRA_ARGS[@]}" || exit 1

echo "orJson tests..."
run_lib_tests orjson orjson || exit 1
Expand Down
129 changes: 129 additions & 0 deletions tests/test_pydantic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/python
# -- Content-Encoding: UTF-8 --
"""
Tests the handling of Pydantic BaseModel arguments

:license: Apache License 2.0
"""

# Standard library
import socket
import threading
import unittest

from jsonrpclib.jsonrpc import ProtocolError

try:
# Pydantic
from pydantic import BaseModel, Field
except ImportError:
raise unittest.SkipTest("Pydantic not found.")

# JSON-RPC library
from jsonrpclib import ServerProxy
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer

# ------------------------------------------------------------------------------

class Argument(BaseModel):
name: str
value: int


class Result(BaseModel):
response: str
universal_answer: bool = False
value: int = Field(description="Query", gt=0, lt=50)
argument: Argument


def handler(arg):
# type: (Argument) -> Result
"""
Handler test function
"""
return Result(
response="{} answered {}".format(arg.name, arg.value),
value=arg.value,
universal_answer=arg.value == 42,
argument=arg
)


# ------------------------------------------------------------------------------

HOST = socket.gethostbyname("localhost")


class PydanticTests(unittest.TestCase):
"""
These tests ensures that the server and client work with Pydantic models
"""

def test_all_pydantic(self):
""" Test with valid data """
# Prepare the server
srv = SimpleJSONRPCServer((HOST, 0))
srv.register_function(handler, "test")

thread = threading.Thread(target=srv.serve_forever)
thread.daemon = True
thread.start()

try:
# Find its port
port = srv.socket.getsockname()[1]

# Make the client
target_url = "http://{0}:{1}".format(HOST, port)
client = ServerProxy(target_url)

arg = Argument(name="foo", value=42)
result = client.test(arg)
self.assertIsInstance(result, Result)
self.assertEqual(arg.value, result.value)
self.assertTrue(result.universal_answer)

arg = Argument(name="foo", value=20)
result = client.test(arg)
self.assertIsInstance(result, Result)
self.assertEqual(arg.value, result.value)
self.assertFalse(result.universal_answer)
self.assertEqual(arg, result.argument)
finally:
srv.shutdown()
srv.server_close()
thread.join(5)

def test_invalid(self):
"""
Test Pydantic when the client uses an invalid value
"""
# Prepare the server
srv = SimpleJSONRPCServer((HOST, 0))
srv.register_function(handler, "test")

thread = threading.Thread(target=srv.serve_forever)
thread.daemon = True
thread.start()

try:
# Find its port
port = srv.socket.getsockname()[1]

# Make the client
target_url = "http://{0}:{1}".format(HOST, port)
client = ServerProxy(target_url)

arg = Argument(name="foo", value=100)
try:
client.test(arg)
except ProtocolError as e:
# Something when wrong on the other side, as expected
self.assertEqual(-32603, e.args[0][0])
else:
self.fail("No error raised")
finally:
srv.shutdown()
srv.server_close()
thread.join(5)