diff --git a/pydantic/__init__.py b/pydantic/__init__.py index 8a71bbd85c..293f820d6c 100644 --- a/pydantic/__init__.py +++ b/pydantic/__init__.py @@ -176,6 +176,7 @@ 'PastDatetime', 'FutureDatetime', 'AwareDatetime', + 'UTCDatetime', 'NaiveDatetime', 'AllowInfNan', 'EncoderProtocol', @@ -325,6 +326,7 @@ 'PastDatetime': (__package__, '.types'), 'FutureDatetime': (__package__, '.types'), 'AwareDatetime': (__package__, '.types'), + 'UTCDatetime': (__package__, '.types'), 'NaiveDatetime': (__package__, '.types'), 'AllowInfNan': (__package__, '.types'), 'EncoderProtocol': (__package__, '.types'), diff --git a/pydantic/types.py b/pydantic/types.py index d8b042e438..b50dd63cad 100644 --- a/pydantic/types.py +++ b/pydantic/types.py @@ -92,6 +92,7 @@ 'FutureDatetime', 'condate', 'AwareDatetime', + 'UTCDatetime', 'NaiveDatetime', 'AllowInfNan', 'EncoderProtocol', @@ -2067,6 +2068,7 @@ def condate( if TYPE_CHECKING: AwareDatetime = Annotated[datetime, ...] + UTCDatetime = Annotated[datetime, ...] NaiveDatetime = Annotated[datetime, ...] PastDatetime = Annotated[datetime, ...] FutureDatetime = Annotated[datetime, ...] @@ -2092,6 +2094,25 @@ def __get_pydantic_core_schema__( def __repr__(self) -> str: return 'AwareDatetime' + class UTCDatetime: + """A datetime that needs UTC as the timezone.""" + + @classmethod + def __get_pydantic_core_schema__( + cls, source: type[Any], handler: GetCoreSchemaHandler + ) -> core_schema.CoreSchema: + if cls is source: + # used directly as a type + return core_schema.datetime_schema(tz_constraint=0) + else: + schema = handler(source) + _check_annotated_type(schema['type'], 'datetime', cls.__name__) + schema['tz_constraint'] = 0 + return schema + + def __repr__(self) -> str: + return 'UTCDatetime' + class NaiveDatetime: """A datetime that doesn't require timezone info.""" diff --git a/tests/mypy/modules/success.py b/tests/mypy/modules/success.py index 4f0d6db99e..0863a88eb2 100644 --- a/tests/mypy/modules/success.py +++ b/tests/mypy/modules/success.py @@ -39,6 +39,7 @@ StrictInt, StrictStr, UrlConstraints, + UTCDatetime, WrapValidator, create_model, field_validator, @@ -243,6 +244,7 @@ class PydanticTypes(BaseModel): my_past_datetime: PastDatetime = datetime.now() - timedelta(1) my_future_datetime: FutureDatetime = datetime.now() + timedelta(1) my_aware_datetime: AwareDatetime = datetime.now(tz=timezone.utc) + my_utc_datetime: UTCDatetime = datetime.now(tz=timezone.utc) my_naive_datetime: NaiveDatetime = datetime.now() diff --git a/tests/mypy/outputs/1.0.1/mypy-default_ini/success.py b/tests/mypy/outputs/1.0.1/mypy-default_ini/success.py index 9406c543df..d83569efbc 100644 --- a/tests/mypy/outputs/1.0.1/mypy-default_ini/success.py +++ b/tests/mypy/outputs/1.0.1/mypy-default_ini/success.py @@ -14,6 +14,7 @@ from pydantic import ( UUID1, AwareDatetime, + UTCDatetime, BaseModel, ConfigDict, DirectoryPath, @@ -249,6 +250,7 @@ class PydanticTypes(BaseModel): my_past_datetime: PastDatetime = datetime.now() - timedelta(1) my_future_datetime: FutureDatetime = datetime.now() + timedelta(1) my_aware_datetime: AwareDatetime = datetime.now(tz=timezone.utc) + my_utc_datetime: UTCDatetime = datetime.now(tz=timezone.utc) my_naive_datetime: NaiveDatetime = datetime.now() diff --git a/tests/mypy/outputs/1.0.1/pyproject-default_toml/success.py b/tests/mypy/outputs/1.0.1/pyproject-default_toml/success.py index 9406c543df..d83569efbc 100644 --- a/tests/mypy/outputs/1.0.1/pyproject-default_toml/success.py +++ b/tests/mypy/outputs/1.0.1/pyproject-default_toml/success.py @@ -14,6 +14,7 @@ from pydantic import ( UUID1, AwareDatetime, + UTCDatetime, BaseModel, ConfigDict, DirectoryPath, @@ -249,6 +250,7 @@ class PydanticTypes(BaseModel): my_past_datetime: PastDatetime = datetime.now() - timedelta(1) my_future_datetime: FutureDatetime = datetime.now() + timedelta(1) my_aware_datetime: AwareDatetime = datetime.now(tz=timezone.utc) + my_utc_datetime: UTCDatetime = datetime.now(tz=timezone.utc) my_naive_datetime: NaiveDatetime = datetime.now() diff --git a/tests/test_datetime.py b/tests/test_datetime.py index 6d05031396..222f632928 100644 --- a/tests/test_datetime.py +++ b/tests/test_datetime.py @@ -13,6 +13,7 @@ NaiveDatetime, PastDate, PastDatetime, + UTCDatetime, ValidationError, condate, ) @@ -49,6 +50,11 @@ def aware_datetime_type(request): return request.param +@pytest.fixture(scope='module', params=[UTCDatetime, Annotated[datetime, UTCDatetime()]]) +def utc_datetime_type(request): + return request.param + + @pytest.fixture(scope='module', params=[NaiveDatetime, Annotated[datetime, NaiveDatetime()]]) def naive_datetime_type(request): return request.param @@ -605,6 +611,7 @@ class Model(BaseModel): FutureDatetime, NaiveDatetime, AwareDatetime, + UTCDatetime, ), ) def test_invalid_annotated_type(annotation): diff --git a/tests/test_types.py b/tests/test_types.py index 217c867a57..5172801e22 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -101,6 +101,7 @@ StringConstraints, Tag, TypeAdapter, + UTCDatetime, ValidationError, conbytes, condate, @@ -4305,6 +4306,7 @@ class Foobar(BaseModel): PastDatetime, FutureDatetime, AwareDatetime, + UTCDatetime, NaiveDatetime, ], ) @@ -6128,6 +6130,7 @@ def test_annotated_default_value_functional_validator() -> None: (PastDate, 'PastDate'), (FutureDate, 'FutureDate'), (AwareDatetime, 'AwareDatetime'), + (UTCDatetime, 'UTCDatetime'), (NaiveDatetime, 'NaiveDatetime'), (PastDatetime, 'PastDatetime'), (FutureDatetime, 'FutureDatetime'),