From 74c2552bd742005955822548acf8eb5e6fab7560 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 5 Feb 2025 14:09:17 +0100 Subject: [PATCH] Adding upstream version 2.10.2. Signed-off-by: Daniel Baumann --- .github/workflows/ci.yml | 10 ++--- .pre-commit-config.yaml | 9 ++++- HISTORY.md | 44 ++++++++++++++++++++++ Makefile | 4 ++ README.md | 14 +++++++ pydantic_extra_types/__init__.py | 2 +- pydantic_extra_types/color.py | 8 ++-- pydantic_extra_types/coordinate.py | 8 ++-- pydantic_extra_types/domain.py | 6 +-- pydantic_extra_types/pendulum_dt.py | 30 ++++++++++++--- pydantic_extra_types/phone_numbers.py | 10 ++--- pydantic_extra_types/routing_number.py | 4 +- pydantic_extra_types/semantic_version.py | 4 ++ pydantic_extra_types/semver.py | 3 +- pydantic_extra_types/timezone_name.py | 2 +- pyproject.toml | 2 +- tests/test_json_schema.py | 7 +--- tests/test_pendulum_dt.py | 48 ++++++++++++++++++++++-- tests/test_phone_numbers_validator.py | 8 +--- uv.lock | 2 +- 20 files changed, 176 insertions(+), 49 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2fc6c3..3e92400 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v4 + - uses: astral-sh/setup-uv@v5 with: enable-cache: true @@ -39,7 +39,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] env: UV_PYTHON: ${{ matrix.python-version }} @@ -47,7 +47,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v4 + - uses: astral-sh/setup-uv@v5 with: enable-cache: true @@ -74,7 +74,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v4 + - uses: astral-sh/setup-uv@v5 with: enable-cache: true @@ -111,7 +111,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v4 + - uses: astral-sh/setup-uv@v5 with: enable-cache: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1bbdc67..52cedf4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v5.0.0 hooks: - id: no-commit-to-branch # prevent direct commits to the `main` branch - id: check-yaml @@ -24,3 +24,10 @@ repos: types: [python] language: system pass_filenames: false + - id: Typecheck + name: Typecheck + entry: make + args: [typecheck] + types: [python] + language: system + pass_filenames: false diff --git a/HISTORY.md b/HISTORY.md index 381beca..afbcc75 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,50 @@ ## Latest Changes +## 2.10.2 + +* Add back Python 3.8 support by @Viicos in https://github.com/pydantic/pydantic-extra-types/pull/249 +* ⬆ Bump astral-sh/setup-uv from 4 to 5 by @dependabot in https://github.com/pydantic/pydantic-extra-types/pull/282 +* Preserve months when using the Pendulum Duration type by @gareththackeray in https://github.com/pydantic/pydantic-extra-types/pull/283 +* ✨ Add type checking support and improve type hints across the codebase by @yezz123 in https://github.com/pydantic/pydantic-extra-types/pull/285 +* 📝 Add additional installation information to README by @oakhan3 in https://github.com/pydantic/pydantic-extra-types/pull/233 + +## 2.10.1 + +* Allow build with python-ulid 3.0.0 by @sunpoet in https://github.com/pydantic/pydantic-extra-types/pull/225 +* 🔨 added automatic syntax-upgrade hook ~ pyupgrade by @janas-adam in https://github.com/pydantic/pydantic-extra-types/pull/229 +* :fire: Revert adding pyupgrade as a hook in pre-commit by @yezz123 in https://github.com/pydantic/pydantic-extra-types/pull/230 +* isolate url in Currency by @edasubert in https://github.com/pydantic/pydantic-extra-types/pull/235 +* lower case currency is valid by @edasubert in https://github.com/pydantic/pydantic-extra-types/pull/236 +* Update SemanticVersion by @viccie30 in https://github.com/pydantic/pydantic-extra-types/pull/237 +* Epoch - unix timestamp by @commonism in https://github.com/pydantic/pydantic-extra-types/pull/240 +* :recycle: Migrate Pydantic Extra Types to use uv by @yezz123 in https://github.com/pydantic/pydantic-extra-types/pull/241 +* ⬆ Bump astral-sh/setup-uv from 3 to 4 by @dependabot in https://github.com/pydantic/pydantic-extra-types/pull/245 +* ⬆ Bump pre-commit/action from 3.0.0 to 3.0.1 by @dependabot in https://github.com/pydantic/pydantic-extra-types/pull/244 +* 🔖 Release version 2.10.1 by @yezz123 in https://github.com/pydantic/pydantic-extra-types/pull/246 +* Fix check python version for release by @hramezani in https://github.com/pydantic/pydantic-extra-types/pull/247 + +## 2.10.0 + +### Types + +* Add semantic version type by @jbkroner in https://github.com/pydantic/pydantic-extra-types/pull/199 +* feat: add S3Path by @lucianosrp in https://github.com/pydantic/pydantic-extra-types/pull/206 + +### Refactor + +* feature: Improve phone number validator by @mZbZ in https://github.com/pydantic/pydantic-extra-types/pull/202 +* Feature: Add phone number validator by @mZbZ in https://github.com/pydantic/pydantic-extra-types/pull/203 +* Domain name string type by @matter1-git in https://github.com/pydantic/pydantic-extra-types/pull/212 +* Adjust test_json_schema() for Pydantic 2.9 by @musicinmybrain in https://github.com/pydantic/pydantic-extra-types/pull/215 +* Allow python-ulid 3.0 by @musicinmybrain in https://github.com/pydantic/pydantic-extra-types/pull/222 + +### Dependencies + +* ⬆ Bump the python-packages group with 5 updates by @dependabot in https://github.com/pydantic/pydantic-extra-types/pull/201 +* ✨ deprecate `semver` in favor of `semantic_version` by @07pepa in https://github.com/pydantic/pydantic-extra-types/pull/209 +* 🔖 Release version 2.10.0 by @yezz123 in https://github.com/pydantic/pydantic-extra-types/pull/224 + ## 2.9.0 ### Types diff --git a/Makefile b/Makefile index 9c49a64..5f7f05c 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,10 @@ lint: uv run ruff format --check uv run ruff check +.PHONY: typecheck # Typecheck the code +typecheck: + uv run mypy pydantic_extra_types + .PHONY: test test: uv run pytest diff --git a/README.md b/README.md index 0d87915..6e2a2ec 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,17 @@ A place for pydantic types that probably shouldn't exist in the main pydantic lib. See [pydantic/pydantic#5012](https://github.com/pydantic/pydantic/issues/5012) for more info. + +## Installation + +Install this library with the desired extras dependencies as listed in [project.optional-dependencies](./pyproject.toml). + +For example, if pendulum support was desired: + +```shell +# via uv +$ uv add "pydantic-extra-types[pendulum]" + +# via pip +$ pip install -U "pydantic-extra-types[pendulum]" +``` diff --git a/pydantic_extra_types/__init__.py b/pydantic_extra_types/__init__.py index a1c6124..a6950a3 100644 --- a/pydantic_extra_types/__init__.py +++ b/pydantic_extra_types/__init__.py @@ -1 +1 @@ -__version__ = '2.10.1' +__version__ = '2.10.2' diff --git a/pydantic_extra_types/color.py b/pydantic_extra_types/color.py index 5f1e3a5..92995e5 100644 --- a/pydantic_extra_types/color.py +++ b/pydantic_extra_types/color.py @@ -12,16 +12,16 @@ from __future__ import annotations import math import re from colorsys import hls_to_rgb, rgb_to_hls -from typing import Any, Callable, Literal, Union, cast +from typing import Any, Callable, Literal, Tuple, Union, cast from pydantic import GetJsonSchemaHandler from pydantic._internal import _repr from pydantic.json_schema import JsonSchemaValue from pydantic_core import CoreSchema, PydanticCustomError, core_schema -ColorTuple = Union[tuple[int, int, int], tuple[int, int, int, float]] +ColorTuple = Union[Tuple[int, int, int], Tuple[int, int, int, float]] ColorType = Union[ColorTuple, str, 'Color'] -HslColorTuple = Union[tuple[float, float, float], tuple[float, float, float, float]] +HslColorTuple = Union[Tuple[float, float, float], Tuple[float, float, float, float]] class RGBA: @@ -115,7 +115,7 @@ class Color(_repr.Representation): """ if self._rgba.alpha is not None: return self.as_hex() - rgb = cast(tuple[int, int, int], self.as_rgb_tuple()) + rgb = cast('tuple[int, int, int]', self.as_rgb_tuple()) if rgb in COLORS_BY_VALUE: return COLORS_BY_VALUE[rgb] diff --git a/pydantic_extra_types/coordinate.py b/pydantic_extra_types/coordinate.py index 04717b9..cfd14fe 100644 --- a/pydantic_extra_types/coordinate.py +++ b/pydantic_extra_types/coordinate.py @@ -3,8 +3,10 @@ [`Coordinate`][pydantic_extra_types.coordinate.Coordinate] data types. """ +from __future__ import annotations + from dataclasses import dataclass -from typing import Any, ClassVar +from typing import Any, ClassVar, Tuple from pydantic import GetCoreSchemaHandler from pydantic._internal import _repr @@ -89,7 +91,7 @@ class Coordinate(_repr.Representation): ``` """ - _NULL_ISLAND: ClassVar[tuple[float, float]] = (0.0, 0.0) + _NULL_ISLAND: ClassVar[Tuple[float, float]] = (0.0, 0.0) latitude: Latitude longitude: Longitude @@ -100,7 +102,7 @@ class Coordinate(_repr.Representation): core_schema.no_info_wrap_validator_function(cls._parse_str, core_schema.str_schema()), core_schema.no_info_wrap_validator_function( cls._parse_tuple, - handler.generate_schema(tuple[float, float]), + handler.generate_schema(Tuple[float, float]), ), handler(source), ] diff --git a/pydantic_extra_types/domain.py b/pydantic_extra_types/domain.py index 9dfef55..6640de6 100644 --- a/pydantic_extra_types/domain.py +++ b/pydantic_extra_types/domain.py @@ -5,7 +5,6 @@ This class depends on the `pydantic` package and implements custom validation fo from __future__ import annotations import re -from collections.abc import Mapping from typing import Any from pydantic import GetCoreSchemaHandler @@ -54,5 +53,6 @@ class DomainStr(str): @classmethod def __get_pydantic_json_schema__( cls, schema: core_schema.CoreSchema, handler: GetCoreSchemaHandler - ) -> Mapping[str, Any]: - return handler(schema) + ) -> dict[str, Any]: + # Cast the return value to dict[str, Any] + return dict(handler(schema)) diff --git a/pydantic_extra_types/pendulum_dt.py b/pydantic_extra_types/pendulum_dt.py index 9b7b00b..f306529 100644 --- a/pydantic_extra_types/pendulum_dt.py +++ b/pydantic_extra_types/pendulum_dt.py @@ -2,6 +2,8 @@ CoreSchema implementation. This allows Pydantic to validate the DateTime object. """ +from __future__ import annotations + try: from pendulum import Date as _Date from pendulum import DateTime as _DateTime @@ -63,7 +65,7 @@ class DateTime(_DateTime, metaclass=DateTimeSettings): return core_schema.no_info_wrap_validator_function(cls._validate, core_schema.datetime_schema()) @classmethod - def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> 'DateTime': + def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> DateTime: """Validate the datetime object and return it. Args: @@ -128,7 +130,7 @@ class Date(_Date): return core_schema.no_info_wrap_validator_function(cls._validate, core_schema.date_schema()) @classmethod - def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> 'Date': + def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> Date: """Validate the date object and return it. Args: @@ -187,7 +189,7 @@ class Duration(_Duration): return core_schema.no_info_wrap_validator_function(cls._validate, core_schema.timedelta_schema()) @classmethod - def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> 'Duration': + def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> Duration: """Validate the Duration object and return it. Args: @@ -197,9 +199,25 @@ class Duration(_Duration): Returns: The validated value or raises a PydanticCustomError. """ - # if we are passed an existing instance, pass it straight through. - if isinstance(value, (_Duration, timedelta)): - return Duration(seconds=value.total_seconds()) + + if isinstance(value, _Duration): + return Duration( + years=value.years, + months=value.months, + weeks=value.weeks, + days=value.remaining_days, + hours=value.hours, + minutes=value.minutes, + seconds=value.remaining_seconds, + microseconds=value.microseconds, + ) + + if isinstance(value, timedelta): + return Duration( + days=value.days, + seconds=value.seconds, + microseconds=value.microseconds, + ) try: parsed = parse(value, exact=True) diff --git a/pydantic_extra_types/phone_numbers.py b/pydantic_extra_types/phone_numbers.py index e405d8c..880446f 100644 --- a/pydantic_extra_types/phone_numbers.py +++ b/pydantic_extra_types/phone_numbers.py @@ -9,7 +9,7 @@ from __future__ import annotations from collections.abc import Sequence from dataclasses import dataclass from functools import partial -from typing import Any, ClassVar, Optional +from typing import Any, ClassVar from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler from pydantic_core import PydanticCustomError, core_schema @@ -90,7 +90,7 @@ class PhoneNumberValidator: supported_regions (list[str]): The supported regions. If empty, all regions are supported (default). Returns: - str: The formatted phone number. + The formatted phone number. Example: MyNumberType = Annotated[ @@ -107,9 +107,9 @@ class PhoneNumberValidator: us_number: USNumberType """ - default_region: Optional[str] = None + default_region: str | None = None number_format: str = 'RFC3966' - supported_regions: Optional[Sequence[str]] = None + supported_regions: Sequence[str] | None = None def __post_init__(self) -> None: if self.default_region and self.default_region not in phonenumbers.SUPPORTED_REGIONS: @@ -131,7 +131,7 @@ class PhoneNumberValidator: def _parse( region: str | None, number_format: str, - supported_regions: Optional[Sequence[str]], + supported_regions: Sequence[str] | None, phone_number: Any, ) -> str: if not phone_number: diff --git a/pydantic_extra_types/routing_number.py b/pydantic_extra_types/routing_number.py index 912e3e8..fa9438d 100644 --- a/pydantic_extra_types/routing_number.py +++ b/pydantic_extra_types/routing_number.py @@ -2,6 +2,8 @@ [`ABARoutingNumber`][pydantic_extra_types.routing_number.ABARoutingNumber] data type. """ +from __future__ import annotations + from typing import Any, ClassVar from pydantic import GetCoreSchemaHandler @@ -54,7 +56,7 @@ class ABARoutingNumber(str): ) @classmethod - def _validate(cls, __input_value: str, _: core_schema.ValidationInfo) -> 'ABARoutingNumber': + def _validate(cls, __input_value: str, _: core_schema.ValidationInfo) -> ABARoutingNumber: return cls(__input_value) @classmethod diff --git a/pydantic_extra_types/semantic_version.py b/pydantic_extra_types/semantic_version.py index 1da3f83..8ffdfde 100644 --- a/pydantic_extra_types/semantic_version.py +++ b/pydantic_extra_types/semantic_version.py @@ -53,3 +53,7 @@ class SemanticVersion(semver.Version): pattern=r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' ) ) + + @classmethod + def validate_from_str(cls, value: str) -> 'SemanticVersion': + return cls.parse(value) diff --git a/pydantic_extra_types/semver.py b/pydantic_extra_types/semver.py index 2b58864..f53b80f 100644 --- a/pydantic_extra_types/semver.py +++ b/pydantic_extra_types/semver.py @@ -4,12 +4,13 @@ This class depends on the [semver](https://python-semver.readthedocs.io/en/lates """ import warnings -from typing import Annotated, Any, Callable +from typing import Any, Callable from pydantic import GetJsonSchemaHandler from pydantic.json_schema import JsonSchemaValue from pydantic_core import core_schema from semver import Version +from typing_extensions import Annotated warnings.warn( 'Use from pydantic_extra_types.semver import SemanticVersion instead. Will be removed in 3.0.0.', DeprecationWarning diff --git a/pydantic_extra_types/timezone_name.py b/pydantic_extra_types/timezone_name.py index b33b6e6..773a612 100644 --- a/pydantic_extra_types/timezone_name.py +++ b/pydantic_extra_types/timezone_name.py @@ -57,7 +57,7 @@ def get_timezones() -> set[str]: class TimeZoneNameSettings(type): def __new__(cls, name: str, bases: tuple[type, ...], dct: dict[str, Any], **kwargs: Any) -> type[TimeZoneName]: dct['strict'] = kwargs.pop('strict', True) - return cast(type[TimeZoneName], super().__new__(cls, name, bases, dct)) + return cast('type[TimeZoneName]', super().__new__(cls, name, bases, dct)) def __init__(cls, name: str, bases: tuple[type, ...], dct: dict[str, Any], **kwargs: Any) -> None: super().__init__(name, bases, dct) diff --git a/pyproject.toml b/pyproject.toml index 8192da1..3b55e38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ keep-runtime-typing = true [tool.ruff] line-length = 120 -target-version = "py39" +target-version = 'py38' [tool.ruff.lint] extend-select = [ diff --git a/tests/test_json_schema.py b/tests/test_json_schema.py index 96773bc..abe419e 100644 --- a/tests/test_json_schema.py +++ b/tests/test_json_schema.py @@ -3,12 +3,7 @@ from typing import Union import pycountry import pytest from pydantic import BaseModel - -try: - from typing import Annotated -except ImportError: - # Python 3.8 - from typing import Annotated +from typing_extensions import Annotated import pydantic_extra_types from pydantic_extra_types import epoch diff --git a/tests/test_pendulum_dt.py b/tests/test_pendulum_dt.py index 868648b..7635b5d 100644 --- a/tests/test_pendulum_dt.py +++ b/tests/test_pendulum_dt.py @@ -92,8 +92,30 @@ def test_pendulum_date_existing_instance(instance): [ pendulum.duration(days=42, hours=13, minutes=37), pendulum.duration(days=-42, hours=13, minutes=37), + pendulum.duration(weeks=97), + pendulum.duration(days=463), + pendulum.duration(milliseconds=90122), + pendulum.duration(microseconds=90122), + pendulum.duration( + years=2, + months=3, + weeks=19, + days=1, + hours=25, + seconds=732, + milliseconds=123, + microseconds=1324, + ), timedelta(days=42, hours=13, minutes=37), timedelta(days=-42, hours=13, minutes=37), + timedelta( + weeks=19, + days=1, + hours=25, + seconds=732, + milliseconds=123, + microseconds=1324, + ), ], ) def test_duration_timedelta__existing_instance(instance): @@ -227,7 +249,11 @@ def test_pendulum_dt_from_str_unix_timestamp_is_utc(dt): @pytest.mark.parametrize( 'd', - [pendulum.now().date().isoformat(), pendulum.now().to_w3c_string(), pendulum.now().to_iso8601_string()], + [ + pendulum.now().date().isoformat(), + pendulum.now().to_w3c_string(), + pendulum.now().to_iso8601_string(), + ], ) def test_pendulum_date_from_serialized(d): """Verifies that building an instance from serialized, well-formed strings decode properly.""" @@ -308,7 +334,10 @@ def test_pendulum_dt_non_strict_malformed(dt): DtModelNotStrict(dt=dt) -@pytest.mark.parametrize('invalid_value', [None, 'malformed', pendulum.today().to_iso8601_string()[:5], 'P10Y10M10D']) +@pytest.mark.parametrize( + 'invalid_value', + [None, 'malformed', pendulum.today().to_iso8601_string()[:5], 'P10Y10M10D'], +) def test_pendulum_date_malformed(invalid_value): """Verifies that the instance fails to validate if malformed date are passed.""" with pytest.raises(ValidationError): @@ -317,7 +346,14 @@ def test_pendulum_date_malformed(invalid_value): @pytest.mark.parametrize( 'delta_t', - [None, 'malformed', pendulum.today().to_iso8601_string()[:5], 42, '12m', '2021-01-01T12:00:00'], + [ + None, + 'malformed', + pendulum.today().to_iso8601_string()[:5], + 42, + '12m', + '2021-01-01T12:00:00', + ], ) def test_pendulum_duration_malformed(delta_t): """Verifies that the instance fails to validate if malformed durations are passed.""" @@ -344,3 +380,9 @@ def test_date_type_adapter(input_type: type, value, is_instance: type): assert type(validated) is input_type assert isinstance(validated, input_type) assert isinstance(validated, is_instance) + + +def test_pendulum_duration_months_are_preserved(): + m = DurationModel(delta_t=pendulum.Duration(months=1)) + + assert m.delta_t.months == 1 diff --git a/tests/test_phone_numbers_validator.py b/tests/test_phone_numbers_validator.py index e882da8..81f682c 100644 --- a/tests/test_phone_numbers_validator.py +++ b/tests/test_phone_numbers_validator.py @@ -1,16 +1,10 @@ from typing import Any, Optional, Union -try: - from typing import Annotated -except ImportError: - # Python 3.8 - from typing import Annotated - - import phonenumbers import pytest from phonenumbers import PhoneNumber from pydantic import BaseModel, TypeAdapter, ValidationError +from typing_extensions import Annotated from pydantic_extra_types.phone_numbers import PhoneNumberValidator diff --git a/uv.lock b/uv.lock index d8ad2db..765a14c 100644 --- a/uv.lock +++ b/uv.lock @@ -477,7 +477,7 @@ wheels = [ [[package]] name = "pydantic-extra-types" -version = "2.10.0" +version = "2.10.1" source = { editable = "." } dependencies = [ { name = "pydantic" },