diff --git a/tests/test_main.py b/tests/test_main.py index 72e1bca31b2..eb154591f5d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,21 +3,6 @@ from typing import Any, Callable, ClassVar, Dict, List, Mapping, Optional, Type, get_type_hints from uuid import UUID, uuid4 -if sys.version_info >= (3, 7): - from contextlib import nullcontext -else: - - class nullcontext: - def __init__(self, enter_result=None): - self.enter_result = enter_result - - def __enter__(self): - return self.enter_result - - def __exit__(self, *excinfo): - pass - - import pytest from pydantic import ( @@ -1443,113 +1428,114 @@ class M(BaseModel): get_type_hints(M.__config__) -_5 = 5 - - -def _Annotated(*args): - return Annotated[args] if Annotated else None - - @pytest.mark.skipif(not Annotated, reason='typing_extensions not installed') @pytest.mark.parametrize( - ['hint', 'value', 'subclass_ctx', 'empty_init_ctx'], + ['hint_fn', 'value'], [ - # Test non-pydantic Annotated uses (random metadata) + # Test Annotated types with arbitrary metadata pytest.param( - _Annotated(int, 0), - Undefined, - None, - pytest.raises(ValueError, match='field required'), - id='misc-no-default', - ), - pytest.param( - _Annotated(int, 0), - _5, - None, - None, + lambda: Annotated[int, 0], + 5, id='misc-default', ), pytest.param( - _Annotated(int, 0), - Field(default=_5, ge=0), - None, - None, + lambda: Annotated[int, 0], + Field(default=5, ge=0), id='misc-field-default-constraint', ), # Test valid Annotated Field uses pytest.param( - _Annotated(int, Field(description='Test')), - _5, - None, - None, + lambda: Annotated[int, Field(description='Test')], + 5, id='annotated-field-value-default', ), pytest.param( - _Annotated(int, Field(default_factory=lambda: _5, description='Test')), + lambda: Annotated[int, Field(default_factory=lambda: 5, description='Test')], Undefined, - None, - None, id='annotated-field-default_factory', ), - # Test invalid Annotated Field uses - pytest.param( - _Annotated(int, Field()), - Undefined, - None, - pytest.raises(ValueError, match='field required'), - id='annotated-field-no-default', - ), + ], +) +def test_annotated(hint_fn, value): + hint = hint_fn() + + class M(BaseModel): + x: hint = value + + assert M().x == 5 + assert M(x=10).x == 10 + + # get_type_hints doesn't recognize typing_extensions.Annotated, so will return the full + # annotation. 3.9 w/ stock Annotated will return the wrapped type by default, but return the + # full thing with the new include_extras flag. + if sys.version_info >= (3, 9): + assert get_type_hints(M)['x'] is int + assert get_type_hints(M, include_extras=True)['x'] == hint + else: + assert get_type_hints(M)['x'] == hint + + +@pytest.mark.skipif(not Annotated, reason='typing_extensions not installed') +@pytest.mark.parametrize( + ['hint_fn', 'value', 'subclass_ctx'], + [ pytest.param( - _Annotated(int, Field(_5)), + lambda: Annotated[int, Field(5)], Undefined, pytest.raises(ValueError, match='`Field` default cannot be set in `Annotated`'), - None, id='annotated-field-default', ), pytest.param( - _Annotated(int, Field(), Field()), + lambda: Annotated[int, Field(), Field()], Undefined, pytest.raises(ValueError, match='cannot specify multiple `Annotated` `Field`s'), - None, id='annotated-field-dup', ), pytest.param( - _Annotated(int, Field()), + lambda: Annotated[int, Field()], Field(), pytest.raises(ValueError, match='cannot specify `Annotated` and value `Field`'), - None, id='annotated-field-value-field-dup', ), pytest.param( - _Annotated(int, Field(default_factory=lambda: _5)), # The factory is not used - _5, + lambda: Annotated[int, Field(default_factory=lambda: 5)], # The factory is not used + 5, pytest.raises(ValueError, match='cannot specify both default and default_factory'), - None, id='annotated-field-default_factory-value-default', ), ], ) -def test_annotated(hint, value, subclass_ctx, empty_init_ctx): - if hint is None: - pytest.skip('typing_extensions not installed') - - M = None - with (subclass_ctx or nullcontext()): +def test_annotated_model_exceptions(hint_fn, value, subclass_ctx): + hint = hint_fn() + with subclass_ctx: class M(BaseModel): x: hint = value - if M is None: - return - with (empty_init_ctx or nullcontext()): - assert M().x == _5 - assert M(x=10).x == 10 - # get_type_hints doesn't recognize typing_extensions.Annotated, so will return the full - # annotation. 3.9 w/ stock Annotated will return the wrapped type by default, but return the - # full thing with the new include_extras flag. - if sys.version_info >= (3, 9): - assert get_type_hints(M)['x'] is int - assert get_type_hints(M, include_extras=True)['x'] == hint - else: - assert get_type_hints(M)['x'] == hint +@pytest.mark.skipif(not Annotated, reason='typing_extensions not installed') +@pytest.mark.parametrize( + ['hint_fn', 'value', 'empty_init_ctx'], + [ + pytest.param( + lambda: Annotated[int, 0], + Undefined, + pytest.raises(ValueError, match='field required'), + id='misc-no-default', + ), + pytest.param( + lambda: Annotated[int, Field()], + Undefined, + pytest.raises(ValueError, match='field required'), + id='annotated-field-no-default', + ), + ], +) +def test_annotated_instance_exceptions(hint_fn, value, empty_init_ctx): + hint = hint_fn() + + class M(BaseModel): + x: hint = value + + with empty_init_ctx: + assert M().x == 5