You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

208 lines
7.1 KiB

# path: tests/test_pydantic_v2_first_compat.py
"""
Tests for v2-first compatibility layer with lazy v1 loading.
This test suite validates that:
1. Python 3.14 + Pydantic v2 runs without warnings
2. Python 3.14 + Pydantic v1 shows controlled warnings/errors
3. isinstance() checks work with proxy classes
4. Zero breaking changes for existing imports
"""
import sys
import warnings
import pytest
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
class TestV2FirstCompatibility:
"""Test v2-first compatibility layer."""
def test_v2_imports_no_warnings(self):
"""Test that v2-only usage doesn't trigger warnings on Python 3.14."""
if sys.version_info < (3, 14):
pytest.skip("Python 3.14+ specific test")
# Capture warnings
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("error", DeprecationWarning)
# These should not trigger any warnings
from fastapi import FastAPI
_ = FastAPI() # Create app but don't use it
# Test jsonable_encoder with v2 model
class TestModel(BaseModel):
name: str
value: int
model = TestModel(name="test", value=42)
result = jsonable_encoder(model)
assert result == {"name": "test", "value": 42}
assert len(w) == 0, f"Unexpected warnings: {[str(warning.message) for warning in w]}"
def test_proxy_classes_isinstance(self):
"""Test that proxy classes work correctly with isinstance()."""
import fastapi.temp_pydantic_v1_params as T
# Test that proxy classes are available
assert hasattr(T, 'Param')
assert hasattr(T, 'Body')
assert hasattr(T, 'Form')
assert hasattr(T, 'File')
assert hasattr(T, 'Query')
assert hasattr(T, 'Header')
assert hasattr(T, 'Cookie')
assert hasattr(T, 'Path')
# Test isinstance() with proxy classes (expect DeprecationWarning)
with warnings.catch_warnings():
warnings.simplefilter("ignore", (DeprecationWarning, UserWarning))
param_instance = T.Param()
assert isinstance(param_instance, T.Param)
# Test that proxy classes are different from v2 params
from fastapi import params as v2_params
assert T.Param is not v2_params.Param
assert T.Body is not v2_params.Body
def test_backward_compatibility_imports(self):
"""Test that existing imports continue to work."""
# Test direct import from temp_pydantic_v1_params
from fastapi.temp_pydantic_v1_params import (
Body,
Cookie,
File,
Form,
Header,
Param,
Path,
Query,
)
# Test that classes are available and callable (expect DeprecationWarning)
with warnings.catch_warnings():
warnings.simplefilter("ignore", (DeprecationWarning, UserWarning))
body = Body()
query = Query()
form = Form()
file_param = File()
param = Param()
header = Header()
cookie = Cookie()
path = Path()
# Test that they have expected attributes
assert hasattr(body, 'default')
assert hasattr(query, 'default')
assert hasattr(form, 'default')
assert hasattr(file_param, 'default')
assert hasattr(param, 'default')
assert hasattr(header, 'default')
assert hasattr(cookie, 'default')
assert hasattr(path, 'default')
def test_lazy_loading_behavior(self):
"""Test that v1 is only loaded when actually used."""
import sys
# Clear any existing pydantic.v1 from sys.modules
_ = "pydantic.v1" in sys.modules # Check but don't use
# Import FastAPI components
_ = FastAPI() # Create app but don't use it
# At this point, pydantic.v1 should not be loaded unless it was already loaded
# (we can't test the exact state because it might have been loaded by other tests)
# Test that we can still access v1 proxy
from fastapi._compat import v1
assert v1 is not None
def test_encoders_lazy_registration(self):
"""Test that v1 encoders are registered lazily."""
# Test with a v2 model (should not trigger v1 encoder registration)
class V2Model(BaseModel):
name: str
model = V2Model(name="test")
result = jsonable_encoder(model)
assert result == {"name": "test"}
# The encoders should work without importing pydantic.v1
# (unless it was already imported by other tests)
def test_compat_module_structure(self):
"""Test that the compat module has expected structure."""
# Test that v1 proxy has expected attributes (expect DeprecationWarning)
with warnings.catch_warnings():
warnings.simplefilter("ignore", (DeprecationWarning, UserWarning))
from fastapi._compat import v1
# Test that v1 proxy has expected attributes
assert hasattr(v1, 'BaseModel')
assert hasattr(v1, 'FieldInfo')
assert hasattr(v1, 'ValidationError')
# Test that wrapper functions are available
assert hasattr(v1, '_normalize_errors')
assert hasattr(v1, '_model_dump')
assert hasattr(v1, '_model_rebuild')
def test_strict_mode_environment_variable(self):
"""Test FASTAPI_PYDANTIC_V1_STRICT environment variable behavior."""
import os
# Save original value
original_strict = os.environ.get("FASTAPI_PYDANTIC_V1_STRICT")
try:
# Test with strict mode enabled
os.environ["FASTAPI_PYDANTIC_V1_STRICT"] = "1"
# This should not raise an error unless we actually try to use v1
from fastapi import FastAPI
_ = FastAPI() # Create app but don't use it
# The strict mode only affects actual v1 usage, not imports
assert True
finally:
# Restore original value
if original_strict is not None:
os.environ["FASTAPI_PYDANTIC_V1_STRICT"] = original_strict
else:
os.environ.pop("FASTAPI_PYDANTIC_V1_STRICT", None)
def test_v1_params_composition(self):
"""Test that v1 params use composition instead of inheritance."""
from fastapi._compat._v1_params import Body, Form, Param
# Test that they can be instantiated (expect DeprecationWarning)
with warnings.catch_warnings():
warnings.simplefilter("ignore", (DeprecationWarning, UserWarning))
param = Param()
body = Body()
form = Form()
# Test that they delegate to internal v1.FieldInfo
assert hasattr(param, '_fi')
assert hasattr(body, '_fi')
assert hasattr(form, '_fi')
# Test that they have expected interface
assert hasattr(param, 'default')
assert hasattr(body, 'default')
assert hasattr(form, 'default')
if __name__ == "__main__":
pytest.main([__file__])