Skip to content

Commit

Permalink
Add language code ISO 639-3 and ISO 639-5 and definitions and tests
Browse files Browse the repository at this point in the history
* added dynamically generated literals based on pycountry
* tested all possibilities and errors exhaustively
  • Loading branch information
07pepa committed Feb 26, 2024
1 parent f6dba1e commit 5a0a268
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 0 deletions.
75 changes: 75 additions & 0 deletions pydantic_extra_types/language_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from typing import Any

from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
from pydantic_core import PydanticCustomError, core_schema

try:
import pycountry
except ModuleNotFoundError: # pragma: no cover
raise RuntimeError(
'The `language_code` module requires "pycountry" to be installed.'
' You can install it with "pip install pycountry".'
)


class ISO639_3(str):
# noinspection PyUnresolvedReferences
allowed_values_list = [lang.alpha_3 for lang in pycountry.languages]
allowed_values = set(allowed_values_list)

@classmethod
def _validate(cls, __input_value: str, _: core_schema.ValidationInfo) -> 'ISO639_3':
if __input_value not in cls.allowed_values:
raise PydanticCustomError(
'ISO649_3', 'Invalid ISO 639-3 language code. See https://en.wikipedia.org/wiki/ISO_639-3'
)
return cls(__input_value)

@classmethod
def __get_pydantic_core_schema__(
cls, _: type[Any], __: GetCoreSchemaHandler
) -> core_schema.AfterValidatorFunctionSchema:
return core_schema.with_info_after_validator_function(
cls._validate,
core_schema.str_schema(min_length=3, max_length=3),
)

@classmethod
def __get_pydantic_json_schema__(
cls, schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
) -> dict[str, Any]:
json_schema = handler(schema)
json_schema.update({'enum': cls.allowed_values_list})
return json_schema


class ISO639_5(str):
# noinspection PyUnresolvedReferences
allowed_values_list = [lang.alpha_3 for lang in pycountry.language_families]
allowed_values_list.sort()
allowed_values = set(allowed_values_list)

@classmethod
def _validate(cls, __input_value: str, _: core_schema.ValidationInfo) -> 'ISO639_5':
if __input_value not in cls.allowed_values:
raise PydanticCustomError(
'ISO649_5', 'Invalid ISO 639-5 language code. See https://en.wikipedia.org/wiki/ISO_639-5'
)
return cls(__input_value)

@classmethod
def __get_pydantic_core_schema__(
cls, _: type[Any], __: GetCoreSchemaHandler
) -> core_schema.AfterValidatorFunctionSchema:
return core_schema.with_info_after_validator_function(
cls._validate,
core_schema.str_schema(min_length=3, max_length=3),
)

@classmethod
def __get_pydantic_json_schema__(
cls, schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
) -> dict[str, Any]:
json_schema = handler(schema)
json_schema.update({'enum': cls.allowed_values_list})
return json_schema
53 changes: 53 additions & 0 deletions tests/test_language_codes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import re

import pycountry
import pytest
from pydantic import BaseModel, ValidationError

from pydantic_extra_types import language_code


class ISO3CheckingModel(BaseModel):
lang: language_code.ISO639_3


class ISO5CheckingModel(BaseModel):
lang: language_code.ISO639_5


@pytest.mark.parametrize('lang', map(lambda lang: lang.alpha_3, pycountry.languages))
def test_iso_ISO639_3_code_ok(lang: str):
model = ISO3CheckingModel(lang=lang)
assert model.lang == lang


@pytest.mark.parametrize('lang', map(lambda lang: lang.alpha_3, pycountry.language_families))
def test_iso_639_5_code_ok(lang: str):
model = ISO5CheckingModel(lang=lang)
assert model.lang == lang


def test_iso3_language_fail():
with pytest.raises(
ValidationError,
match=re.escape(
'1 validation error for ISO3CheckingModel\nlang\n '
'Value error, LOL is not a valid ISO 639-3 language code. '
'See https://en.wikipedia.org/wiki/ISO_639-3. '
)
+ '.+',
):
ISO3CheckingModel(lang='LOL')


def test_iso5_language_fail():
with pytest.raises(
ValidationError,
match=re.escape(
'1 validation error for ISO5CheckingModel\nlang\n '
'Value error, LOL is not a valid ISO 639-5 language code. '
'See https://en.wikipedia.org/wiki/ISO_639-5. '
)
+ '.+',
):
ISO5CheckingModel(lang='LOL')

0 comments on commit 5a0a268

Please sign in to comment.