From 74743e6176592a1f8622a78d6ab0279074226ac6 Mon Sep 17 00:00:00 2001 From: Ju4tCode <42488585+yanyongyu@users.noreply.github.com> Date: Wed, 22 Feb 2023 23:32:48 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20Develop:=20=E5=8D=87=E7=BA=A7=20No?= =?UTF-8?q?neBug=20=E7=89=88=E6=9C=AC=20(#1725)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nonebot/__init__.py | 22 +----- nonebot/utils.py | 20 +++++- poetry.lock | 42 ++++++------ pyproject.toml | 4 +- tests/conftest.py | 21 ++++-- tests/dynamic/manager.py | 0 tests/dynamic/path.py | 0 tests/dynamic/require_not_declared.py | 0 tests/dynamic/require_not_loaded.py | 0 tests/dynamic/simple.py | 0 tests/plugins.json | 2 +- tests/plugins.toml | 2 +- tests/test_adapters/test_message.py | 2 +- tests/test_adapters/test_template.py | 3 +- tests/test_driver.py | 77 +++++++++++---------- tests/test_examples/test_weather.py | 8 ++- tests/test_init.py | 40 +++-------- tests/test_matcher/test_matcher.py | 55 ++++++++------- tests/test_matcher/test_provider.py | 15 ++-- tests/test_param.py | 76 +++++++++++---------- tests/test_permission.py | 98 ++++++++------------------- tests/test_plugin/test_get.py | 30 ++++---- tests/test_plugin/test_load.py | 76 +++++++-------------- tests/test_plugin/test_manager.py | 13 ++-- tests/test_plugin/test_on.py | 30 ++++---- tests/test_rule.py | 94 +++++++++++++------------ tests/test_utils.py | 3 +- tests/utils.py | 15 ++-- 28 files changed, 340 insertions(+), 408 deletions(-) create mode 100644 tests/dynamic/manager.py create mode 100644 tests/dynamic/path.py create mode 100644 tests/dynamic/require_not_declared.py create mode 100644 tests/dynamic/require_not_loaded.py create mode 100644 tests/dynamic/simple.py diff --git a/nonebot/__init__.py b/nonebot/__init__.py index e49c48d1..e16f39a2 100644 --- a/nonebot/__init__.py +++ b/nonebot/__init__.py @@ -38,7 +38,6 @@ FrontMatter: """ import os -import importlib from importlib.metadata import version from typing import Any, Dict, Type, Optional @@ -46,9 +45,9 @@ import loguru from pydantic.env_settings import DotenvType from nonebot.adapters import Bot -from nonebot.utils import escape_tag from nonebot.config import Env, Config from nonebot.log import logger as logger +from nonebot.utils import escape_tag, resolve_dot_notation from nonebot.drivers import Driver, ReverseDriver, combine_driver try: @@ -175,31 +174,16 @@ def get_bots() -> Dict[str, Bot]: return get_driver().bots -def _resolve_dot_notation( - obj_str: str, default_attr: str, default_prefix: Optional[str] = None -) -> Any: - modulename, _, cls = obj_str.partition(":") - if default_prefix is not None and modulename.startswith("~"): - modulename = default_prefix + modulename[1:] - module = importlib.import_module(modulename) - if not cls: - return getattr(module, default_attr) - instance = module - for attr_str in cls.split("."): - instance = getattr(instance, attr_str) - return instance - - def _resolve_combine_expr(obj_str: str) -> Type[Driver]: drivers = obj_str.split("+") - DriverClass = _resolve_dot_notation( + DriverClass = resolve_dot_notation( drivers[0], "Driver", default_prefix="nonebot.drivers." ) if len(drivers) == 1: logger.trace(f"Detected driver {DriverClass} with no mixins.") return DriverClass mixins = [ - _resolve_dot_notation(mixin, "Mixin", default_prefix="nonebot.drivers.") + resolve_dot_notation(mixin, "Mixin", default_prefix="nonebot.drivers.") for mixin in drivers[1:] ] logger.trace(f"Detected driver {DriverClass} with mixins {mixins}.") diff --git a/nonebot/utils.py b/nonebot/utils.py index 8957aebc..07d8f8ec 100644 --- a/nonebot/utils.py +++ b/nonebot/utils.py @@ -9,6 +9,7 @@ import re import json import asyncio import inspect +import importlib import dataclasses from pathlib import Path from functools import wraps, partial @@ -167,13 +168,30 @@ def get_name(obj: Any) -> str: def path_to_module_name(path: Path) -> str: - rel_path = path.resolve().relative_to(Path(".").resolve()) + """转换路径为模块名""" + rel_path = path.resolve().relative_to(Path.cwd().resolve()) if rel_path.stem == "__init__": return ".".join(rel_path.parts[:-1]) else: return ".".join(rel_path.parts[:-1] + (rel_path.stem,)) +def resolve_dot_notation( + obj_str: str, default_attr: str, default_prefix: Optional[str] = None +) -> Any: + """解析并导入点分表示法的对象""" + modulename, _, cls = obj_str.partition(":") + if default_prefix is not None and modulename.startswith("~"): + modulename = default_prefix + modulename[1:] + module = importlib.import_module(modulename) + if not cls: + return getattr(module, default_attr) + instance = module + for attr_str in cls.split("."): + instance = getattr(instance, attr_str) + return instance + + class DataclassEncoder(json.JSONEncoder): """在JSON序列化 {re}`nonebot.adapters._message.Message` (List[Dataclass]) 时使用的 `JSONEncoder`""" diff --git a/poetry.lock b/poetry.lock index 0ad691a2..cd6ead8c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1431,26 +1431,22 @@ setuptools = "*" [[package]] name = "nonebug" -version = "0.2.3" +version = "0.3.0" description = "nonebot2 test framework" category = "dev" optional = false -python-versions = "^3.8" -files = [] -develop = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "nonebug-0.3.0-py3-none-any.whl", hash = "sha256:ba6adef16c73042ac717c840aceb0e68fe06ebca948fe69511139265752b77f9"}, + {file = "nonebug-0.3.0.tar.gz", hash = "sha256:189d0d8efbcaf06dd9bdf73b48f270b60a31ec9e76b546ab6607c392929de9bd"}, +] [package.dependencies] -asgiref = "^3.4.0" -async-asgi-testclient = "^1.4.8" -nonebot2 = "^2.0.0-beta.1" -pytest = "^7.0.0" -typing-extensions = "^4.0.0" - -[package.source] -type = "git" -url = "https://github.com/nonebot/nonebug.git" -reference = "HEAD" -resolved_reference = "9e40f5717b8f7bf3e19933f9a2b63a470db6005a" +asgiref = ">=3.4.0,<4.0.0" +async-asgi-testclient = ">=1.4.8,<2.0.0" +nonebot2 = ">=2.0.0-rc.2,<3.0.0" +pytest = ">=7.0.0,<8.0.0" +typing-extensions = ">=4.0.0,<5.0.0" [[package]] name = "nonemoji" @@ -1571,14 +1567,14 @@ files = [ [[package]] name = "prompt-toolkit" -version = "3.0.36" +version = "3.0.37" description = "Library for building powerful interactive command lines in Python" category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, - {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, + {file = "prompt_toolkit-3.0.37-py3-none-any.whl", hash = "sha256:6a2948ec427dfcc7c983027b1044b355db6aaa8be374f54ad2015471f7d81c5b"}, + {file = "prompt_toolkit-3.0.37.tar.gz", hash = "sha256:d5d73d4b5eb1a92ba884a88962b157f49b71e06c4348b417dd622b25cdd3800b"}, ] [package.dependencies] @@ -1966,14 +1962,14 @@ idna2008 = ["idna"] [[package]] name = "setuptools" -version = "67.3.2" +version = "67.4.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.3.2-py3-none-any.whl", hash = "sha256:bb6d8e508de562768f2027902929f8523932fcd1fb784e6d573d2cafac995a48"}, - {file = "setuptools-67.3.2.tar.gz", hash = "sha256:95f00380ef2ffa41d9bba85d95b27689d923c93dfbafed4aecd7cf988a25e012"}, + {file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"}, + {file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"}, ] [package.extras] @@ -2491,4 +2487,4 @@ websockets = ["websockets"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "9b82bb373f396eb553e6fcc33d26722957ff85590a1bd8b29b89cce4d7ed4489" +content-hash = "d8e1468d5f5e1c5e48a7bf9717cd3f181cc05d4c0153cab3a75f429c6453766f" diff --git a/pyproject.toml b/pyproject.toml index 58d12caf..82b3eb0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,10 +45,10 @@ nonemoji = "^0.1.2" pre-commit = "^3.0.0" [tool.poetry.group.test.dependencies] +nonebug = "^0.3.0" pytest-cov = "^4.0.0" pytest-xdist = "^3.0.2" pytest-asyncio = "^0.20.0" -nonebug = { git = "https://github.com/nonebot/nonebug.git" } [tool.poetry.group.docs.dependencies] nb-autodoc = { git = "https://github.com/nonebot/nb-autodoc.git" } @@ -63,7 +63,7 @@ all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"] [tool.pytest.ini_options] asyncio_mode = "auto" -addopts = "--cov=nonebot --cov-report=term-missing" +addopts = "--cov=nonebot --cov-append --cov-report=term-missing" filterwarnings = [ "error", "ignore::DeprecationWarning", diff --git a/tests/conftest.py b/tests/conftest.py index c70eacf2..b1a31cac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,21 +1,30 @@ +import os from pathlib import Path from typing import TYPE_CHECKING, Set import pytest +from nonebug import NONEBOT_INIT_KWARGS + +import nonebot + +os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}' +os.environ["CONFIG_OVERRIDE"] = "new" if TYPE_CHECKING: from nonebot.plugin import Plugin -@pytest.fixture -def load_plugin(nonebug_init: None) -> Set["Plugin"]: - import nonebot +def pytest_configure(config: pytest.Config) -> None: + config.stash[NONEBOT_INIT_KWARGS] = {"config_from_init": "init"} + +@pytest.fixture(scope="session", autouse=True) +def load_plugin(nonebug_init: None) -> Set["Plugin"]: + # preload global plugins return nonebot.load_plugins(str(Path(__file__).parent / "plugins")) -@pytest.fixture +@pytest.fixture(scope="session", autouse=True) def load_example(nonebug_init: None) -> Set["Plugin"]: - import nonebot - + # preload example plugins return nonebot.load_plugins(str(Path(__file__).parent / "examples")) diff --git a/tests/dynamic/manager.py b/tests/dynamic/manager.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/dynamic/path.py b/tests/dynamic/path.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/dynamic/require_not_declared.py b/tests/dynamic/require_not_declared.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/dynamic/require_not_loaded.py b/tests/dynamic/require_not_loaded.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/dynamic/simple.py b/tests/dynamic/simple.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugins.json b/tests/plugins.json index eb91dd97..4e0e2469 100644 --- a/tests/plugins.json +++ b/tests/plugins.json @@ -1,4 +1,4 @@ { "plugins": [], - "plugin_dirs": ["plugins"] + "plugin_dirs": [] } diff --git a/tests/plugins.toml b/tests/plugins.toml index 2ddf954e..686a44eb 100644 --- a/tests/plugins.toml +++ b/tests/plugins.toml @@ -1,3 +1,3 @@ [tool.nonebot] plugins = [] -plugin_dirs = ["plugins"] +plugin_dirs = [] diff --git a/tests/test_adapters/test_message.py b/tests/test_adapters/test_message.py index c1281cff..1827d465 100644 --- a/tests/test_adapters/test_message.py +++ b/tests/test_adapters/test_message.py @@ -101,7 +101,7 @@ def test_message_getitem(): assert message[0] == MessageSegment.text("test") - assert message[0:2] == Message( + assert message[:2] == Message( [MessageSegment.text("test"), MessageSegment.image("test2")] ) diff --git a/tests/test_adapters/test_template.py b/tests/test_adapters/test_template.py index 43f29171..865a03b2 100644 --- a/tests/test_adapters/test_template.py +++ b/tests/test_adapters/test_template.py @@ -1,9 +1,8 @@ +from nonebot.adapters import MessageTemplate from utils import escape_text, make_fake_message def test_template_basis(): - from nonebot.adapters import MessageTemplate - template = MessageTemplate("{key:.3%}") formatted = template.format(key=0.123456789) assert formatted == "12.346%" diff --git a/tests/test_driver.py b/tests/test_driver.py index 37342a69..128d7f05 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -5,30 +5,45 @@ from typing import cast import pytest from nonebug import App +import nonebot +from nonebot.config import Env +from nonebot import _resolve_combine_expr +from nonebot.exception import WebSocketClosed +from nonebot.drivers import ( + URL, + Driver, + Request, + Response, + WebSocket, + ForwardDriver, + ReverseDriver, + HTTPServerSetup, + WebSocketServerSetup, +) + + +@pytest.fixture(name="driver") +def load_driver(request: pytest.FixtureRequest) -> Driver: + driver_name = getattr(request, "param", None) + global_driver = nonebot.get_driver() + if driver_name is None: + return global_driver + + DriverClass = _resolve_combine_expr(driver_name) + return DriverClass(Env(environment=global_driver.env), global_driver.config) + @pytest.mark.asyncio @pytest.mark.parametrize( - "nonebug_init", + "driver", [ - pytest.param({"driver": "nonebot.drivers.fastapi:Driver"}, id="fastapi"), - pytest.param({"driver": "nonebot.drivers.quart:Driver"}, id="quart"), + pytest.param("nonebot.drivers.fastapi:Driver", id="fastapi"), + pytest.param("nonebot.drivers.quart:Driver", id="quart"), ], indirect=True, ) -async def test_reverse_driver(app: App): - import nonebot - from nonebot.exception import WebSocketClosed - from nonebot.drivers import ( - URL, - Request, - Response, - WebSocket, - ReverseDriver, - HTTPServerSetup, - WebSocketServerSetup, - ) - - driver = cast(ReverseDriver, nonebot.get_driver()) +async def test_reverse_driver(app: App, driver: Driver): + driver = cast(ReverseDriver, driver) async def _handle_http(request: Request) -> Response: assert request.content in (b"test", "test") @@ -61,7 +76,7 @@ async def test_reverse_driver(app: App): ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws) driver.setup_websocket_server(ws_setup) - async with app.test_server() as ctx: + async with app.test_server(driver.asgi) as ctx: client = ctx.get_client() response = await client.post("/http_test", data="test") assert response.status_code == 200 @@ -86,18 +101,15 @@ async def test_reverse_driver(app: App): @pytest.mark.asyncio @pytest.mark.parametrize( - "nonebug_init", + "driver", [ - pytest.param({"driver": "nonebot.drivers.httpx:Driver"}, id="httpx"), - pytest.param({"driver": "nonebot.drivers.aiohttp:Driver"}, id="aiohttp"), + pytest.param("nonebot.drivers.httpx:Driver", id="httpx"), + pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"), ], indirect=True, ) -async def test_http_driver(app: App): - import nonebot - from nonebot.drivers import Request, ForwardDriver - - driver = cast(ForwardDriver, nonebot.get_driver()) +async def test_http_driver(driver: Driver): + driver = cast(ForwardDriver, driver) request = Request( "POST", @@ -140,23 +152,20 @@ async def test_http_driver(app: App): @pytest.mark.asyncio @pytest.mark.parametrize( - "nonebug_init, driver_type", + "driver, driver_type", [ pytest.param( - {"driver": "nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin"}, + "nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin", "fastapi+aiohttp", id="fastapi+aiohttp", ), pytest.param( - {"driver": "~httpx:Driver+~websockets"}, + "~httpx:Driver+~websockets", "none+httpx+websockets", id="httpx+websockets", ), ], - indirect=["nonebug_init"], + indirect=["driver"], ) -async def test_combine_driver(app: App, driver_type: str): - import nonebot - - driver = nonebot.get_driver() +async def test_combine_driver(driver: Driver, driver_type: str): assert driver.type == driver_type diff --git a/tests/test_examples/test_weather.py b/tests/test_examples/test_weather.py index a4432c1b..437ad1b7 100644 --- a/tests/test_examples/test_weather.py +++ b/tests/test_examples/test_weather.py @@ -1,13 +1,15 @@ import pytest from nonebug import App +from utils import make_fake_event, make_fake_message + @pytest.mark.asyncio -async def test_weather(app: App, load_example): +async def test_weather(app: App): from examples.weather import weather - from utils import make_fake_event, make_fake_message # 将此处的 make_fake_message() 替换为你要发送的平台消息 Message 类型 + # from nonebot.adapters.console import Message Message = make_fake_message() async with app.test_matcher(weather) as ctx: @@ -15,6 +17,8 @@ async def test_weather(app: App, load_example): msg = Message("/天气 上海") # 将此处的 make_fake_event() 替换为你要发送的平台事件 Event 类型 + # from nonebot.adapters.console import MessageEvent + # event = MessageEvent(message=msg, to_me=True, ...) event = make_fake_event(_message=msg, _to_me=True)() ctx.receive_event(bot, event) diff --git a/tests/test_init.py b/tests/test_init.py index 3d49d5ce..d527f92d 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,30 +1,16 @@ -import os - import pytest -os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}' -os.environ["CONFIG_OVERRIDE"] = "new" +import nonebot +from nonebot.drivers import ReverseDriver +from nonebot import get_app, get_bot, get_asgi, get_bots, get_driver @pytest.mark.asyncio -@pytest.mark.parametrize( - "nonebug_init", - [ - { - "config_from_init": "init", - "driver": "~fastapi+~httpx+~websockets", - }, - {"config_from_init": "init", "driver": "~fastapi+~aiohttp"}, - ], - indirect=True, -) -async def test_init(nonebug_init): - from nonebot import get_driver - - env = get_driver().env +async def test_init(): + env = nonebot.get_driver().env assert env == "test" - config = get_driver().config + config = nonebot.get_driver().config assert config.config_from_env == {"test": "test"} assert config.config_override == "new" assert config.config_from_init == "init" @@ -36,15 +22,11 @@ async def test_init(nonebug_init): @pytest.mark.asyncio -async def test_get(monkeypatch: pytest.MonkeyPatch, nonebug_clear): - import nonebot - from nonebot.drivers import ForwardDriver, ReverseDriver - from nonebot import get_app, get_bot, get_asgi, get_bots, get_driver - - with pytest.raises(ValueError): - get_driver() - - nonebot.init(driver="nonebot.drivers.fastapi") +async def test_get(monkeypatch: pytest.MonkeyPatch): + with monkeypatch.context() as m: + m.setattr(nonebot, "_driver", None) + with pytest.raises(ValueError): + get_driver() driver = get_driver() assert isinstance(driver, ReverseDriver) diff --git a/tests/test_matcher/test_matcher.py b/tests/test_matcher/test_matcher.py index 1a942937..6b32d6f4 100644 --- a/tests/test_matcher/test_matcher.py +++ b/tests/test_matcher/test_matcher.py @@ -1,11 +1,14 @@ import pytest from nonebug import App +from nonebot.permission import User +from nonebot.message import _check_matcher +from nonebot.matcher import Matcher, matchers from utils import make_fake_event, make_fake_message @pytest.mark.asyncio -async def test_matcher(app: App, load_plugin): +async def test_matcher(app: App): from plugins.matcher.matcher_process import ( test_got, test_handle, @@ -77,7 +80,7 @@ async def test_matcher(app: App, load_plugin): @pytest.mark.asyncio -async def test_type_updater(app: App, load_plugin): +async def test_type_updater(app: App): from plugins.matcher.matcher_type import test_type_updater, test_custom_updater event = make_fake_event()() @@ -98,8 +101,7 @@ async def test_type_updater(app: App, load_plugin): @pytest.mark.asyncio -async def test_permission_updater(app: App, load_plugin): - from nonebot.permission import User +async def test_permission_updater(app: App): from plugins.matcher.matcher_permission import ( default_permission, test_custom_updater, @@ -143,40 +145,37 @@ async def test_permission_updater(app: App, load_plugin): @pytest.mark.asyncio async def test_run(app: App): - from nonebot.matcher import Matcher, matchers + with app.provider.context({}): + assert not matchers + event = make_fake_event()() - assert not matchers - event = make_fake_event()() + async def reject(): + await Matcher.reject() - async def reject(): - await Matcher.reject() + test_reject = Matcher.new(handlers=[reject]) - test_reject = Matcher.new(handlers=[reject]) + async with app.test_api() as ctx: + bot = ctx.create_bot() + await test_reject().run(bot, event, {}) + assert len(matchers[0]) == 1 + assert len(matchers[0][0].handlers) == 1 - async with app.test_api() as ctx: - bot = ctx.create_bot() - await test_reject().run(bot, event, {}) - assert len(matchers[0]) == 1 - assert len(matchers[0][0].handlers) == 1 + del matchers[0] - del matchers[0] + async def pause(): + await Matcher.pause() - async def pause(): - await Matcher.pause() + test_pause = Matcher.new(handlers=[pause]) - test_pause = Matcher.new(handlers=[pause]) - - async with app.test_api() as ctx: - bot = ctx.create_bot() - await test_pause().run(bot, event, {}) - assert len(matchers[0]) == 1 - assert len(matchers[0][0].handlers) == 0 + async with app.test_api() as ctx: + bot = ctx.create_bot() + await test_pause().run(bot, event, {}) + assert len(matchers[0]) == 1 + assert len(matchers[0][0].handlers) == 0 @pytest.mark.asyncio -async def test_expire(app: App, load_plugin): - from nonebot.matcher import matchers - from nonebot.message import _check_matcher +async def test_expire(app: App): from plugins.matcher.matcher_expire import ( test_temp_matcher, test_datetime_matcher, diff --git a/tests/test_matcher/test_provider.py b/tests/test_matcher/test_provider.py index c9f8588c..9a76ee2e 100644 --- a/tests/test_matcher/test_provider.py +++ b/tests/test_matcher/test_provider.py @@ -1,11 +1,14 @@ import pytest from nonebug import App +from nonebot.matcher import DEFAULT_PROVIDER_CLASS, matchers + @pytest.mark.asyncio -async def test_manager(app: App, load_plugin): - from nonebot.matcher import DEFAULT_PROVIDER_CLASS, matchers - - default_provider = matchers.provider - matchers.set_provider(DEFAULT_PROVIDER_CLASS) - assert matchers.provider == default_provider +async def test_manager(app: App): + try: + default_provider = matchers.provider + matchers.set_provider(DEFAULT_PROVIDER_CLASS) + assert default_provider == matchers.provider + finally: + matchers.provider = app.provider diff --git a/tests/test_param.py b/tests/test_param.py index f73f1307..a8ba0560 100644 --- a/tests/test_param.py +++ b/tests/test_param.py @@ -1,12 +1,40 @@ import pytest from nonebug import App +from nonebot.matcher import Matcher +from nonebot.exception import TypeMisMatch from utils import make_fake_event, make_fake_message +from nonebot.params import ( + ArgParam, + BotParam, + EventParam, + StateParam, + DependParam, + DefaultParam, + MatcherParam, + ExceptionParam, +) +from nonebot.consts import ( + CMD_KEY, + REGEX_STR, + PREFIX_KEY, + REGEX_DICT, + SHELL_ARGS, + SHELL_ARGV, + CMD_ARG_KEY, + KEYWORD_KEY, + RAW_CMD_KEY, + REGEX_GROUP, + ENDSWITH_KEY, + CMD_START_KEY, + FULLMATCH_KEY, + REGEX_MATCHED, + STARTSWITH_KEY, +) @pytest.mark.asyncio -async def test_depend(app: App, load_plugin): - from nonebot.params import DependParam +async def test_depend(app: App): from plugins.param.param_depend import ( ClassDependency, runned, @@ -29,14 +57,14 @@ async def test_depend(app: App, load_plugin): assert len(runned) == 2 and runned[0] == runned[1] == 1 + runned.clear() + async with app.test_dependent(class_depend, allow_types=[DependParam]) as ctx: ctx.should_return(ClassDependency(x=1, y=2)) @pytest.mark.asyncio -async def test_bot(app: App, load_plugin): - from nonebot.params import BotParam - from nonebot.exception import TypeMisMatch +async def test_bot(app: App): from plugins.param.param_bot import ( FooBot, get_bot, @@ -82,9 +110,7 @@ async def test_bot(app: App, load_plugin): @pytest.mark.asyncio -async def test_event(app: App, load_plugin): - from nonebot.exception import TypeMisMatch - from nonebot.params import EventParam, DependParam +async def test_event(app: App): from plugins.param.param_event import ( FooEvent, event, @@ -159,25 +185,7 @@ async def test_event(app: App, load_plugin): @pytest.mark.asyncio -async def test_state(app: App, load_plugin): - from nonebot.params import StateParam, DependParam - from nonebot.consts import ( - CMD_KEY, - REGEX_STR, - PREFIX_KEY, - REGEX_DICT, - SHELL_ARGS, - SHELL_ARGV, - CMD_ARG_KEY, - KEYWORD_KEY, - RAW_CMD_KEY, - REGEX_GROUP, - ENDSWITH_KEY, - CMD_START_KEY, - FULLMATCH_KEY, - REGEX_MATCHED, - STARTSWITH_KEY, - ) +async def test_state(app: App): from plugins.param.param_state import ( state, command, @@ -318,9 +326,7 @@ async def test_state(app: App, load_plugin): @pytest.mark.asyncio -async def test_matcher(app: App, load_plugin): - from nonebot.matcher import Matcher - from nonebot.params import DependParam, MatcherParam +async def test_matcher(app: App): from plugins.param.param_matcher import matcher, receive, last_receive fake_matcher = Matcher() @@ -348,9 +354,7 @@ async def test_matcher(app: App, load_plugin): @pytest.mark.asyncio -async def test_arg(app: App, load_plugin): - from nonebot.matcher import Matcher - from nonebot.params import ArgParam +async def test_arg(app: App): from plugins.param.param_arg import arg, arg_str, arg_plain_text matcher = Matcher() @@ -371,8 +375,7 @@ async def test_arg(app: App, load_plugin): @pytest.mark.asyncio -async def test_exception(app: App, load_plugin): - from nonebot.params import ExceptionParam +async def test_exception(app: App): from plugins.param.param_exception import exc exception = ValueError("test") @@ -382,8 +385,7 @@ async def test_exception(app: App, load_plugin): @pytest.mark.asyncio -async def test_default(app: App, load_plugin): - from nonebot.params import DefaultParam +async def test_default(app: App): from plugins.param.param_default import default async with app.test_dependent(default, allow_types=[DefaultParam]) as ctx: diff --git a/tests/test_permission.py b/tests/test_permission.py index 5d988977..13c01a44 100644 --- a/tests/test_permission.py +++ b/tests/test_permission.py @@ -4,13 +4,26 @@ import pytest from nonebug import App from utils import make_fake_event +from nonebot.exception import SkippedException +from nonebot.permission import ( + USER, + NOTICE, + MESSAGE, + REQUEST, + METAEVENT, + SUPERUSER, + User, + Notice, + Message, + Request, + MetaEvent, + SuperUser, + Permission, +) @pytest.mark.asyncio async def test_permission(app: App): - from nonebot.permission import Permission - from nonebot.exception import SkippedException - async def falsy(): return False @@ -42,20 +55,8 @@ async def test_permission(app: App): @pytest.mark.asyncio -@pytest.mark.parametrize( - "type,expected", - [ - ("message", True), - ("notice", False), - ], -) -async def test_message( - app: App, - type: str, - expected: bool, -): - from nonebot.permission import MESSAGE, Message - +@pytest.mark.parametrize("type, expected", [("message", True), ("notice", False)]) +async def test_message(type: str, expected: bool): dependent = list(MESSAGE.checkers)[0] checker = dependent.call @@ -66,20 +67,8 @@ async def test_message( @pytest.mark.asyncio -@pytest.mark.parametrize( - "type,expected", - [ - ("message", False), - ("notice", True), - ], -) -async def test_notice( - app: App, - type: str, - expected: bool, -): - from nonebot.permission import NOTICE, Notice - +@pytest.mark.parametrize("type, expected", [("message", False), ("notice", True)]) +async def test_notice(type: str, expected: bool): dependent = list(NOTICE.checkers)[0] checker = dependent.call @@ -90,20 +79,8 @@ async def test_notice( @pytest.mark.asyncio -@pytest.mark.parametrize( - "type,expected", - [ - ("message", False), - ("request", True), - ], -) -async def test_request( - app: App, - type: str, - expected: bool, -): - from nonebot.permission import REQUEST, Request - +@pytest.mark.parametrize("type, expected", [("message", False), ("request", True)]) +async def test_request(type: str, expected: bool): dependent = list(REQUEST.checkers)[0] checker = dependent.call @@ -114,20 +91,8 @@ async def test_request( @pytest.mark.asyncio -@pytest.mark.parametrize( - "type,expected", - [ - ("message", False), - ("meta_event", True), - ], -) -async def test_metaevent( - app: App, - type: str, - expected: bool, -): - from nonebot.permission import METAEVENT, MetaEvent - +@pytest.mark.parametrize("type, expected", [("message", False), ("meta_event", True)]) +async def test_metaevent(type: str, expected: bool): dependent = list(METAEVENT.checkers)[0] checker = dependent.call @@ -139,7 +104,7 @@ async def test_metaevent( @pytest.mark.asyncio @pytest.mark.parametrize( - "type,user_id,expected", + "type, user_id, expected", [ ("message", "test", True), ("message", "foo", False), @@ -148,14 +113,7 @@ async def test_metaevent( ("notice", "test", True), ], ) -async def test_superuser( - app: App, - type: str, - user_id: str, - expected: bool, -): - from nonebot.permission import SUPERUSER, SuperUser - +async def test_superuser(app: App, type: str, user_id: str, expected: bool): dependent = list(SUPERUSER.checkers)[0] checker = dependent.call @@ -170,7 +128,7 @@ async def test_superuser( @pytest.mark.asyncio @pytest.mark.parametrize( - "session_ids,session_id,expected", + "session_ids, session_id, expected", [ (("user", "foo"), "user", True), (("user", "foo"), "bar", False), @@ -180,8 +138,6 @@ async def test_superuser( async def test_user( app: App, session_ids: Tuple[str, ...], session_id: Optional[str], expected: bool ): - from nonebot.permission import USER, User - dependent = list(USER(*session_ids).checkers)[0] checker = dependent.call diff --git a/tests/test_plugin/test_get.py b/tests/test_plugin/test_get.py index 0e068643..a63e8ef2 100644 --- a/tests/test_plugin/test_get.py +++ b/tests/test_plugin/test_get.py @@ -1,16 +1,11 @@ -from typing import TYPE_CHECKING, Set - import pytest -from nonebug import App -if TYPE_CHECKING: - from nonebot.plugin import Plugin +import nonebot +from nonebot.plugin import PluginManager, _managers @pytest.mark.asyncio -async def test_get_plugin(app: App, load_plugin: Set["Plugin"]): - import nonebot - +async def test_get_plugin(): # check simple plugin plugin = nonebot.get_plugin("export") assert plugin @@ -28,12 +23,15 @@ async def test_get_plugin(app: App, load_plugin: Set["Plugin"]): @pytest.mark.asyncio -async def test_get_available_plugin(app: App): - import nonebot - from nonebot.plugin import PluginManager, _managers +async def test_get_available_plugin(): + old_managers = _managers.copy() + _managers.clear() + try: + _managers.append(PluginManager(["plugins.export", "plugin.require"])) - _managers.append(PluginManager(["plugins.export", "plugin.require"])) - - # check get available plugins - plugin_names = nonebot.get_available_plugin_names() - assert plugin_names == {"export", "require"} + # check get available plugins + plugin_names = nonebot.get_available_plugin_names() + assert plugin_names == {"export", "require"} + finally: + _managers.clear() + _managers.extend(old_managers) diff --git a/tests/test_plugin/test_load.py b/tests/test_plugin/test_load.py index a5e56566..644bbee8 100644 --- a/tests/test_plugin/test_load.py +++ b/tests/test_plugin/test_load.py @@ -1,38 +1,32 @@ import sys +from typing import Set from pathlib import Path from dataclasses import asdict -from typing import TYPE_CHECKING, Set import pytest -from nonebug import App -if TYPE_CHECKING: - from nonebot.plugin import Plugin +import nonebot +from nonebot.plugin import Plugin, PluginManager, _managers @pytest.mark.asyncio -async def test_load_plugin(app: App): - import nonebot - +async def test_load_plugin(): # check regular - assert nonebot.load_plugin("plugins.metadata") + assert nonebot.load_plugin("dynamic.simple") # check path - assert nonebot.load_plugin(Path("plugins/export")) + assert nonebot.load_plugin(Path("dynamic/path.py")) # check not found assert nonebot.load_plugin("some_plugin_not_exist") is None @pytest.mark.asyncio -async def test_load_plugins(app: App, load_plugin: Set["Plugin"]): - import nonebot - from nonebot.plugin import PluginManager - +async def test_load_plugins(load_plugin: Set[Plugin], load_example: Set[Plugin]): loaded_plugins = { plugin for plugin in nonebot.get_loaded_plugins() if not plugin.parent_plugin } - assert loaded_plugins == load_plugin + assert loaded_plugins >= load_plugin | load_example # check simple plugin assert "plugins.export" in sys.modules @@ -51,9 +45,7 @@ async def test_load_plugins(app: App, load_plugin: Set["Plugin"]): @pytest.mark.asyncio -async def test_load_nested_plugin(app: App, load_plugin: Set["Plugin"]): - import nonebot - +async def test_load_nested_plugin(): parent_plugin = nonebot.get_plugin("nested") sub_plugin = nonebot.get_plugin("nested_subplugin") sub_plugin2 = nonebot.get_plugin("nested_subplugin2") @@ -64,9 +56,7 @@ async def test_load_nested_plugin(app: App, load_plugin: Set["Plugin"]): @pytest.mark.asyncio -async def test_load_json(app: App): - import nonebot - +async def test_load_json(): nonebot.load_from_json("./plugins.json") with pytest.raises(TypeError): @@ -74,9 +64,7 @@ async def test_load_json(app: App): @pytest.mark.asyncio -async def test_load_toml(app: App): - import nonebot - +async def test_load_toml(): nonebot.load_from_toml("./plugins.toml") with pytest.raises(ValueError): @@ -87,35 +75,27 @@ async def test_load_toml(app: App): @pytest.mark.asyncio -async def test_bad_plugin(app: App): - import nonebot - +async def test_bad_plugin(): nonebot.load_plugins("bad_plugins") assert nonebot.get_plugin("bad_plugins") is None @pytest.mark.asyncio -async def test_require_loaded(app: App, monkeypatch: pytest.MonkeyPatch): - import nonebot - +async def test_require_loaded(monkeypatch: pytest.MonkeyPatch): def _patched_find(name: str): - assert False + pytest.fail("require existing plugin should not call find_manager_by_name") monkeypatch.setattr("nonebot.plugin.load._find_manager_by_name", _patched_find) - nonebot.load_plugin("plugins.export") - nonebot.require("plugins.export") @pytest.mark.asyncio -async def test_require_not_loaded(app: App, monkeypatch: pytest.MonkeyPatch): - import nonebot - from nonebot.plugin import PluginManager, _managers - - m = PluginManager(["plugins.export"]) +async def test_require_not_loaded(monkeypatch: pytest.MonkeyPatch): + m = PluginManager(["dynamic.require_not_loaded"]) _managers.append(m) + num_managers = len(_managers) origin_load = PluginManager.load_plugin @@ -125,33 +105,29 @@ async def test_require_not_loaded(app: App, monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr(PluginManager, "load_plugin", _patched_load) - nonebot.require("plugins.export") + nonebot.require("dynamic.require_not_loaded") - assert len(_managers) == 1 + assert len(_managers) == num_managers @pytest.mark.asyncio -async def test_require_not_declared(app: App): - import nonebot - from nonebot.plugin import _managers +async def test_require_not_declared(): + num_managers = len(_managers) - nonebot.require("plugins.export") + nonebot.require("dynamic.require_not_declared") - assert len(_managers) == 1 - assert _managers[-1].plugins == {"plugins.export"} + assert len(_managers) == num_managers + 1 + assert _managers[-1].plugins == {"dynamic.require_not_declared"} @pytest.mark.asyncio -async def test_require_not_found(app: App): - import nonebot - +async def test_require_not_found(): with pytest.raises(RuntimeError): nonebot.require("some_plugin_not_exist") @pytest.mark.asyncio -async def test_plugin_metadata(app: App, load_plugin: Set["Plugin"]): - import nonebot +async def test_plugin_metadata(): from plugins.metadata import Config plugin = nonebot.get_plugin("metadata") diff --git a/tests/test_plugin/test_manager.py b/tests/test_plugin/test_manager.py index e5758f78..22dce1e1 100644 --- a/tests/test_plugin/test_manager.py +++ b/tests/test_plugin/test_manager.py @@ -1,12 +1,11 @@ import pytest -from nonebug import App + +from nonebot.plugin import PluginManager @pytest.mark.asyncio -async def test_load_plugin_name(app: App): - from nonebot.plugin import PluginManager - - m = PluginManager(plugins=["plugins.export"]) - module1 = m.load_plugin("export") - module2 = m.load_plugin("plugins.export") +async def test_load_plugin_name(): + m = PluginManager(plugins=["dynamic.manager"]) + module1 = m.load_plugin("manager") + module2 = m.load_plugin("dynamic.manager") assert module1 is module2 diff --git a/tests/test_plugin/test_on.py b/tests/test_plugin/test_on.py index 48efe4c0..b5b69fd2 100644 --- a/tests/test_plugin/test_on.py +++ b/tests/test_plugin/test_on.py @@ -1,25 +1,25 @@ from typing import Type, Optional import pytest -from nonebug import App + +import nonebot +from nonebot.typing import T_RuleChecker +from nonebot.matcher import Matcher, matchers +from nonebot.rule import ( + RegexRule, + IsTypeRule, + CommandRule, + EndswithRule, + KeywordsRule, + FullmatchRule, + StartswithRule, + ShellCommandRule, +) @pytest.mark.asyncio -async def test_on(app: App, load_plugin): - import nonebot +async def test_on(): import plugins.plugin.matchers as module - from nonebot.typing import T_RuleChecker - from nonebot.matcher import Matcher, matchers - from nonebot.rule import ( - RegexRule, - IsTypeRule, - CommandRule, - EndswithRule, - KeywordsRule, - FullmatchRule, - StartswithRule, - ShellCommandRule, - ) from plugins.plugin.matchers import ( TestEvent, rule, diff --git a/tests/test_rule.py b/tests/test_rule.py index 04548477..fe11fd7d 100644 --- a/tests/test_rule.py +++ b/tests/test_rule.py @@ -4,14 +4,51 @@ from typing import Dict, Tuple, Union, Optional import pytest from nonebug import App +from nonebot.typing import T_State from utils import make_fake_event, make_fake_message +from nonebot.exception import ParserExit, SkippedException +from nonebot.consts import ( + CMD_KEY, + REGEX_STR, + PREFIX_KEY, + REGEX_DICT, + SHELL_ARGS, + SHELL_ARGV, + CMD_ARG_KEY, + KEYWORD_KEY, + REGEX_GROUP, + ENDSWITH_KEY, + FULLMATCH_KEY, + REGEX_MATCHED, + STARTSWITH_KEY, +) +from nonebot.rule import ( + Rule, + ToMeRule, + Namespace, + RegexRule, + IsTypeRule, + CommandRule, + EndswithRule, + KeywordsRule, + FullmatchRule, + ArgumentParser, + StartswithRule, + ShellCommandRule, + regex, + to_me, + command, + is_type, + keyword, + endswith, + fullmatch, + startswith, + shell_command, +) @pytest.mark.asyncio async def test_rule(app: App): - from nonebot.rule import Rule - from nonebot.exception import SkippedException - async def falsy(): return False @@ -44,7 +81,7 @@ async def test_rule(app: App): @pytest.mark.asyncio @pytest.mark.parametrize( - "msg,ignorecase,type,text,expected", + "msg, ignorecase, type, text, expected", [ ("prefix", False, "message", "prefix_", True), ("prefix", False, "message", "Prefix_", False), @@ -58,16 +95,12 @@ async def test_rule(app: App): ], ) async def test_startswith( - app: App, msg: Union[str, Tuple[str, ...]], ignorecase: bool, type: str, text: Optional[str], expected: bool, ): - from nonebot.consts import STARTSWITH_KEY - from nonebot.rule import StartswithRule, startswith - test_startswith = startswith(msg, ignorecase) dependent = list(test_startswith.checkers)[0] checker = dependent.call @@ -87,7 +120,7 @@ async def test_startswith( @pytest.mark.asyncio @pytest.mark.parametrize( - "msg,ignorecase,type,text,expected", + "msg, ignorecase, type, text, expected", [ ("suffix", False, "message", "_suffix", True), ("suffix", False, "message", "_Suffix", False), @@ -101,16 +134,12 @@ async def test_startswith( ], ) async def test_endswith( - app: App, msg: Union[str, Tuple[str, ...]], ignorecase: bool, type: str, text: Optional[str], expected: bool, ): - from nonebot.consts import ENDSWITH_KEY - from nonebot.rule import EndswithRule, endswith - test_endswith = endswith(msg, ignorecase) dependent = list(test_endswith.checkers)[0] checker = dependent.call @@ -130,7 +159,7 @@ async def test_endswith( @pytest.mark.asyncio @pytest.mark.parametrize( - "msg,ignorecase,type,text,expected", + "msg, ignorecase, type, text, expected", [ ("fullmatch", False, "message", "fullmatch", True), ("fullmatch", False, "message", "Fullmatch", False), @@ -144,16 +173,12 @@ async def test_endswith( ], ) async def test_fullmatch( - app: App, msg: Union[str, Tuple[str, ...]], ignorecase: bool, type: str, text: Optional[str], expected: bool, ): - from nonebot.consts import FULLMATCH_KEY - from nonebot.rule import FullmatchRule, fullmatch - test_fullmatch = fullmatch(msg, ignorecase) dependent = list(test_fullmatch.checkers)[0] checker = dependent.call @@ -173,7 +198,7 @@ async def test_fullmatch( @pytest.mark.asyncio @pytest.mark.parametrize( - "kws,type,text,expected", + "kws, type, text, expected", [ (("key",), "message", "_key_", True), (("key", "foo"), "message", "_foo_", True), @@ -183,15 +208,11 @@ async def test_fullmatch( ], ) async def test_keyword( - app: App, kws: Tuple[str, ...], type: str, text: Optional[str], expected: bool, ): - from nonebot.consts import KEYWORD_KEY - from nonebot.rule import KeywordsRule, keyword - test_keyword = keyword(*kws) dependent = list(test_keyword.checkers)[0] checker = dependent.call @@ -210,10 +231,7 @@ async def test_keyword( @pytest.mark.parametrize( "cmds", [(("help",),), (("help", "foo"),), (("help",), ("foo",))] ) -async def test_command(app: App, cmds: Tuple[Tuple[str, ...]]): - from nonebot.rule import CommandRule, command - from nonebot.consts import CMD_KEY, PREFIX_KEY - +async def test_command(cmds: Tuple[Tuple[str, ...]]): test_command = command(*cmds) dependent = list(test_command.checkers)[0] checker = dependent.call @@ -227,12 +245,7 @@ async def test_command(app: App, cmds: Tuple[Tuple[str, ...]]): @pytest.mark.asyncio -async def test_shell_command(app: App): - from nonebot.typing import T_State - from nonebot.exception import ParserExit - from nonebot.consts import CMD_KEY, PREFIX_KEY, SHELL_ARGS, SHELL_ARGV, CMD_ARG_KEY - from nonebot.rule import Namespace, ArgumentParser, ShellCommandRule, shell_command - +async def test_shell_command(): state: T_State CMD = ("test",) Message = make_fake_message() @@ -328,7 +341,7 @@ async def test_shell_command(app: App): @pytest.mark.asyncio @pytest.mark.parametrize( - "pattern,type,text,expected,matched,string,group,dict", + "pattern, type, text, expected, matched, string, group, dict", [ ( r"(?Pkey\d)", @@ -345,7 +358,6 @@ async def test_shell_command(app: App): ], ) async def test_regex( - app: App, pattern: str, type: str, text: Optional[str], @@ -355,10 +367,6 @@ async def test_regex( group: Optional[Tuple[str, ...]], dict: Optional[Dict[str, str]], ): - from nonebot.typing import T_State - from nonebot.rule import RegexRule, regex - from nonebot.consts import REGEX_STR, REGEX_DICT, REGEX_GROUP, REGEX_MATCHED - test_regex = regex(pattern) dependent = list(test_regex.checkers)[0] checker = dependent.call @@ -378,9 +386,7 @@ async def test_regex( @pytest.mark.asyncio @pytest.mark.parametrize("expected", [True, False]) -async def test_to_me(app: App, expected: bool): - from nonebot.rule import ToMeRule, to_me - +async def test_to_me(expected: bool): test_to_me = to_me() dependent = list(test_to_me.checkers)[0] checker = dependent.call @@ -392,9 +398,7 @@ async def test_to_me(app: App, expected: bool): @pytest.mark.asyncio -async def test_is_type(app: App): - from nonebot.rule import IsTypeRule, is_type - +async def test_is_type(): Event1 = make_fake_event() Event2 = make_fake_event() Event3 = make_fake_event() diff --git a/tests/test_utils.py b/tests/test_utils.py index 282b2ab7..95b88f1f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,11 +1,10 @@ import json from utils import make_fake_message +from nonebot.utils import DataclassEncoder def test_dataclass_encoder(): - from nonebot.utils import DataclassEncoder - simple = json.dumps("123", cls=DataclassEncoder) assert simple == '"123"' diff --git a/tests/utils.py b/tests/utils.py index 46f0ade0..9b15557f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,9 +1,8 @@ -from typing import TYPE_CHECKING, Type, Union, Mapping, Iterable, Optional +from typing import Type, Union, Mapping, Iterable, Optional from pydantic import create_model -if TYPE_CHECKING: - from nonebot.adapters import Event, Message +from nonebot.adapters import Event, Message, MessageSegment def escape_text(s: str, *, escape_comma: bool = True) -> str: @@ -14,8 +13,6 @@ def escape_text(s: str, *, escape_comma: bool = True) -> str: def make_fake_message(): - from nonebot.adapters import Message, MessageSegment - class FakeMessageSegment(MessageSegment): @classmethod def get_message_class(cls): @@ -61,18 +58,16 @@ def make_fake_message(): def make_fake_event( - _base: Optional[Type["Event"]] = None, + _base: Optional[Type[Event]] = None, _type: str = "message", _name: str = "test", _description: str = "test", _user_id: Optional[str] = "test", _session_id: Optional[str] = "test", - _message: Optional["Message"] = None, + _message: Optional[Message] = None, _to_me: bool = True, **fields, -) -> Type["Event"]: - from nonebot.adapters import Event - +) -> Type[Event]: _Fake = create_model("_Fake", __base__=_base or Event, **fields) class FakeEvent(_Fake):