summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-11 16:31:24 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-11 16:31:24 +0000
commit23693ac9cf0fd4e736c456715b1cf5aa14c9f384 (patch)
treee10bf4b80f5d1b61ddf4e05b350974d41d7091c9
parentAdding upstream version 2.7.0. (diff)
downloadpydantic-extra-types-23693ac9cf0fd4e736c456715b1cf5aa14c9f384.tar.xz
pydantic-extra-types-23693ac9cf0fd4e736c456715b1cf5aa14c9f384.zip
Adding upstream version 2.8.0.upstream/2.8.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--HISTORY.md21
-rw-r--r--pydantic_extra_types/__init__.py2
-rw-r--r--pydantic_extra_types/color.py59
-rw-r--r--pydantic_extra_types/coordinate.py8
-rw-r--r--pydantic_extra_types/country.py4
-rw-r--r--pydantic_extra_types/currency_code.py4
-rw-r--r--pydantic_extra_types/language_code.py4
-rw-r--r--pydantic_extra_types/pendulum_dt.py16
-rw-r--r--pydantic_extra_types/phone_numbers.py10
-rw-r--r--pydantic_extra_types/script_code.py100
-rw-r--r--pydantic_extra_types/ulid.py8
-rw-r--r--pyproject.toml7
-rw-r--r--requirements/linting.txt8
-rw-r--r--requirements/testing.txt6
-rw-r--r--tests/test_json_schema.py20
-rw-r--r--tests/test_pendulum_dt.py74
-rw-r--r--tests/test_phone_numbers.py14
-rw-r--r--tests/test_scripts.py53
18 files changed, 341 insertions, 77 deletions
diff --git a/HISTORY.md b/HISTORY.md
index c3f4864..3101c27 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -2,6 +2,27 @@
## Latest Changes
+## 2.8.0
+
+### Refactor
+
+* ♻️ refactor some functions & minor changes. [#180](https://github.com/pydantic/pydantic-extra-types/pull/180) by [@yezz123](https://github.com/yezz123)
+
+### Internal
+
+* Allow requiring extra dependencies. [#178](https://github.com/pydantic/pydantic-extra-types/pull/178) by [@yezz123](https://github.com/yezz123)
+
+### Types
+
+* Add ISO 15924 and tests. [#174](https://github.com/pydantic/pydantic-extra-types/pull/174) by [@07pepa](https://github.com/07pepa)
+* add native datetime to pendulum_dt.py. [#176](https://github.com/pydantic/pydantic-extra-types/pull/176) by [@07pepa](https://github.com/07pepa)
+* add hash and eq to phone_numbers. [#177](https://github.com/pydantic/pydantic-extra-types/pull/177) by [@07pepa](https://github.com/07pepa)
+
+### Dependencies
+
+* ⬆ Bump the python-packages group with 5 updates. PR [#179](https://github.com/pydantic/pydantic-extra-types/pull/179) by @dependabot
+* ⬆ Bump the python-packages group with 4 updates. PR [#171](https://github.com/pydantic/pydantic-extra-types/pull/171) by @dependabot
+
## 2.7.0
* 🔥 Remove latest-changes workflow. PR [#165](https://github.com/pydantic/pydantic-extra-types/pull/165) by [yezz123](https://github.com/yezz123)
diff --git a/pydantic_extra_types/__init__.py b/pydantic_extra_types/__init__.py
index 766ce2d..f2df444 100644
--- a/pydantic_extra_types/__init__.py
+++ b/pydantic_extra_types/__init__.py
@@ -1 +1 @@
-__version__ = '2.7.0'
+__version__ = '2.8.0'
diff --git a/pydantic_extra_types/color.py b/pydantic_extra_types/color.py
index 8b9eabe..8e28ff4 100644
--- a/pydantic_extra_types/color.py
+++ b/pydantic_extra_types/color.py
@@ -121,17 +121,16 @@ class Color(_repr.Representation):
Raises:
ValueError: When no named color is found and fallback is `False`.
"""
- if self._rgba.alpha is None:
- rgb = cast(Tuple[int, int, int], self.as_rgb_tuple())
- try:
- return COLORS_BY_VALUE[rgb]
- except KeyError as e:
- if fallback:
- return self.as_hex()
- else:
- raise ValueError('no named color found, use fallback=True, as_hex() or as_rgb()') from e
- else:
+ if self._rgba.alpha is not None:
return self.as_hex()
+ rgb = cast(Tuple[int, int, int], self.as_rgb_tuple())
+ try:
+ return COLORS_BY_VALUE[rgb]
+ except KeyError as e:
+ if fallback:
+ return self.as_hex()
+ else:
+ raise ValueError('no named color found, use fallback=True, as_hex() or as_rgb()') from e
def as_hex(self, format: Literal['short', 'long'] = 'short') -> str:
"""Returns the hexadecimal representation of the color.
@@ -149,7 +148,7 @@ class Color(_repr.Representation):
as_hex = ''.join(f'{v:02x}' for v in values)
if format == 'short' and all(c in repeat_colors for c in values):
as_hex = ''.join(as_hex[c] for c in range(0, len(as_hex), 2))
- return '#' + as_hex
+ return f'#{as_hex}'
def as_rgb(self) -> str:
"""
@@ -179,16 +178,10 @@ class Color(_repr.Representation):
If alpha is included, it is in the range 0 to 1.
"""
r, g, b = (float_to_255(c) for c in self._rgba[:3])
- if alpha is None:
- if self._rgba.alpha is None:
- return r, g, b
- else:
- return r, g, b, self._alpha_float()
- elif alpha:
- return r, g, b, self._alpha_float()
- else:
- # alpha is False
+ if alpha is None and self._rgba.alpha is None or alpha is not None and not alpha:
return r, g, b
+ else:
+ return r, g, b, self._alpha_float()
def as_hsl(self) -> str:
"""
@@ -225,11 +218,7 @@ class Color(_repr.Representation):
return h, s, l
else:
return h, s, l, self._alpha_float()
- if alpha:
- return h, s, l, self._alpha_float()
- else:
- # alpha is False
- return h, s, l
+ return (h, s, l, self._alpha_float()) if alpha else (h, s, l)
def _alpha_float(self) -> float:
return 1 if self._rgba.alpha is None else self._rgba.alpha
@@ -315,20 +304,14 @@ def parse_str(value: str) -> RGBA:
if m:
*rgb, a = m.groups()
r, g, b = (int(v * 2, 16) for v in rgb)
- if a:
- alpha: float | None = int(a * 2, 16) / 255
- else:
- alpha = None
+ alpha = int(a * 2, 16) / 255 if a else None
return ints_to_rgba(r, g, b, alpha)
m = re.fullmatch(r_hex_long, value_lower)
if m:
*rgb, a = m.groups()
r, g, b = (int(v, 16) for v in rgb)
- if a:
- alpha = int(a, 16) / 255
- else:
- alpha = None
+ alpha = int(a, 16) / 255 if a else None
return ints_to_rgba(r, g, b, alpha)
m = re.fullmatch(r_rgb, value_lower) or re.fullmatch(r_rgb_v4_style, value_lower)
@@ -390,11 +373,11 @@ def parse_color_value(value: int | str, max_val: int = 255) -> float:
"""
try:
color = float(value)
- except ValueError:
+ except ValueError as e:
raise PydanticCustomError(
'color_error',
'value is not a valid color: color values must be a valid number',
- )
+ ) from e
if 0 <= color <= max_val:
return color / max_val
else:
@@ -425,11 +408,11 @@ def parse_float_alpha(value: None | str | float | int) -> float | None:
alpha = float(value[:-1]) / 100
else:
alpha = float(value)
- except ValueError:
+ except ValueError as e:
raise PydanticCustomError(
'color_error',
'value is not a valid color: alpha values must be a valid float',
- )
+ ) from e
if math.isclose(alpha, 1):
return None
@@ -465,7 +448,7 @@ def parse_hsl(h: str, h_units: str, sat: str, light: str, alpha: float | None =
h_value = h_value % rads / rads
else:
# turns
- h_value = h_value % 1
+ h_value %= 1
r, g, b = hls_to_rgb(h_value, l_value, s_value)
return RGBA(r, g, b, parse_float_alpha(alpha))
diff --git a/pydantic_extra_types/coordinate.py b/pydantic_extra_types/coordinate.py
index 10eaa05..29d5892 100644
--- a/pydantic_extra_types/coordinate.py
+++ b/pydantic_extra_types/coordinate.py
@@ -123,18 +123,16 @@ class Coordinate(_repr.Representation):
return value
try:
value = tuple(float(x) for x in value.split(','))
- except ValueError:
+ except ValueError as e:
raise PydanticCustomError(
'coordinate_error',
'value is not a valid coordinate: string is not recognized as a valid coordinate',
- )
+ ) from e
return ArgsKwargs(args=value)
@classmethod
def _parse_tuple(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> Any:
- if not isinstance(value, tuple):
- return value
- return ArgsKwargs(args=handler(value))
+ return ArgsKwargs(args=handler(value)) if isinstance(value, tuple) else value
def __str__(self) -> str:
return f'{self.latitude},{self.longitude}'
diff --git a/pydantic_extra_types/country.py b/pydantic_extra_types/country.py
index 7af99c7..c449b34 100644
--- a/pydantic_extra_types/country.py
+++ b/pydantic_extra_types/country.py
@@ -13,10 +13,10 @@ from pydantic_core import PydanticCustomError, core_schema
try:
import pycountry
-except ModuleNotFoundError: # pragma: no cover
+except ModuleNotFoundError as e: # pragma: no cover
raise RuntimeError(
'The `country` module requires "pycountry" to be installed. You can install it with "pip install pycountry".'
- )
+ ) from e
@dataclass
diff --git a/pydantic_extra_types/currency_code.py b/pydantic_extra_types/currency_code.py
index fbc0cbd..3c8e0a1 100644
--- a/pydantic_extra_types/currency_code.py
+++ b/pydantic_extra_types/currency_code.py
@@ -11,11 +11,11 @@ from pydantic_core import PydanticCustomError, core_schema
try:
import pycountry
-except ModuleNotFoundError: # pragma: no cover
+except ModuleNotFoundError as e: # pragma: no cover
raise RuntimeError(
'The `currency_code` module requires "pycountry" to be installed. You can install it with "pip install '
'pycountry".'
- )
+ ) from e
# List of codes that should not be usually used within regular transactions
_CODES_FOR_BONDS_METAL_TESTING = {
diff --git a/pydantic_extra_types/language_code.py b/pydantic_extra_types/language_code.py
index 8c15385..fb033b8 100644
--- a/pydantic_extra_types/language_code.py
+++ b/pydantic_extra_types/language_code.py
@@ -13,11 +13,11 @@ from pydantic_core import PydanticCustomError, core_schema
try:
import pycountry
-except ModuleNotFoundError: # pragma: no cover
+except ModuleNotFoundError as e: # pragma: no cover
raise RuntimeError(
'The `language_code` module requires "pycountry" to be installed.'
' You can install it with "pip install pycountry".'
- )
+ ) from e
@dataclass
diff --git a/pydantic_extra_types/pendulum_dt.py b/pydantic_extra_types/pendulum_dt.py
index f3a304f..633b02d 100644
--- a/pydantic_extra_types/pendulum_dt.py
+++ b/pydantic_extra_types/pendulum_dt.py
@@ -3,15 +3,18 @@ Native Pendulum DateTime object implementation. This is a copy of the Pendulum D
CoreSchema implementation. This allows Pydantic to validate the DateTime object.
"""
+import pendulum
+
try:
from pendulum import Date as _Date
from pendulum import DateTime as _DateTime
from pendulum import Duration as _Duration
from pendulum import parse
-except ModuleNotFoundError: # pragma: no cover
+except ModuleNotFoundError as e: # pragma: no cover
raise RuntimeError(
'The `pendulum_dt` module requires "pendulum" to be installed. You can install it with "pip install pendulum".'
- )
+ ) from e
+from datetime import date, datetime, timedelta
from typing import Any, List, Type
from pydantic import GetCoreSchemaHandler
@@ -68,6 +71,9 @@ class DateTime(_DateTime):
if isinstance(value, _DateTime):
return handler(value)
+ if isinstance(value, datetime):
+ return handler(DateTime.instance(value))
+
# otherwise, parse it.
try:
data = parse(value)
@@ -126,6 +132,9 @@ class Date(_Date):
if isinstance(value, _Date):
return handler(value)
+ if isinstance(value, date):
+ return handler(pendulum.instance(value))
+
# otherwise, parse it.
try:
data = parse(value)
@@ -184,6 +193,9 @@ class Duration(_Duration):
if isinstance(value, _Duration):
return handler(value)
+ if isinstance(value, timedelta):
+ return handler(_Duration(seconds=value.total_seconds()))
+
# otherwise, parse it.
try:
data = parse(value)
diff --git a/pydantic_extra_types/phone_numbers.py b/pydantic_extra_types/phone_numbers.py
index cf03417..1fb9678 100644
--- a/pydantic_extra_types/phone_numbers.py
+++ b/pydantic_extra_types/phone_numbers.py
@@ -14,10 +14,10 @@ from pydantic_core import PydanticCustomError, core_schema
try:
import phonenumbers
-except ModuleNotFoundError: # pragma: no cover
+except ModuleNotFoundError as e: # pragma: no cover
raise RuntimeError(
'`PhoneNumber` requires "phonenumbers" to be installed. You can install it with "pip install phonenumbers"'
- )
+ ) from e
GeneratorCallableStr = Generator[Callable[..., str], None, None]
@@ -67,3 +67,9 @@ class PhoneNumber(str):
raise PydanticCustomError('value_error', 'value is not a valid phone number')
return phonenumbers.format_number(parsed_number, getattr(phonenumbers.PhoneNumberFormat, cls.phone_format))
+
+ def __eq__(self, other: Any) -> bool:
+ return super().__eq__(other)
+
+ def __hash__(self) -> int:
+ return super().__hash__()
diff --git a/pydantic_extra_types/script_code.py b/pydantic_extra_types/script_code.py
new file mode 100644
index 0000000..eea44f0
--- /dev/null
+++ b/pydantic_extra_types/script_code.py
@@ -0,0 +1,100 @@
+"""
+script definitions that are based on the [ISO 15924](https://en.wikipedia.org/wiki/ISO_15924)
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
+from pydantic_core import PydanticCustomError, core_schema
+
+try:
+ import pycountry
+except ModuleNotFoundError as e: # pragma: no cover
+ raise RuntimeError(
+ 'The `script_code` module requires "pycountry" to be installed.'
+ ' You can install it with "pip install pycountry".'
+ ) from e
+
+
+class ISO_15924(str):
+ """ISO_15924 parses script in the [ISO 15924](https://en.wikipedia.org/wiki/ISO_15924)
+ format.
+
+ ```py
+ from pydantic import BaseModel
+
+ from pydantic_extra_types.language_code import ISO_15924
+
+ class Script(BaseModel):
+ alpha_4: ISO_15924
+
+ script = Script(alpha_4='Java')
+ print(lang)
+ # > script='Java'
+ ```
+ """
+
+ allowed_values_list = [script.alpha_4 for script in pycountry.scripts]
+ allowed_values = set(allowed_values_list)
+
+ @classmethod
+ def _validate(cls, __input_value: str, _: core_schema.ValidationInfo) -> ISO_15924:
+ """
+ Validate a ISO 15924 language code from the provided str value.
+
+ Args:
+ __input_value: The str value to be validated.
+ _: The Pydantic ValidationInfo.
+
+ Returns:
+ The validated ISO 15924 script code.
+
+ Raises:
+ PydanticCustomError: If the ISO 15924 script code is not valid.
+ """
+ if __input_value not in cls.allowed_values:
+ raise PydanticCustomError(
+ 'ISO_15924', 'Invalid ISO 15924 script code. See https://en.wikipedia.org/wiki/ISO_15924'
+ )
+ return cls(__input_value)
+
+ @classmethod
+ def __get_pydantic_core_schema__(
+ cls, _: type[Any], __: GetCoreSchemaHandler
+ ) -> core_schema.AfterValidatorFunctionSchema:
+ """
+ Return a Pydantic CoreSchema with the ISO 639-3 language code validation.
+
+ Args:
+ _: The source type.
+ __: The handler to get the CoreSchema.
+
+ Returns:
+ A Pydantic CoreSchema with the ISO 639-3 language code validation.
+
+ """
+ return core_schema.with_info_after_validator_function(
+ cls._validate,
+ core_schema.str_schema(min_length=4, max_length=4),
+ )
+
+ @classmethod
+ def __get_pydantic_json_schema__(
+ cls, schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
+ ) -> dict[str, Any]:
+ """
+ Return a Pydantic JSON Schema with the ISO 639-3 language code validation.
+
+ Args:
+ schema: The Pydantic CoreSchema.
+ handler: The handler to get the JSON Schema.
+
+ Returns:
+ A Pydantic JSON Schema with the ISO 639-3 language code validation.
+
+ """
+ json_schema = handler(schema)
+ json_schema.update({'enum': cls.allowed_values_list})
+ return json_schema
diff --git a/pydantic_extra_types/ulid.py b/pydantic_extra_types/ulid.py
index 5891f9f..3173b4d 100644
--- a/pydantic_extra_types/ulid.py
+++ b/pydantic_extra_types/ulid.py
@@ -15,10 +15,10 @@ from pydantic_core import PydanticCustomError, core_schema
try:
from ulid import ULID as _ULID
-except ModuleNotFoundError: # pragma: no cover
+except ModuleNotFoundError as e: # pragma: no cover
raise RuntimeError(
'The `ulid` module requires "python-ulid" to be installed. You can install it with "pip install python-ulid".'
- )
+ ) from e
UlidType = Union[str, bytes, int]
@@ -58,6 +58,6 @@ class ULID(_repr.Representation):
ulid = value
else:
ulid = _ULID.from_bytes(value)
- except ValueError:
- raise PydanticCustomError('ulid_format', 'Unrecognized format')
+ except ValueError as e:
+ raise PydanticCustomError('ulid_format', 'Unrecognized format') from e
return handler(ulid)
diff --git a/pyproject.toml b/pyproject.toml
index d713e4d..5ccb844 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -51,6 +51,13 @@ all = [
'python-ulid>=1,<3; python_version>="3.9"',
'pendulum>=3.0.0,<4.0.0'
]
+phonenumbers = ['phonenumbers>=8,<9']
+pycountry = ['pycountry>=23']
+python_ulid = [
+ 'python-ulid>=1,<2; python_version<"3.9"',
+ 'python-ulid>=1,<3; python_version>="3.9"',
+]
+pendulum = ['pendulum>=3.0.0,<4.0.0']
[project.urls]
Homepage = 'https://github.com/pydantic/pydantic-extra-types'
diff --git a/requirements/linting.txt b/requirements/linting.txt
index 07ccbb2..1fdcce5 100644
--- a/requirements/linting.txt
+++ b/requirements/linting.txt
@@ -4,7 +4,7 @@
#
# pip-compile --no-emit-index-url --output-file=requirements/linting.txt requirements/linting.in
#
-annotated-types==0.6.0
+annotated-types==0.7.0
# via -r requirements/linting.in
cfgv==3.4.0
# via pre-commit
@@ -14,7 +14,7 @@ filelock==3.13.1
# via virtualenv
identify==2.5.35
# via pre-commit
-mypy==1.9.0
+mypy==1.10.0
# via -r requirements/linting.in
mypy-extensions==1.0.0
# via mypy
@@ -22,11 +22,11 @@ nodeenv==1.8.0
# via pre-commit
platformdirs==4.2.0
# via virtualenv
-pre-commit==3.7.0
+pre-commit==3.7.1
# via -r requirements/linting.in
pyyaml==6.0.1
# via pre-commit
-ruff==0.3.5
+ruff==0.4.7
# via -r requirements/linting.in
typing-extensions==4.10.0
# via mypy
diff --git a/requirements/testing.txt b/requirements/testing.txt
index 37e7c23..72d9dfb 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -10,7 +10,7 @@ charset-normalizer==3.3.2
# via requests
codecov==2.1.13
# via -r requirements/testing.in
-coverage[toml]==7.4.4
+coverage[toml]==7.5.3
# via
# -r requirements/testing.in
# codecov
@@ -27,11 +27,11 @@ mdurl==0.1.2
# via markdown-it-py
packaging==23.2
# via pytest
-pluggy==1.4.0
+pluggy==1.5.0
# via pytest
pygments==2.17.2
# via rich
-pytest==8.1.1
+pytest==8.2.1
# via
# -r requirements/testing.in
# pytest-cov
diff --git a/tests/test_json_schema.py b/tests/test_json_schema.py
index d0798bc..43ad932 100644
--- a/tests/test_json_schema.py
+++ b/tests/test_json_schema.py
@@ -17,6 +17,7 @@ from pydantic_extra_types.language_code import ISO639_3, ISO639_5, LanguageAlpha
from pydantic_extra_types.mac_address import MacAddress
from pydantic_extra_types.payment import PaymentCardNumber
from pydantic_extra_types.pendulum_dt import DateTime
+from pydantic_extra_types.script_code import ISO_15924
from pydantic_extra_types.ulid import ULID
languages = [lang.alpha_3 for lang in pycountry.languages]
@@ -32,6 +33,8 @@ everyday_currencies = [
if currency.alpha_3 not in pydantic_extra_types.currency_code._CODES_FOR_BONDS_METAL_TESTING
]
+scripts = [script.alpha_4 for script in pycountry.scripts]
+
everyday_currencies.sort()
@@ -305,6 +308,23 @@ everyday_currencies.sort()
'type': 'object',
},
),
+ (
+ ISO_15924,
+ {
+ 'properties': {
+ 'x': {
+ 'title': 'X',
+ 'type': 'string',
+ 'enum': scripts,
+ 'maxLength': 4,
+ 'minLength': 4,
+ }
+ },
+ 'required': ['x'],
+ 'title': 'Model',
+ 'type': 'object',
+ },
+ ),
],
)
def test_json_schema(cls, expected):
diff --git a/tests/test_pendulum_dt.py b/tests/test_pendulum_dt.py
index 18ec5ef..2cd0545 100644
--- a/tests/test_pendulum_dt.py
+++ b/tests/test_pendulum_dt.py
@@ -1,9 +1,14 @@
+from datetime import date, datetime, timedelta
+from datetime import timezone as tz
+
import pendulum
import pytest
from pydantic import BaseModel, ValidationError
from pydantic_extra_types.pendulum_dt import Date, DateTime, Duration
+UTC = tz.utc
+
class DtModel(BaseModel):
dt: DateTime
@@ -17,32 +22,77 @@ class DurationModel(BaseModel):
delta_t: Duration
-def test_pendulum_dt_existing_instance():
+@pytest.mark.parametrize(
+ 'instance',
+ [
+ pendulum.now(),
+ datetime.now(),
+ datetime.now(UTC),
+ ],
+)
+def test_existing_instance(instance):
"""
Verifies that constructing a model with an existing pendulum dt doesn't throw.
"""
- now = pendulum.now()
- model = DtModel(dt=now)
- assert model.dt == now
+ model = DtModel(dt=instance)
+ if isinstance(instance, datetime):
+ assert model.dt == pendulum.instance(instance)
+ if instance.tzinfo is None and isinstance(instance, datetime):
+ instance = model.dt.replace(tzinfo=UTC) # pendulum defaults to UTC
+ dt = model.dt
+ else:
+ assert model.dt == instance
+ dt = model.dt
+
+ assert dt.day == instance.day
+ assert dt.month == instance.month
+ assert dt.year == instance.year
+ assert dt.hour == instance.hour
+ assert dt.minute == instance.minute
+ assert dt.second == instance.second
+ assert dt.microsecond == instance.microsecond
+ if dt.tzinfo != instance.tzinfo:
+ assert dt.tzinfo.utcoffset(dt) == instance.tzinfo.utcoffset(instance)
-def test_pendulum_date_existing_instance():
+@pytest.mark.parametrize(
+ 'instance',
+ [
+ pendulum.today(),
+ date.today(),
+ ],
+)
+def test_pendulum_date_existing_instance(instance):
"""
Verifies that constructing a model with an existing pendulum date doesn't throw.
"""
- today = pendulum.today().date()
- model = DateModel(d=today)
- assert model.d == today
+ model = DateModel(d=instance)
+ if isinstance(instance, datetime):
+ assert model.d == pendulum.instance(instance).date()
+ else:
+ assert model.d == instance
+ d = model.d
+ assert d.day == instance.day
+ assert d.month == instance.month
+ assert d.year == instance.year
-def test_pendulum_duration_existing_instance():
+@pytest.mark.parametrize(
+ 'instance',
+ [
+ pendulum.duration(days=42, hours=13, minutes=37),
+ pendulum.duration(days=-42, hours=13, minutes=37),
+ timedelta(days=42, hours=13, minutes=37),
+ timedelta(days=-42, hours=13, minutes=37),
+ ],
+)
+def test_duration_timedelta__existing_instance(instance):
"""
Verifies that constructing a model with an existing pendulum duration doesn't throw.
"""
- delta_t = pendulum.duration(days=42, hours=13, minutes=37)
- model = DurationModel(delta_t=delta_t)
+ model = DurationModel(delta_t=instance)
- assert model.delta_t.total_seconds() == delta_t.total_seconds()
+ assert model.delta_t.total_seconds() == instance.total_seconds()
@pytest.mark.parametrize(
diff --git a/tests/test_phone_numbers.py b/tests/test_phone_numbers.py
index 1d3a105..d2b8570 100644
--- a/tests/test_phone_numbers.py
+++ b/tests/test_phone_numbers.py
@@ -52,6 +52,20 @@ def test_parsed_but_not_a_valid_number() -> None:
Something(phone_number='+1 555-1212')
+def test_hashes() -> None:
+ assert hash(PhoneNumber('555-1212')) == hash(PhoneNumber('555-1212'))
+ assert hash(PhoneNumber('555-1212')) == hash('555-1212')
+ assert hash(PhoneNumber('555-1212')) != hash('555-1213')
+ assert hash(PhoneNumber('555-1212')) != hash(PhoneNumber('555-1213'))
+
+
+def test_eq() -> None:
+ assert PhoneNumber('555-1212') == PhoneNumber('555-1212')
+ assert PhoneNumber('555-1212') == '555-1212'
+ assert PhoneNumber('555-1212') != '555-1213'
+ assert PhoneNumber('555-1212') != PhoneNumber('555-1213')
+
+
def test_json_schema() -> None:
assert Something.model_json_schema() == {
'title': 'Something',
diff --git a/tests/test_scripts.py b/tests/test_scripts.py
new file mode 100644
index 0000000..6bf099f
--- /dev/null
+++ b/tests/test_scripts.py
@@ -0,0 +1,53 @@
+import re
+
+import pycountry
+import pytest
+from pydantic import BaseModel, ValidationError
+
+from pydantic_extra_types.script_code import ISO_15924
+
+
+class ScriptCheck(BaseModel):
+ script: ISO_15924
+
+
+@pytest.mark.parametrize('script', map(lambda lang: lang.alpha_4, pycountry.scripts))
+def test_ISO_15924_code_ok(script: str):
+ model = ScriptCheck(script=script)
+ assert model.script == script
+ assert str(model.script) == script
+ assert model.model_dump() == {'script': script} # test serialization
+
+
+def test_ISO_15924_code_fail_not_enought_letters():
+ with pytest.raises(
+ ValidationError,
+ match=re.escape(
+ '1 validation error for ScriptCheck\nscript\n '
+ "String should have at least 4 characters [type=string_too_short, input_value='X', input_type=str]\n"
+ ),
+ ):
+ ScriptCheck(script='X')
+
+
+def test_ISO_15924_code_fail_too_much_letters():
+ with pytest.raises(
+ ValidationError,
+ match=re.escape(
+ '1 validation error for ScriptCheck\nscript\n '
+ "String should have at most 4 characters [type=string_too_long, input_value='Klingon', input_type=str]"
+ ),
+ ):
+ ScriptCheck(script='Klingon')
+
+
+def test_ISO_15924_code_fail_not_existing():
+ with pytest.raises(
+ ValidationError,
+ match=re.escape(
+ '1 validation error for ScriptCheck\nscript\n '
+ 'Invalid ISO 15924 script code. See https://en.wikipedia.org/wiki/ISO_15924 '
+ "[type=ISO_15924, input_value='Klin', input_type=str]"
+ ),
+ ):
+ ScriptCheck(script='Klin')