diff --git a/crates/voicevox_core_python_api/python/test/test_non_exhaustive_literal_types.py b/crates/voicevox_core_python_api/python/test/test_non_exhaustive_literal_types.py new file mode 100644 index 000000000..c620c4b83 --- /dev/null +++ b/crates/voicevox_core_python_api/python/test/test_non_exhaustive_literal_types.py @@ -0,0 +1,10 @@ +import pydantic +import pytest +from pydantic import TypeAdapter +from voicevox_core import AccelerationMode, StyleType, UserDictWordType + + +def test_invalid_input() -> None: + for ty in [AccelerationMode, StyleType, UserDictWordType]: + with pytest.raises(pydantic.ValidationError, match="^2 validation errors for"): + TypeAdapter(ty).validate_python("不正な文字列") diff --git a/crates/voicevox_core_python_api/python/voicevox_core/_models.py b/crates/voicevox_core_python_api/python/voicevox_core/_models/__init__.py similarity index 61% rename from crates/voicevox_core_python_api/python/voicevox_core/_models.py rename to crates/voicevox_core_python_api/python/voicevox_core/_models/__init__.py index 8ce30eb6f..788d5e98b 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/_models.py +++ b/crates/voicevox_core_python_api/python/voicevox_core/_models/__init__.py @@ -4,7 +4,8 @@ import pydantic -from ._rust import _to_zenkaku, _validate_pronunciation +from .._rust import _to_zenkaku, _validate_pronunciation +from ._please_do_not_use import _Reserved StyleId = NewType("StyleId", int) """ @@ -33,7 +34,9 @@ x : UUID """ -StyleType: TypeAlias = Literal["talk", "singing_teacher", "frame_decode", "sing"] +StyleType: TypeAlias = ( + Literal["talk", "singing_teacher", "frame_decode", "sing"] | _Reserved +) """ **スタイル** (_style_)に対応するモデルの種類。 @@ -44,10 +47,44 @@ ``"singing_teacher"`` 歌唱音声合成用のクエリの作成が可能。 ``"frame_decode"`` 歌唱音声合成が可能。 ``"sing"`` 歌唱音声合成用のクエリの作成と歌唱音声合成が可能。 +``_Reserved`` 将来のために予約されている値。この値が存在することは決してない。 + ``str`` のサブタイプであるため、 ``StyleType`` を ``str`` として + 扱うことは可能。 ===================== ================================================== + +``_Reserved`` の存在により、例えば次のコードはPyright/Pylanceの型検査に通らない。これは意図的なデザインである。 + +.. code-block:: + + def _(style_type: StyleType) -> int: + match style_type: + case "talk": + return 0 + case "singing_teacher": + return 1 + case "frame_decode": + return 2 + case "sing": + return 3 + +.. code-block:: text + + error: Function with declared return type "int" must return value on all code paths + "None" is not assignable to "int" (reportReturnType) + +``str`` として扱うことは可能。 + +.. code-block:: + + def _(style_type: StyleType): + _: str = style_type # OK """ +def _(style_type: StyleType): + _: str = style_type + + @pydantic.dataclasses.dataclass class StyleMeta: """**スタイル** (_style_)のメタ情報。""" @@ -126,19 +163,51 @@ class SupportedDevices: """ -AccelerationMode: TypeAlias = Literal["AUTO", "CPU", "GPU"] +AccelerationMode: TypeAlias = Literal["AUTO", "CPU", "GPU"] | _Reserved """ ハードウェアアクセラレーションモードを設定する設定値。 -========== ====================================================================== -値 説明 -``"AUTO"`` 実行環境に合った適切なハードウェアアクセラレーションモードを選択する。 -``"CPU"`` ハードウェアアクセラレーションモードを"CPU"に設定する。 -``"GPU"`` ハードウェアアクセラレーションモードを"GPU"に設定する。 -========== ====================================================================== +============= ======================================================================= +値 説明 +``"AUTO"`` 実行環境に合った適切なハードウェアアクセラレーションモードを選択する。 +``"CPU"`` ハードウェアアクセラレーションモードを"CPU"に設定する。 +``"GPU"`` ハードウェアアクセラレーションモードを"GPU"に設定する。 +``_Reserved`` 将来のために予約されている値。この値が存在することは決してない。 + ``str`` のサブタイプであるため、 ``AccelerationMode`` を ``str`` として + 扱うことは可能。 +============= ======================================================================= + +``_Reserved`` の存在により、例えば次のコードはPyright/Pylanceの型検査に通らない。これは意図的なデザインである。 + +.. code-block:: + + def _(mode: AccelerationMode) -> int: + match mode: + case "AUTO": + return 0 + case "CPU": + return 1 + case "GPU": + return 2 + +.. code-block:: text + + error: Function with declared return type "int" must return value on all code paths + "None" is not assignable to "int" (reportReturnType) + +``str`` として扱うことは可能。 + +.. code-block:: + + def _(mode: AccelerationMode): + _: str = mode # OK """ +def _(mode: AccelerationMode): + _: str = mode + + @pydantic.dataclasses.dataclass class Mora: """モーラ(子音+母音)ごとの情報。""" @@ -225,9 +294,9 @@ class AudioQuery: """ -UserDictWordType: TypeAlias = Literal[ - "PROPER_NOUN", "COMMON_NOUN", "VERB", "ADJECTIVE", "SUFFIX" -] +UserDictWordType: TypeAlias = ( + Literal["PROPER_NOUN", "COMMON_NOUN", "VERB", "ADJECTIVE", "SUFFIX"] | _Reserved +) """ ユーザー辞書の単語の品詞。 @@ -238,10 +307,46 @@ class AudioQuery: ``"VERB"`` 動詞。 ``"ADJECTIVE"`` 形容詞。 ``"SUFFIX"`` 語尾。 +``_Reserved`` 将来のために予約されている値。この値が存在することは決してない。 + ``str`` のサブタイプであるため、 ``UserDictWordType`` を ``str`` として + 扱うことは可能。 ================= ========== + +``_Reserved`` の存在により、例えば次のコードはPyright/Pylanceの型検査に通らない。これは意図的なデザインである。 + +.. code-block:: + + def _(word_type: UserDictWordType) -> int: + match word_type: + case "PROPER_NOUN": + return 0 + case "COMMON_NOUN": + return 1 + case "VERB": + return 2 + case "ADJECTIVE": + return 3 + case "SUFFIX": + return 4 + +.. code-block:: text + + error: Function with declared return type "int" must return value on all code paths + "None" is not assignable to "int" (reportReturnType) + +``str`` として扱うことは可能。 + +.. code-block:: + + def _(word_type: UserDictWordType): + _: str = word_type # OK """ +def _(word_type: UserDictWordType): + _: str = word_type + + @pydantic.dataclasses.dataclass class UserDictWord: """ユーザー辞書の単語。""" diff --git a/crates/voicevox_core_python_api/python/voicevox_core/_models/_please_do_not_use.py b/crates/voicevox_core_python_api/python/voicevox_core/_models/_please_do_not_use.py new file mode 100644 index 000000000..74986b44e --- /dev/null +++ b/crates/voicevox_core_python_api/python/voicevox_core/_models/_please_do_not_use.py @@ -0,0 +1,25 @@ +from typing import Any, NoReturn + +from pydantic import GetCoreSchemaHandler +from pydantic_core import CoreSchema, core_schema + +__all__ = ["_Reserved"] + + +class _Reserved(str): + def __new__(cls) -> NoReturn: + raise TypeError() + + @classmethod + def __get_pydantic_core_schema__( + cls, source_type: Any, handler: GetCoreSchemaHandler + ) -> CoreSchema: + _ = source_type, handler + # TODO: pydantic/pydantic-core#1579 がリリースに入ったら`NeverSchema`にする + return core_schema.no_info_after_validator_function( + cls._no_input_allowed, core_schema.any_schema() + ) + + @classmethod + def _no_input_allowed(cls, _: object) -> NoReturn: + raise ValueError(f"No input is allowed for `{cls.__name__}`")