245 lines
6.1 KiB
Python
245 lines
6.1 KiB
Python
import logging
|
||
import sys
|
||
from dataclasses import dataclass
|
||
from typing import Any, Dict, List, Optional, Tuple, Type
|
||
|
||
import pydantic
|
||
import pytest
|
||
from fastapi import FastAPI
|
||
from packaging import version
|
||
from pydantic import BaseModel, ValidationError
|
||
|
||
import ray
|
||
from ray.tests.pydantic_module import User, app, closure, user
|
||
|
||
BASE_MODELS = [BaseModel]
|
||
BASE_MODEL_AND_ERRORS = [(BaseModel, ValidationError)]
|
||
|
||
|
||
@pytest.fixture(scope="session")
|
||
def start_ray():
|
||
ray.init(ignore_reinit_error=True)
|
||
|
||
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_cls(start_ray, BaseModel: Type):
|
||
class User(BaseModel):
|
||
name: str
|
||
|
||
ray.get(ray.put(User))
|
||
|
||
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_instance(start_ray, BaseModel: Type):
|
||
class User(BaseModel):
|
||
name: str
|
||
|
||
ray.get(ray.put(User(name="a")))
|
||
|
||
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_imported_cls(start_ray, BaseModel: Type):
|
||
ray.get(ray.put(User))
|
||
|
||
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_imported_instance(start_ray, BaseModel: Type):
|
||
ray.get(ray.put(user))
|
||
|
||
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_app_no_route(start_ray, BaseModel: Type):
|
||
app = FastAPI()
|
||
ray.get(ray.put(app))
|
||
|
||
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_app_no_validation(start_ray, BaseModel: Type):
|
||
app = FastAPI()
|
||
|
||
@app.get("/")
|
||
def hello() -> str:
|
||
return "hi"
|
||
|
||
ray.get(ray.put(app))
|
||
|
||
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_app_primitive_type(start_ray, BaseModel: Type):
|
||
app = FastAPI()
|
||
|
||
@app.get("/")
|
||
def hello(v: str) -> str:
|
||
return "hi"
|
||
|
||
ray.get(ray.put(app))
|
||
|
||
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_app_pydantic_type_imported(start_ray, BaseModel: Type):
|
||
app = FastAPI()
|
||
|
||
@app.get("/")
|
||
def hello(v: str, u: User) -> str:
|
||
return "hi"
|
||
|
||
ray.get(ray.put(app))
|
||
|
||
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_app_pydantic_type_inline(start_ray, BaseModel: Type):
|
||
class User(BaseModel):
|
||
name: str
|
||
|
||
app = FastAPI()
|
||
|
||
@app.get("/")
|
||
def hello(v: str, u: User) -> str:
|
||
return "hi"
|
||
|
||
ray.get(ray.put(app))
|
||
|
||
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_app_imported(start_ray, BaseModel: Type):
|
||
ray.get(ray.put(app))
|
||
|
||
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_app_pydantic_type_closure_ref(start_ray, BaseModel: Type):
|
||
class User(BaseModel):
|
||
name: str
|
||
|
||
def make():
|
||
app = FastAPI()
|
||
|
||
@app.get("/")
|
||
def hello(v: str, u: User) -> str:
|
||
return "hi"
|
||
|
||
return app
|
||
|
||
ray.get(ray.put(make))
|
||
|
||
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_app_pydantic_type_closure_ref_import(start_ray, BaseModel: Type):
|
||
def make():
|
||
app = FastAPI()
|
||
|
||
@app.get("/")
|
||
def hello(v: str, u: User) -> str:
|
||
return "hi"
|
||
|
||
return app
|
||
|
||
ray.get(ray.put(make))
|
||
|
||
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_app_pydantic_type_closure(start_ray, BaseModel: Type):
|
||
def make():
|
||
class User(BaseModel):
|
||
name: str
|
||
|
||
app = FastAPI()
|
||
|
||
@app.get("/")
|
||
def hello(v: str, u: User) -> str:
|
||
return "hi"
|
||
|
||
return app
|
||
|
||
ray.get(ray.put(make))
|
||
|
||
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_app_imported_closure(start_ray, BaseModel: Type):
|
||
ray.get(ray.put(closure))
|
||
|
||
|
||
# TODO: Serializing a Serve dataclass doesn't work in Pydantic 1.10 – 2.0.
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_serve_dataclass(start_ray, BaseModel: Type):
|
||
@dataclass
|
||
class BackendMetadata:
|
||
is_blocking: bool = True
|
||
autoscaling_config: Optional[Dict[str, Any]] = None
|
||
|
||
class BackendConfig(BaseModel):
|
||
internal_metadata: BackendMetadata = BackendMetadata()
|
||
|
||
ray.get(ray.put(BackendConfig()))
|
||
|
||
@ray.remote
|
||
def consume(f):
|
||
pass
|
||
|
||
ray.get(consume.remote(BackendConfig()))
|
||
|
||
|
||
@pytest.mark.parametrize("BaseModel", BASE_MODELS)
|
||
def test_serialize_nested_field(start_ray, BaseModel: Type):
|
||
class B(BaseModel):
|
||
v: List[int]
|
||
|
||
# this shouldn't error
|
||
B(v=[1])
|
||
|
||
@ray.remote
|
||
def func():
|
||
# this shouldn't error
|
||
return B(v=[1])
|
||
|
||
ray.get(func.remote())
|
||
|
||
|
||
@pytest.mark.skipif(
|
||
version.parse(pydantic.__version__) < version.parse("2.6.0"),
|
||
reason="pydantic version < 2.6.0 has a bug that ValidationError "
|
||
"is not picklable: See "
|
||
"https://github.com/pydantic/pydantic-core/pull/1119",
|
||
)
|
||
@pytest.mark.parametrize("base_model_and_error", BASE_MODEL_AND_ERRORS)
|
||
def test_validation_error(
|
||
start_ray, propagate_logs, caplog, base_model_and_error: Tuple[Type, Type]
|
||
):
|
||
BaseModel, ValidationError = base_model_and_error
|
||
|
||
class B(BaseModel):
|
||
s: str
|
||
|
||
# This should error.
|
||
with pytest.raises(ValidationError):
|
||
B(s=None)
|
||
|
||
@ray.remote
|
||
def func():
|
||
# This should also error. The problem is that Pydantic v2 ValidationError is
|
||
# marked @final so we can't subclass it. This means Ray can't raise an exception
|
||
# that can be caught as both `RayTaskError` and `pydantic.ValidationError`. So
|
||
# we issue a warning and just raise it as `RayTaskError`. The user needs to use
|
||
# `e.cause` to get the ValidationError.
|
||
class B(BaseModel):
|
||
s: str
|
||
|
||
return B(v=None)
|
||
|
||
with caplog.at_level(logging.WARNING, logger="ray.exceptions"):
|
||
with pytest.raises(ray.exceptions.RayTaskError) as exc_info:
|
||
ray.get(func.remote())
|
||
|
||
# Pydantic v2 validation errors are final, can't be subclassed.
|
||
assert (
|
||
"This exception is raised as RayTaskError only. You can use "
|
||
"`ray_task_error.cause` to access the user exception."
|
||
) in caplog.text
|
||
assert isinstance(exc_info.value, ray.exceptions.RayTaskError)
|
||
assert isinstance(exc_info.value.cause, ValidationError)
|
||
|
||
caplog.clear()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(pytest.main(["-sv", __file__]))
|