diff --git a/nonebot/compat.py b/nonebot/compat.py index a2c96968..582ec07f 100644 --- a/nonebot/compat.py +++ b/nonebot/compat.py @@ -11,7 +11,7 @@ FrontMatter: from collections.abc import Generator from dataclasses import dataclass, is_dataclass -from functools import cached_property +from functools import cached_property, wraps from typing import ( TYPE_CHECKING, Annotated, @@ -25,13 +25,14 @@ from typing import ( Union, overload, ) -from typing_extensions import Self, get_args, get_origin, is_typeddict +from typing_extensions import ParamSpec, Self, get_args, get_origin, is_typeddict from pydantic import VERSION, BaseModel from nonebot.typing import origin_is_annotated T = TypeVar("T") +P = ParamSpec("P") PYDANTIC_V2 = int(VERSION.split(".", 1)[0]) == 2 @@ -49,6 +50,7 @@ __all__ = ( "PYDANTIC_V2", "ConfigDict", "FieldInfo", + "LegacyUnionField", "ModelField", "PydanticUndefined", "PydanticUndefinedType", @@ -71,7 +73,7 @@ __autodoc__ = { if PYDANTIC_V2: # pragma: pydantic-v2 - from pydantic import GetCoreSchemaHandler + from pydantic import Field, GetCoreSchemaHandler from pydantic import TypeAdapter as TypeAdapter from pydantic import field_validator as field_validator from pydantic import model_validator as model_validator @@ -94,6 +96,17 @@ if PYDANTIC_V2: # pragma: pydantic-v2 DEFAULT_CONFIG = ConfigDict(extra="allow", arbitrary_types_allowed=True) """Default config for validations""" + def _get_legacy_union_field(func: Callable[P, T]) -> Callable[P, T]: + @wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + kwargs["union_mode"] = "left_to_right" + return func(*args, **kwargs) + + return wrapper + + LegacyUnionField = _get_legacy_union_field(Field) + LegacyUnionField.__doc__ = "Mark field to use legacy left to right union mode" + class FieldInfo(BaseFieldInfo): # pyright: ignore[reportGeneralTypeIssues] """FieldInfo class with extra property for compatibility with pydantic v1""" @@ -292,6 +305,8 @@ else: # pragma: pydantic-v1 extra = Extra.allow arbitrary_types_allowed = True + from pydantic.fields import Field as LegacyUnionField + class FieldInfo(BaseFieldInfo): def __init__(self, default: Any = PydanticUndefined, **kwargs: Any): # preprocess default value to make it compatible with pydantic v2 diff --git a/nonebot/config.py b/nonebot/config.py index dc2dba94..8ea08ded 100644 --- a/nonebot/config.py +++ b/nonebot/config.py @@ -30,6 +30,7 @@ from pydantic.networks import IPvAnyAddress from nonebot.compat import ( PYDANTIC_V2, ConfigDict, + LegacyUnionField, ModelField, PydanticUndefined, PydanticUndefinedType, @@ -424,7 +425,7 @@ class Config(BaseSettings): """NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。""" port: int = Field(default=8080, ge=1, le=65535) """NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。""" - log_level: Union[int, str] = "INFO" + log_level: Union[int, str] = LegacyUnionField(default="INFO") """NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称。 参考 [记录日志](https://nonebot.dev/docs/appendices/log),[loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。 diff --git a/tests/.env.example b/tests/.env.example index d6d5b109..dc4f855b 100644 --- a/tests/.env.example +++ b/tests/.env.example @@ -1,4 +1,5 @@ SIMPLE=simple +int_str=123 COMPLEX=' [1, 2, 3] ' diff --git a/tests/test_config.py b/tests/test_config.py index 3ad9bbf4..345426af 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Optional, Union from pydantic import BaseModel, Field import pytest -from nonebot.compat import PYDANTIC_V2 +from nonebot.compat import PYDANTIC_V2, LegacyUnionField from nonebot.config import DOTENV_TYPE, BaseSettings, SettingsConfig, SettingsError @@ -32,6 +32,7 @@ class Example(BaseSettings): env_nested_delimiter = "__" simple: str = "" + int_str: Union[int, str] = LegacyUnionField(default="") complex: list[int] = Field(default=[1]) complex_none: Optional[list[int]] = None complex_union: Union[int, list[int]] = 1 @@ -62,6 +63,8 @@ def test_config_with_env(): config = Example(_env_file=(".env", ".env.example")) assert config.simple == "simple" + assert config.int_str == 123 + assert config.complex == [1, 2, 3] assert config.complex_none is None diff --git a/tests/uv.lock b/tests/uv.lock new file mode 100644 index 00000000..7518fc90 --- /dev/null +++ b/tests/uv.lock @@ -0,0 +1,3 @@ +version = 1 +revision = 3 +requires-python = ">=3.12"