Skip to content

Commit

Permalink
feat! [Python] リテラル型をnon exhaustiveに (#957)
Browse files Browse the repository at this point in the history
#941, #955, #950 の続き。

`_Reserved`は`voicevox_core._models._please_do_not_use._Reserved`として
でしか存在せず、ユーザーは原則触ることはできない状態にしてある。

BREAKING-CHANGE: Rust API, Java APIと同様`AccelerationMode`, `StyleType`, `UserDictWordType`がnon exhaustiveに。
  • Loading branch information
qryxip authored Jan 30, 2025
1 parent 2410043 commit 78a92a9
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -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("不正な文字列")
Original file line number Diff line number Diff line change
Expand Up @@ -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)
"""
Expand Down Expand Up @@ -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_)に対応するモデルの種類。
Expand All @@ -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_)のメタ情報。"""
Expand Down Expand Up @@ -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:
"""モーラ(子音+母音)ごとの情報。"""
Expand Down Expand Up @@ -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
)
"""
ユーザー辞書の単語の品詞。
Expand All @@ -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:
"""ユーザー辞書の単語。"""
Expand Down
Original file line number Diff line number Diff line change
@@ -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__}`")

0 comments on commit 78a92a9

Please sign in to comment.