diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-11 16:31:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-11 16:31:27 +0000 |
commit | 54f3ee9b64816917315724a16b68de7f9e24503f (patch) | |
tree | e307266b48d594d334bcb9d5c3d004cd6bcc1cd6 | |
parent | Releasing debian version 2.7.0-1. (diff) | |
download | pydantic-extra-types-54f3ee9b64816917315724a16b68de7f9e24503f.tar.xz pydantic-extra-types-54f3ee9b64816917315724a16b68de7f9e24503f.zip |
Merging upstream version 2.8.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r-- | HISTORY.md | 21 | ||||
-rw-r--r-- | pydantic_extra_types/__init__.py | 2 | ||||
-rw-r--r-- | pydantic_extra_types/color.py | 59 | ||||
-rw-r--r-- | pydantic_extra_types/coordinate.py | 8 | ||||
-rw-r--r-- | pydantic_extra_types/country.py | 4 | ||||
-rw-r--r-- | pydantic_extra_types/currency_code.py | 4 | ||||
-rw-r--r-- | pydantic_extra_types/language_code.py | 4 | ||||
-rw-r--r-- | pydantic_extra_types/pendulum_dt.py | 16 | ||||
-rw-r--r-- | pydantic_extra_types/phone_numbers.py | 10 | ||||
-rw-r--r-- | pydantic_extra_types/script_code.py | 100 | ||||
-rw-r--r-- | pydantic_extra_types/ulid.py | 8 | ||||
-rw-r--r-- | pyproject.toml | 7 | ||||
-rw-r--r-- | requirements/linting.txt | 8 | ||||
-rw-r--r-- | requirements/testing.txt | 6 | ||||
-rw-r--r-- | tests/test_json_schema.py | 20 | ||||
-rw-r--r-- | tests/test_pendulum_dt.py | 74 | ||||
-rw-r--r-- | tests/test_phone_numbers.py | 14 | ||||
-rw-r--r-- | tests/test_scripts.py | 53 |
18 files changed, 341 insertions, 77 deletions
@@ -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') |