Merging upstream version 2.9.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
f1720b9d27
commit
e6be59280f
13 changed files with 542 additions and 14 deletions
15
HISTORY.md
15
HISTORY.md
|
@ -2,6 +2,21 @@
|
|||
|
||||
## Latest Changes
|
||||
|
||||
## 2.9.0
|
||||
|
||||
### Types
|
||||
|
||||
* Add Semantic version type. PR [#195](https://github.com/pydantic/pydantic-extra-types/pull/195) by [@nikstuckenbrock](https://github.com/nikstuckenbrock)
|
||||
* Add timezone name validation. PR [#193](https://github.com/pydantic/pydantic-extra-types/pull/193) by [@07pepa](https://github.com/07pepa)
|
||||
|
||||
### Refactor
|
||||
|
||||
* Replace try-except block by if-else statement. PR [#192](https://github.com/pydantic/pydantic-extra-types/pull/192) by [@maxsos](https://github.com/maxsos)
|
||||
|
||||
### Dependencies
|
||||
|
||||
* ⬆ Bump the python-packages group with 4 updates. PR [#194](https://github.com/pydantic/pydantic-extra-types/pull/194) by @dependabot
|
||||
|
||||
## 2.8.2
|
||||
|
||||
* 🐛 Preserve timezone information when validating Pendulum DateTimes. [#189](https://github.com/pydantic/pydantic-extra-types/pull/189) by [@chrisguidry
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = '2.8.2'
|
||||
__version__ = '2.9.0'
|
||||
|
|
|
@ -124,13 +124,14 @@ 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())
|
||||
try:
|
||||
|
||||
if rgb in COLORS_BY_VALUE:
|
||||
return COLORS_BY_VALUE[rgb]
|
||||
except KeyError as e:
|
||||
else:
|
||||
if fallback:
|
||||
return self.as_hex()
|
||||
else:
|
||||
raise ValueError('no named color found, use fallback=True, as_hex() or as_rgb()') from e
|
||||
raise ValueError('no named color found, use fallback=True, as_hex() or as_rgb()')
|
||||
|
||||
def as_hex(self, format: Literal['short', 'long'] = 'short') -> str:
|
||||
"""Returns the hexadecimal representation of the color.
|
||||
|
@ -292,12 +293,10 @@ def parse_str(value: str) -> RGBA:
|
|||
Raises:
|
||||
ValueError: If the input string cannot be parsed to an RGBA tuple.
|
||||
"""
|
||||
|
||||
value_lower = value.lower()
|
||||
try:
|
||||
if value_lower in COLORS_BY_NAME:
|
||||
r, g, b = COLORS_BY_NAME[value_lower]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
return ints_to_rgba(r, g, b, None)
|
||||
|
||||
m = re.fullmatch(r_hex_short, value_lower)
|
||||
|
|
55
pydantic_extra_types/semantic_version.py
Normal file
55
pydantic_extra_types/semantic_version.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
"""
|
||||
SemanticVersion definition that is based on the Semantiv Versioning Specification [semver](https://semver.org/).
|
||||
"""
|
||||
|
||||
from typing import Any, Callable
|
||||
|
||||
from pydantic import GetJsonSchemaHandler
|
||||
from pydantic.json_schema import JsonSchemaValue
|
||||
from pydantic_core import core_schema
|
||||
|
||||
try:
|
||||
import semver
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise RuntimeError(
|
||||
'The `semantic_version` module requires "semver" to be installed. You can install it with "pip install semver".'
|
||||
) from e
|
||||
|
||||
|
||||
class SemanticVersion:
|
||||
"""
|
||||
Semantic version based on the official [semver thread](https://python-semver.readthedocs.io/en/latest/advanced/combine-pydantic-and-semver.html).
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls,
|
||||
_source_type: Any,
|
||||
_handler: Callable[[Any], core_schema.CoreSchema],
|
||||
) -> core_schema.CoreSchema:
|
||||
def validate_from_str(value: str) -> semver.Version:
|
||||
return semver.Version.parse(value)
|
||||
|
||||
from_str_schema = core_schema.chain_schema(
|
||||
[
|
||||
core_schema.str_schema(),
|
||||
core_schema.no_info_plain_validator_function(validate_from_str),
|
||||
]
|
||||
)
|
||||
|
||||
return core_schema.json_or_python_schema(
|
||||
json_schema=from_str_schema,
|
||||
python_schema=core_schema.union_schema(
|
||||
[
|
||||
core_schema.is_instance_schema(semver.Version),
|
||||
from_str_schema,
|
||||
]
|
||||
),
|
||||
serialization=core_schema.to_string_ser_schema(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_json_schema__(
|
||||
cls, _core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
|
||||
) -> JsonSchemaValue:
|
||||
return handler(core_schema.str_schema())
|
189
pydantic_extra_types/timezone_name.py
Normal file
189
pydantic_extra_types/timezone_name.py
Normal file
|
@ -0,0 +1,189 @@
|
|||
"""Time zone name validation and serialization module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
import warnings
|
||||
from typing import Any, Callable, List, Set, Type, cast
|
||||
|
||||
from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
|
||||
from pydantic_core import PydanticCustomError, core_schema
|
||||
|
||||
|
||||
def _is_available(name: str) -> bool:
|
||||
"""Check if a module is available for import."""
|
||||
try:
|
||||
importlib.import_module(name)
|
||||
return True
|
||||
except ModuleNotFoundError: # pragma: no cover
|
||||
return False
|
||||
|
||||
|
||||
def _tz_provider_from_zone_info() -> Set[str]: # pragma: no cover
|
||||
"""Get timezones from the zoneinfo module."""
|
||||
from zoneinfo import available_timezones
|
||||
|
||||
return set(available_timezones())
|
||||
|
||||
|
||||
def _tz_provider_from_pytz() -> Set[str]: # pragma: no cover
|
||||
"""Get timezones from the pytz module."""
|
||||
from pytz import all_timezones
|
||||
|
||||
return set(all_timezones)
|
||||
|
||||
|
||||
def _warn_about_pytz_usage() -> None:
|
||||
"""Warn about using pytz with Python 3.9 or later."""
|
||||
warnings.warn( # pragma: no cover
|
||||
'Projects using Python 3.9 or later should be using the support now included as part of the standard library. '
|
||||
'Please consider switching to the standard library (zoneinfo) module.'
|
||||
)
|
||||
|
||||
|
||||
def get_timezones() -> Set[str]:
|
||||
"""Determine the timezone provider and return available timezones."""
|
||||
if _is_available('zoneinfo') and _is_available('tzdata'): # pragma: no cover
|
||||
return _tz_provider_from_zone_info()
|
||||
elif _is_available('pytz'): # pragma: no cover
|
||||
if sys.version_info[:2] > (3, 8):
|
||||
_warn_about_pytz_usage()
|
||||
return _tz_provider_from_pytz()
|
||||
else: # pragma: no cover
|
||||
if sys.version_info[:2] == (3, 8):
|
||||
raise ImportError('No pytz module found. Please install it with "pip install pytz"')
|
||||
raise ImportError('No timezone provider found. Please install tzdata with "pip install tzdata"')
|
||||
|
||||
|
||||
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))
|
||||
|
||||
def __init__(cls, name: str, bases: tuple[type, ...], dct: dict[str, Any], **kwargs: Any) -> None:
|
||||
super().__init__(name, bases, dct)
|
||||
cls.strict = kwargs.get('strict', True)
|
||||
|
||||
|
||||
def timezone_name_settings(**kwargs: Any) -> Callable[[Type[TimeZoneName]], Type[TimeZoneName]]:
|
||||
def wrapper(cls: Type[TimeZoneName]) -> Type[TimeZoneName]:
|
||||
cls.strict = kwargs.get('strict', True)
|
||||
return cls
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@timezone_name_settings(strict=True)
|
||||
class TimeZoneName(str):
|
||||
"""
|
||||
TimeZoneName is a custom string subclass for validating and serializing timezone names.
|
||||
|
||||
The TimeZoneName class uses the IANA Time Zone Database for validation.
|
||||
It supports both strict and non-strict modes for timezone name validation.
|
||||
|
||||
|
||||
## Examples:
|
||||
|
||||
Some examples of using the TimeZoneName class:
|
||||
|
||||
### Normal usage:
|
||||
|
||||
```python
|
||||
from pydantic_extra_types.timezone_name import TimeZoneName
|
||||
from pydantic import BaseModel
|
||||
class Location(BaseModel):
|
||||
city: str
|
||||
timezone: TimeZoneName
|
||||
|
||||
loc = Location(city="New York", timezone="America/New_York")
|
||||
print(loc.timezone)
|
||||
|
||||
>> America/New_York
|
||||
|
||||
```
|
||||
|
||||
### Non-strict mode:
|
||||
|
||||
```python
|
||||
|
||||
from pydantic_extra_types.timezone_name import TimeZoneName, timezone_name_settings
|
||||
|
||||
@timezone_name_settings(strict=False)
|
||||
class TZNonStrict(TimeZoneName):
|
||||
pass
|
||||
|
||||
tz = TZNonStrict("america/new_york")
|
||||
|
||||
print(tz)
|
||||
|
||||
>> america/new_york
|
||||
|
||||
```
|
||||
"""
|
||||
|
||||
__slots__: List[str] = []
|
||||
allowed_values: Set[str] = set(get_timezones())
|
||||
allowed_values_list: List[str] = sorted(allowed_values)
|
||||
allowed_values_upper_to_correct: dict[str, str] = {val.upper(): val for val in allowed_values}
|
||||
strict: bool
|
||||
|
||||
@classmethod
|
||||
def _validate(cls, __input_value: str, _: core_schema.ValidationInfo) -> TimeZoneName:
|
||||
"""
|
||||
Validate a time zone name from the provided str value.
|
||||
|
||||
Args:
|
||||
__input_value: The str value to be validated.
|
||||
_: The Pydantic ValidationInfo.
|
||||
|
||||
Returns:
|
||||
The validated time zone name.
|
||||
|
||||
Raises:
|
||||
PydanticCustomError: If the timezone name is not valid.
|
||||
"""
|
||||
if __input_value not in cls.allowed_values: # be fast for the most common case
|
||||
if not cls.strict:
|
||||
upper_value = __input_value.strip().upper()
|
||||
if upper_value in cls.allowed_values_upper_to_correct:
|
||||
return cls(cls.allowed_values_upper_to_correct[upper_value])
|
||||
raise PydanticCustomError('TimeZoneName', 'Invalid timezone name.')
|
||||
return cls(__input_value)
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls, _: Type[Any], __: GetCoreSchemaHandler
|
||||
) -> core_schema.AfterValidatorFunctionSchema:
|
||||
"""
|
||||
Return a Pydantic CoreSchema with the timezone name validation.
|
||||
|
||||
Args:
|
||||
_: The source type.
|
||||
__: The handler to get the CoreSchema.
|
||||
|
||||
Returns:
|
||||
A Pydantic CoreSchema with the timezone name validation.
|
||||
"""
|
||||
return core_schema.with_info_after_validator_function(
|
||||
cls._validate,
|
||||
core_schema.str_schema(min_length=1),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_json_schema__(
|
||||
cls, schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Return a Pydantic JSON Schema with the timezone name validation.
|
||||
|
||||
Args:
|
||||
schema: The Pydantic CoreSchema.
|
||||
handler: The handler to get the JSON Schema.
|
||||
|
||||
Returns:
|
||||
A Pydantic JSON Schema with the timezone name validation.
|
||||
"""
|
||||
json_schema = handler(schema)
|
||||
json_schema.update({'enum': cls.allowed_values_list})
|
||||
return json_schema
|
|
@ -47,12 +47,16 @@ dynamic = ['version']
|
|||
all = [
|
||||
'phonenumbers>=8,<9',
|
||||
'pycountry>=23',
|
||||
'semver>=3.0.2',
|
||||
'python-ulid>=1,<2; python_version<"3.9"',
|
||||
'python-ulid>=1,<3; python_version>="3.9"',
|
||||
'pendulum>=3.0.0,<4.0.0'
|
||||
'pendulum>=3.0.0,<4.0.0',
|
||||
'pytz>=2024.1',
|
||||
'tzdata>=2024.1',
|
||||
]
|
||||
phonenumbers = ['phonenumbers>=8,<9']
|
||||
pycountry = ['pycountry>=23']
|
||||
semver = ['semver>=3.0.2']
|
||||
python_ulid = [
|
||||
'python-ulid>=1,<2; python_version<"3.9"',
|
||||
'python-ulid>=1,<3; python_version>="3.9"',
|
||||
|
|
|
@ -2,3 +2,4 @@ pre-commit
|
|||
mypy
|
||||
annotated-types
|
||||
ruff
|
||||
types-pytz
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.11
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --no-emit-index-url --output-file=requirements/linting.txt requirements/linting.in
|
||||
|
@ -14,7 +14,7 @@ filelock==3.13.1
|
|||
# via virtualenv
|
||||
identify==2.5.35
|
||||
# via pre-commit
|
||||
mypy==1.10.0
|
||||
mypy==1.10.1
|
||||
# via -r requirements/linting.in
|
||||
mypy-extensions==1.0.0
|
||||
# via mypy
|
||||
|
@ -26,7 +26,9 @@ pre-commit==3.7.1
|
|||
# via -r requirements/linting.in
|
||||
pyyaml==6.0.1
|
||||
# via pre-commit
|
||||
ruff==0.4.7
|
||||
ruff==0.5.0
|
||||
# via -r requirements/linting.in
|
||||
types-pytz==2024.1.0.20240417
|
||||
# via -r requirements/linting.in
|
||||
typing-extensions==4.10.0
|
||||
# via mypy
|
||||
|
|
|
@ -22,6 +22,8 @@ python-dateutil==2.8.2
|
|||
# time-machine
|
||||
python-ulid==1.1.0
|
||||
# via pydantic-extra-types (pyproject.toml)
|
||||
semver==3.0.2
|
||||
# via pydantic-extra-types (pyproject.toml)
|
||||
six==1.16.0
|
||||
# via python-dateutil
|
||||
time-machine==2.13.0
|
||||
|
|
|
@ -10,7 +10,7 @@ charset-normalizer==3.3.2
|
|||
# via requests
|
||||
codecov==2.1.13
|
||||
# via -r requirements/testing.in
|
||||
coverage[toml]==7.5.3
|
||||
coverage[toml]==7.5.4
|
||||
# via
|
||||
# -r requirements/testing.in
|
||||
# codecov
|
||||
|
@ -31,7 +31,7 @@ pluggy==1.5.0
|
|||
# via pytest
|
||||
pygments==2.17.2
|
||||
# via rich
|
||||
pytest==8.2.1
|
||||
pytest==8.2.2
|
||||
# via
|
||||
# -r requirements/testing.in
|
||||
# pytest-cov
|
||||
|
|
|
@ -18,6 +18,8 @@ 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.semantic_version import SemanticVersion
|
||||
from pydantic_extra_types.timezone_name import TimeZoneName
|
||||
from pydantic_extra_types.ulid import ULID
|
||||
|
||||
languages = [lang.alpha_3 for lang in pycountry.languages]
|
||||
|
@ -35,6 +37,8 @@ everyday_currencies = [
|
|||
|
||||
scripts = [script.alpha_4 for script in pycountry.scripts]
|
||||
|
||||
timezone_names = TimeZoneName.allowed_values_list
|
||||
|
||||
everyday_currencies.sort()
|
||||
|
||||
|
||||
|
@ -325,6 +329,31 @@ everyday_currencies.sort()
|
|||
'type': 'object',
|
||||
},
|
||||
),
|
||||
(
|
||||
SemanticVersion,
|
||||
{
|
||||
'properties': {'x': {'title': 'X', 'type': 'string'}},
|
||||
'required': ['x'],
|
||||
'title': 'Model',
|
||||
'type': 'object',
|
||||
},
|
||||
),
|
||||
(
|
||||
TimeZoneName,
|
||||
{
|
||||
'properties': {
|
||||
'x': {
|
||||
'title': 'X',
|
||||
'type': 'string',
|
||||
'enum': timezone_names,
|
||||
'minLength': 1,
|
||||
}
|
||||
},
|
||||
'required': ['x'],
|
||||
'title': 'Model',
|
||||
'type': 'object',
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_json_schema(cls, expected):
|
||||
|
|
23
tests/test_semantic_version.py
Normal file
23
tests/test_semantic_version.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
import pytest
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from pydantic_extra_types.semantic_version import SemanticVersion
|
||||
|
||||
|
||||
@pytest.fixture(scope='module', name='SemanticVersionObject')
|
||||
def application_object_fixture():
|
||||
class Application(BaseModel):
|
||||
version: SemanticVersion
|
||||
|
||||
return Application
|
||||
|
||||
|
||||
def test_valid_semantic_version(SemanticVersionObject):
|
||||
application = SemanticVersionObject(version='1.0.0')
|
||||
assert application.version
|
||||
assert application.model_dump() == {'version': '1.0.0'}
|
||||
|
||||
|
||||
def test_invalid_semantic_version(SemanticVersionObject):
|
||||
with pytest.raises(ValidationError):
|
||||
SemanticVersionObject(version='Peter Maffay')
|
209
tests/test_timezone_names.py
Normal file
209
tests/test_timezone_names.py
Normal file
|
@ -0,0 +1,209 @@
|
|||
import re
|
||||
|
||||
import pytest
|
||||
import pytz
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic_core import PydanticCustomError
|
||||
|
||||
from pydantic_extra_types.timezone_name import TimeZoneName, TimeZoneNameSettings, timezone_name_settings
|
||||
|
||||
has_zone_info = True
|
||||
try:
|
||||
from zoneinfo import available_timezones
|
||||
except ImportError:
|
||||
has_zone_info = False
|
||||
|
||||
pytz_zones_bad = [(zone.lower(), zone) for zone in pytz.all_timezones]
|
||||
pytz_zones_bad.extend([(f' {zone}', zone) for zone in pytz.all_timezones_set])
|
||||
|
||||
|
||||
class TZNameCheck(BaseModel):
|
||||
timezone_name: TimeZoneName
|
||||
|
||||
|
||||
@timezone_name_settings(strict=False)
|
||||
class TZNonStrict(TimeZoneName):
|
||||
pass
|
||||
|
||||
|
||||
class NonStrictTzName(BaseModel):
|
||||
timezone_name: TZNonStrict
|
||||
|
||||
|
||||
@pytest.mark.parametrize('zone', pytz.all_timezones)
|
||||
def test_all_timezones_non_strict_pytz(zone):
|
||||
assert TZNameCheck(timezone_name=zone).timezone_name == zone
|
||||
assert NonStrictTzName(timezone_name=zone).timezone_name == zone
|
||||
|
||||
|
||||
@pytest.mark.parametrize('zone', pytz_zones_bad)
|
||||
def test_all_timezones_pytz_lower(zone):
|
||||
assert NonStrictTzName(timezone_name=zone[0]).timezone_name == zone[1]
|
||||
|
||||
|
||||
def test_fail_non_existing_timezone():
|
||||
with pytest.raises(
|
||||
ValidationError,
|
||||
match=re.escape(
|
||||
'1 validation error for TZNameCheck\n'
|
||||
'timezone_name\n '
|
||||
'Invalid timezone name. '
|
||||
"[type=TimeZoneName, input_value='mars', input_type=str]"
|
||||
),
|
||||
):
|
||||
TZNameCheck(timezone_name='mars')
|
||||
|
||||
with pytest.raises(
|
||||
ValidationError,
|
||||
match=re.escape(
|
||||
'1 validation error for NonStrictTzName\n'
|
||||
'timezone_name\n '
|
||||
'Invalid timezone name. '
|
||||
"[type=TimeZoneName, input_value='mars', input_type=str]"
|
||||
),
|
||||
):
|
||||
NonStrictTzName(timezone_name='mars')
|
||||
|
||||
|
||||
if has_zone_info:
|
||||
zones = list(available_timezones())
|
||||
zones.sort()
|
||||
zones_bad = [(zone.lower(), zone) for zone in zones]
|
||||
|
||||
@pytest.mark.parametrize('zone', zones)
|
||||
def test_all_timezones_zone_info(zone):
|
||||
assert TZNameCheck(timezone_name=zone).timezone_name == zone
|
||||
assert NonStrictTzName(timezone_name=zone).timezone_name == zone
|
||||
|
||||
@pytest.mark.parametrize('zone', zones_bad)
|
||||
def test_all_timezones_zone_info_NonStrict(zone):
|
||||
assert NonStrictTzName(timezone_name=zone[0]).timezone_name == zone[1]
|
||||
|
||||
|
||||
def test_timezone_name_settings_metaclass():
|
||||
class TestStrictTZ(TimeZoneName, strict=True, metaclass=TimeZoneNameSettings):
|
||||
pass
|
||||
|
||||
class TestNonStrictTZ(TimeZoneName, strict=False, metaclass=TimeZoneNameSettings):
|
||||
pass
|
||||
|
||||
assert TestStrictTZ.strict is True
|
||||
assert TestNonStrictTZ.strict is False
|
||||
|
||||
# Test default value
|
||||
class TestDefaultStrictTZ(TimeZoneName, metaclass=TimeZoneNameSettings):
|
||||
pass
|
||||
|
||||
assert TestDefaultStrictTZ.strict is True
|
||||
|
||||
|
||||
def test_timezone_name_validation():
|
||||
valid_tz = 'America/New_York'
|
||||
invalid_tz = 'Invalid/Timezone'
|
||||
|
||||
assert TimeZoneName._validate(valid_tz, None) == valid_tz
|
||||
|
||||
with pytest.raises(PydanticCustomError):
|
||||
TimeZoneName._validate(invalid_tz, None)
|
||||
|
||||
assert TZNonStrict._validate(valid_tz.lower(), None) == valid_tz
|
||||
assert TZNonStrict._validate(f' {valid_tz} ', None) == valid_tz
|
||||
|
||||
with pytest.raises(PydanticCustomError):
|
||||
TZNonStrict._validate(invalid_tz, None)
|
||||
|
||||
|
||||
def test_timezone_name_pydantic_core_schema():
|
||||
schema = TimeZoneName.__get_pydantic_core_schema__(TimeZoneName, None)
|
||||
assert isinstance(schema, dict)
|
||||
assert schema['type'] == 'function-after'
|
||||
assert 'function' in schema
|
||||
assert 'schema' in schema
|
||||
assert schema['schema']['type'] == 'str'
|
||||
assert schema['schema']['min_length'] == 1
|
||||
|
||||
|
||||
def test_timezone_name_pydantic_json_schema():
|
||||
core_schema = TimeZoneName.__get_pydantic_core_schema__(TimeZoneName, None)
|
||||
|
||||
class MockJsonSchemaHandler:
|
||||
def __call__(self, schema):
|
||||
return {'type': 'string'}
|
||||
|
||||
handler = MockJsonSchemaHandler()
|
||||
json_schema = TimeZoneName.__get_pydantic_json_schema__(core_schema, handler)
|
||||
assert 'enum' in json_schema
|
||||
assert isinstance(json_schema['enum'], list)
|
||||
assert len(json_schema['enum']) > 0
|
||||
|
||||
|
||||
def test_timezone_name_repr():
|
||||
tz = TimeZoneName('America/New_York')
|
||||
assert repr(tz) == "'America/New_York'"
|
||||
assert str(tz) == 'America/New_York'
|
||||
|
||||
|
||||
def test_timezone_name_allowed_values():
|
||||
assert isinstance(TimeZoneName.allowed_values, set)
|
||||
assert len(TimeZoneName.allowed_values) > 0
|
||||
assert all(isinstance(tz, str) for tz in TimeZoneName.allowed_values)
|
||||
|
||||
assert isinstance(TimeZoneName.allowed_values_list, list)
|
||||
assert len(TimeZoneName.allowed_values_list) > 0
|
||||
assert all(isinstance(tz, str) for tz in TimeZoneName.allowed_values_list)
|
||||
|
||||
assert isinstance(TimeZoneName.allowed_values_upper_to_correct, dict)
|
||||
assert len(TimeZoneName.allowed_values_upper_to_correct) > 0
|
||||
assert all(
|
||||
isinstance(k, str) and isinstance(v, str) for k, v in TimeZoneName.allowed_values_upper_to_correct.items()
|
||||
)
|
||||
|
||||
|
||||
def test_timezone_name_inheritance():
|
||||
class CustomTZ(TimeZoneName, metaclass=TimeZoneNameSettings):
|
||||
pass
|
||||
|
||||
assert issubclass(CustomTZ, TimeZoneName)
|
||||
assert issubclass(CustomTZ, str)
|
||||
assert isinstance(CustomTZ('America/New_York'), (CustomTZ, TimeZoneName, str))
|
||||
|
||||
|
||||
def test_timezone_name_string_operations():
|
||||
tz = TimeZoneName('America/New_York')
|
||||
assert tz.upper() == 'AMERICA/NEW_YORK'
|
||||
assert tz.lower() == 'america/new_york'
|
||||
assert tz.strip() == 'America/New_York'
|
||||
assert f'{tz} Time' == 'America/New_York Time'
|
||||
assert tz.startswith('America')
|
||||
assert tz.endswith('York')
|
||||
|
||||
|
||||
def test_timezone_name_comparison():
|
||||
tz1 = TimeZoneName('America/New_York')
|
||||
tz2 = TimeZoneName('Europe/London')
|
||||
tz3 = TimeZoneName('America/New_York')
|
||||
|
||||
assert tz1 == tz3
|
||||
assert tz1 != tz2
|
||||
assert tz1 < tz2 # Alphabetical comparison
|
||||
assert tz2 > tz1
|
||||
assert tz1 <= tz3
|
||||
assert tz1 >= tz3
|
||||
|
||||
|
||||
def test_timezone_name_hash():
|
||||
tz1 = TimeZoneName('America/New_York')
|
||||
tz2 = TimeZoneName('America/New_York')
|
||||
tz3 = TimeZoneName('Europe/London')
|
||||
|
||||
assert hash(tz1) == hash(tz2)
|
||||
assert hash(tz1) != hash(tz3)
|
||||
|
||||
tz_set = {tz1, tz2, tz3}
|
||||
assert len(tz_set) == 2
|
||||
|
||||
|
||||
def test_timezone_name_slots():
|
||||
tz = TimeZoneName('America/New_York')
|
||||
with pytest.raises(AttributeError):
|
||||
tz.new_attribute = 'test'
|
Loading…
Add table
Reference in a new issue