feat(plugin): attribute free-threaded GIL fallback to plugins (#6767)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
InfinityPacer
2026-09-23 13:12:02 +08:00
committed by GitHub
parent 0bb033afe7
commit 19e062c178
15 changed files with 287 additions and 16 deletions

View File

@@ -259,6 +259,11 @@ async def runtime_status(
pending_count=sum(status in pending for status in statuses.values()),
failed_count=sum(status in failed for status in statuses.values()),
restart_required_plugin_ids=sorted(restart_requirements),
gil_enabled_plugin_ids=[
plugin_id
for plugin_id in plugin_manager.get_plugin_gil_fallbacks()
if plugin_id.lower() in installed_plugin_ids
],
)

View File

@@ -0,0 +1,33 @@
"""free-threaded 运行时下插件触发 GIL 回退的归因。"""
from __future__ import annotations
from collections.abc import Callable, Iterator
from contextlib import contextmanager
from app.foundation.environment import is_free_threaded_runtime, is_gil_enabled
# 按插件 ID 记录"该插件的加载使进程回退到 GIL"的端口,由注册表实现
GilFallbackRecorder = Callable[[str], None]
def ignore_gil_fallback(_plugin_id: str) -> None:
"""默认归因端口:未装配注册表的加载器/生命周期不记录归因。"""
@contextmanager
def attribute_gil_fallback(plugin_id: str, recorder: GilFallbackRecorder) -> Iterator[None]:
"""
观察包裹区间内 GIL 是否从关闭变为开启,发生时把回退归因到该插件。
GIL 一旦被原生扩展开启就会保持到进程结束,因此只有第一个触发转换的插件能被观察到;
之后加载的插件即使同样缺少 free-threaded 声明,也无法再被区分。
插件在加载之后才惰性导入的原生扩展不在任何包裹区间内,同样无法归因。
区间内抛出的异常照常向外传播,归因在 finally 中完成,加载失败的插件同样会被记录。
"""
gil_enabled_before = is_gil_enabled()
try:
yield
finally:
if is_free_threaded_runtime() and not gil_enabled_before and is_gil_enabled():
recorder(plugin_id)

View File

@@ -11,6 +11,11 @@ from pathlib import Path
from typing import Any, Optional, ParamSpec, TypeVar, cast
from app.runtime.extensions.plugin.database import PluginDatabase
from app.runtime.extensions.plugin.gil import (
GilFallbackRecorder,
attribute_gil_fallback,
ignore_gil_fallback,
)
from app.runtime.log import bind_plugin_instance
from app.runtime.observability import record_metric
from app.schemas.plugin import PluginRuntimeStatus
@@ -77,6 +82,7 @@ class PluginLifecycle:
event_sender: Callable[..., Any],
refresh_classification: Callable[[str, Any], None] | None = None,
remove_classification: Callable[[str], None] | None = None,
gil_fallback_recorder: GilFallbackRecorder = ignore_gil_fallback,
) -> None:
"""保存注册表、加载器、数据库和事件端口。"""
self._classes = classes
@@ -100,6 +106,7 @@ class PluginLifecycle:
self._remove_classification = remove_classification or (
lambda _plugin_id: None
)
self._gil_fallback_recorder = gil_fallback_recorder
self._lifecycle_lock = threading.RLock()
self._quiesced_hooks: dict[str, set[str]] = {}
@@ -142,7 +149,11 @@ class PluginLifecycle:
continue
self._remove_classification(current_id)
self._classes[current_id] = plugin
with bind_plugin_instance(current_id):
# 插件在 init_plugin 里才导入原生扩展时,回退发生在这里
with bind_plugin_instance(current_id), attribute_gil_fallback(
current_id,
self._gil_fallback_recorder,
):
instance = plugin()
instance.init_plugin(self._plugin_config(current_id))
self._ensure_database(current_id, instance)

View File

@@ -13,6 +13,11 @@ from typing import Any, Optional
from app.domain.plugin import check_plugin_runtime_compatibility
from app.foundation.environment import is_free_threaded_runtime
from app.runtime.extensions.plugin.gil import (
GilFallbackRecorder,
attribute_gil_fallback,
ignore_gil_fallback,
)
from app.runtime.settings import get_runtime_setting
from app.schemas.plugin import PluginInstance, PluginRuntimeStatus
@@ -46,13 +51,15 @@ class PluginLoader:
log: Any,
runtime_status_writer: Optional[PluginRuntimeStatusWriter] = None,
runtime_declaration: PluginRuntimeDeclarationReader = _undeclared_runtime,
gil_fallback_recorder: GilFallbackRecorder = ignore_gil_fallback,
) -> None:
"""保存插件目录、导入前置能力、状态回写端口、声明读取端口和日志端口。"""
"""保存插件目录、导入前置能力、状态回写端口、声明读取端口、GIL 归因端口和日志端口。"""
self._plugins_root = plugins_root
self._import_preparer = import_preparer
self._import_scanner = import_scanner
self._runtime_status_writer = runtime_status_writer
self._runtime_declaration = runtime_declaration
self._gil_fallback_recorder = gil_fallback_recorder
self._logger = log
def load(
@@ -116,7 +123,13 @@ class PluginLoader:
plugin_id=plugin_dir.name,
plugin_dir=plugin_dir,
)
module = importlib.import_module(module_name)
# 插件模块顶层导入原生扩展最常见,GIL 回退多发生在这一步;
# 全量启动时逐个模块观察,才能把回退归因到具体插件
with attribute_gil_fallback(
installed_ids.get(plugin_dir.name, plugin_dir.name),
self._gil_fallback_recorder,
):
module = importlib.import_module(module_name)
for name, candidate in module.__dict__.items():
if name.startswith("_") or not isinstance(candidate, type):
continue

View File

@@ -35,6 +35,7 @@ from app.runtime.extensions.plugin.dependency import (
from app.runtime.extensions.plugin.metadata import PluginMetadataMapper
from app.runtime.extensions.plugin.monitor import PluginChangeMonitor
from app.runtime.extensions.plugin.projection import PluginProjection
from app.runtime.extensions.plugin.registry import PluginRegistry
from app.runtime.extensions.plugin.runtime import PluginRuntime
from app.runtime.extensions.plugin.tools import PluginToolCatalog
from app.runtime.log import clear_plugin_instance_log_level as clear_instance_log_level_override
@@ -86,17 +87,25 @@ def _warn_if_plugin_enabled_gil(
*,
gil_enabled_before: bool,
plugin_id: Optional[str],
registry: PluginRegistry,
) -> None:
"""记录插件加载使 free-threaded 进程重新启用 GIL 的真实转换。"""
"""
记录插件加载使 free-threaded 进程重新启用 GIL 的真实转换。
加载器与生命周期已按单个插件归因;这里是整次加载的兜底:定向加载时把转换补记到该插件
(覆盖事件注册等未被逐插件包裹的阶段),全量加载时用已归因的插件替代笼统的"集合"。
"""
if (
not is_free_threaded_runtime()
or gil_enabled_before
or not is_gil_enabled()
):
return
if plugin_id:
registry.mark_gil_fallback(plugin_id)
logger.warning(
"加载插件%s后 free-threaded 运行时已启用 GIL,请检查原生扩展兼容性",
plugin_id or "集合",
plugin_id or "、".join(registry.gil_fallback_snapshot()) or "集合",
)
@@ -282,6 +291,7 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
_warn_if_plugin_enabled_gil(
gil_enabled_before=gil_enabled_before,
plugin_id=pid,
registry=self._plugin_registry,
)
except PluginMutationRejectedError as error:
logger.warning(str(error))
@@ -641,6 +651,7 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
_warn_if_plugin_enabled_gil(
gil_enabled_before=gil_enabled_before,
plugin_id=plugin_id,
registry=self._plugin_registry,
)
except PluginMutationRejectedError as error:
logger.warning(str(error))
@@ -747,6 +758,10 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
"""返回当前进程的插件原生依赖重启要求。"""
return self._plugin_registry.restart_required_snapshot()
def get_plugin_gil_fallbacks(self) -> list[str]:
"""返回当前进程内加载时导致 free-threaded 运行时回退到 GIL 的插件 ID。"""
return self._plugin_registry.gil_fallback_snapshot()
def is_plugin_settling(self) -> bool:
"""返回插件源码和依赖是否仍在后台恢复。"""
return self._plugin_registry.settling

View File

@@ -14,6 +14,8 @@ class PluginRegistry:
self._running: Dict[str, Any] = {}
self._runtime_statuses: Dict[str, PluginRuntimeStatus] = {}
self._restart_required_plugins: Dict[str, tuple[str, ...]] = {}
# 加载时使 free-threaded 进程回退到 GIL 的插件,只在当前进程内有效,重启后重新观察
self._gil_fallback_plugins: Dict[str, bool] = {}
self._settling = False
self._generation = 0
@@ -92,6 +94,17 @@ class PluginRegistry:
"""返回重启后才能完整激活的插件及原生发行包。"""
return dict(self._restart_required_plugins)
def mark_gil_fallback(self, plugin_id: str) -> None:
"""记录插件加载使 free-threaded 运行时回退到 GIL。"""
if plugin_id in self._gil_fallback_plugins:
return
self._gil_fallback_plugins[plugin_id] = True
self._generation += 1
def gil_fallback_snapshot(self) -> list[str]:
"""返回当前进程内导致 GIL 回退的插件 ID,保持记录顺序。"""
return list(self._gil_fallback_plugins)
def set_settling(self, settling: bool) -> None:
"""标记启动后的插件源码与依赖收敛任务是否仍在执行。"""
if self._settling == settling:
@@ -117,7 +130,10 @@ class PluginRegistry:
restart_requirement_removed = (
self._restart_required_plugins.pop(plugin_id, None) is not None
)
if status_removed or restart_requirement_removed:
gil_fallback_removed = (
self._gil_fallback_plugins.pop(plugin_id, None) is not None
)
if status_removed or restart_requirement_removed or gil_fallback_removed:
self._generation += 1
def clear(self) -> None:
@@ -125,9 +141,12 @@ class PluginRegistry:
self._classes.clear()
self._running.clear()
had_runtime_state = bool(
self._runtime_statuses or self._restart_required_plugins
self._runtime_statuses
or self._restart_required_plugins
or self._gil_fallback_plugins
)
self._runtime_statuses.clear()
self._restart_required_plugins.clear()
self._gil_fallback_plugins.clear()
if had_runtime_state:
self._generation += 1

View File

@@ -171,6 +171,7 @@ def build_plugin_runtime(
# 否则启动期全量加载会把它们跳过得无声无息
runtime_status_writer=registry.set_runtime_status,
runtime_declaration=environment.runtime_declaration,
gil_fallback_recorder=registry.mark_gil_fallback,
log=environment.logger,
)
tools = PluginToolCatalog(max_attempts=tool_build_max_attempts)
@@ -242,6 +243,7 @@ def build_plugin_runtime(
event_sender=eventmanager.send_event,
refresh_classification=refresh_classification,
remove_classification=classification.remove,
gil_fallback_recorder=registry.mark_gil_fallback,
)
metadata = PluginMetadataMapper(
plugin_instance=registry.instance,

View File

@@ -259,6 +259,10 @@ class PluginRuntimeSummary(BaseModel):
default_factory=list,
description="重启后才能完整激活新原生依赖的物理插件 ID",
)
gil_enabled_plugin_ids: List[str] = Field(
default_factory=list,
description="当前进程内加载时导致 free-threaded 运行时回退到 GIL 的插件 ID",
)
class PluginRuntimeCommandCapability(BaseModel): # type: ignore[misc]

View File

@@ -761,7 +761,7 @@ flowchart LR
| 指标 | 当前值 |
|---|---:|
| Python 模块 | 1044 |
| 内部导入边 | 8,874 |
| 内部导入边 | 8,881 |
| 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) |
| Application / Chain 具体 Adapter 直连 | 0 / 0 |
| Direct egress | 53(债务已清零,53 条精确 containment) |
@@ -771,7 +771,7 @@ flowchart LR
| Model/Oper 自动事务与自建 Session | 0 |
| 组合根外 `SystemConfigOper()` | 0 |
整理失败反馈由 `app.application.transfer.feedback` 集中投影;Agent 持久回执新增 Application 端口及 DB Model/Oper/Adapter 四个冷导入模块。当前 `app.startup.lifecycle` 为 553、`app.factory` 为 565、`app.main` 为 567。性能基线只同步模块数量,原有耗时预算、历史采样和生命周期资源约束保持有效。
整理失败反馈由 `app.application.transfer.feedback` 集中投影;Agent 持久回执新增 Application 端口及 DB Model/Oper/Adapter 四个冷导入模块。当前 `app.startup.lifecycle` 为 566、`app.factory` 为 578、`app.main` 为 580。性能基线只同步模块数量,原有耗时预算、历史采样和生命周期资源约束保持有效。
架构专项验证分为两个 CI 投影:`Check event semantic policy` 先运行依赖、Adapter、出口和 Event
语义门禁,`Check host architecture snapshot` 再执行快照测试及一次

View File

@@ -94,7 +94,7 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁
| 指标 | 当前值 | 解释 |
|---|---:|---|
| 宿主 Python 模块 / 内部依赖边 | 1044 / 8,874 | `dependency-baseline.json` 当前快照;系统设置合同按 `app.application.settings` 包组织,系统设置 API 端点独立按 `app.api.endpoints.settings` 归档,分类、下载资源归类、订阅搜索、整理恢复、Agent 计划、工具视觉、终端生命周期与终端作用域模块的受控依赖 |
| 宿主 Python 模块 / 内部依赖边 | 1045 / 8,881 | `dependency-baseline.json` 当前快照;系统设置合同按 `app.application.settings` 包组织,系统设置 API 端点独立按 `app.api.endpoints.settings` 归档,分类、下载资源归类、订阅搜索、整理恢复、Agent 计划、工具视觉、终端生命周期与终端作用域模块的受控依赖 |
| 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 |
| 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 |
| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 |

View File

@@ -1084,8 +1084,8 @@
"runtime_only": true
}
},
"edge_count": 8874,
"edge_sha256": "d7424a9ce34ac1c30dab0449e660632db539aaa66c1023ac68fe04f979b4efdd",
"edge_count": 8881,
"edge_sha256": "700858f0eb08a886c206c9be9685964b01f0e3412bed7b1c1fa84fa343231a14",
"edges": [
"app -> app.foundation",
"app -> app.foundation.environment",
@@ -8478,10 +8478,13 @@
"app.runtime.extensions.plugin.dependency -> app.runtime.extensions.plugin.system",
"app.runtime.extensions.plugin.dependency -> app.schemas",
"app.runtime.extensions.plugin.dependency -> app.schemas.plugin",
"app.runtime.extensions.plugin.gil -> app.foundation",
"app.runtime.extensions.plugin.gil -> app.foundation.environment",
"app.runtime.extensions.plugin.lifecycle -> app.runtime",
"app.runtime.extensions.plugin.lifecycle -> app.runtime.extensions",
"app.runtime.extensions.plugin.lifecycle -> app.runtime.extensions.plugin",
"app.runtime.extensions.plugin.lifecycle -> app.runtime.extensions.plugin.database",
"app.runtime.extensions.plugin.lifecycle -> app.runtime.extensions.plugin.gil",
"app.runtime.extensions.plugin.lifecycle -> app.runtime.log",
"app.runtime.extensions.plugin.lifecycle -> app.runtime.observability",
"app.runtime.extensions.plugin.lifecycle -> app.schemas",
@@ -8491,6 +8494,9 @@
"app.runtime.extensions.plugin.loader -> app.foundation",
"app.runtime.extensions.plugin.loader -> app.foundation.environment",
"app.runtime.extensions.plugin.loader -> app.runtime",
"app.runtime.extensions.plugin.loader -> app.runtime.extensions",
"app.runtime.extensions.plugin.loader -> app.runtime.extensions.plugin",
"app.runtime.extensions.plugin.loader -> app.runtime.extensions.plugin.gil",
"app.runtime.extensions.plugin.loader -> app.runtime.settings",
"app.runtime.extensions.plugin.loader -> app.schemas",
"app.runtime.extensions.plugin.loader -> app.schemas.plugin",
@@ -8512,6 +8518,7 @@
"app.runtime.extensions.plugin.manager -> app.runtime.extensions.plugin.metadata",
"app.runtime.extensions.plugin.manager -> app.runtime.extensions.plugin.monitor",
"app.runtime.extensions.plugin.manager -> app.runtime.extensions.plugin.projection",
"app.runtime.extensions.plugin.manager -> app.runtime.extensions.plugin.registry",
"app.runtime.extensions.plugin.manager -> app.runtime.extensions.plugin.runtime",
"app.runtime.extensions.plugin.manager -> app.runtime.extensions.plugin.tools",
"app.runtime.extensions.plugin.manager -> app.runtime.log",
@@ -9962,7 +9969,7 @@
"app.workflow.actions.transfer_file -> app.workflow",
"app.workflow.actions.transfer_file -> app.workflow.actions"
],
"module_count": 1044,
"module_count": 1045,
"modules": [
"app",
"app.adapters",
@@ -10827,6 +10834,7 @@
"app.runtime.extensions.plugin.database",
"app.runtime.extensions.plugin.datadir",
"app.runtime.extensions.plugin.dependency",
"app.runtime.extensions.plugin.gil",
"app.runtime.extensions.plugin.lifecycle",
"app.runtime.extensions.plugin.loader",
"app.runtime.extensions.plugin.loglevel",

View File

@@ -6,7 +6,7 @@
"repeat": 3,
"targets": {
"app.startup.lifecycle": {
"loaded_app_module_count": 565,
"loaded_app_module_count": 566,
"max_ms": 1293.338,
"median_ms": 1156.239,
"min_ms": 1102.806,
@@ -17,7 +17,7 @@
]
},
"app.factory": {
"loaded_app_module_count": 577,
"loaded_app_module_count": 578,
"max_ms": 1127.911,
"median_ms": 1122.382,
"min_ms": 1119.221,
@@ -28,7 +28,7 @@
]
},
"app.main": {
"loaded_app_module_count": 579,
"loaded_app_module_count": 580,
"max_ms": 1188.652,
"median_ms": 1183.509,
"min_ms": 1174.522,

View File

@@ -535,6 +535,7 @@ def test_runtime_status_reports_pending_and_terminal_counts():
"NativePlugin": ("native-demo",),
"RemovedPlugin": ("native-removed",),
}
plugin_manager.get_plugin_gil_fallbacks.return_value = ["NativePlugin", "RemovedPlugin"]
config = MagicMock()
config.get.return_value = ["NativePlugin"]
@@ -552,6 +553,7 @@ def test_runtime_status_reports_pending_and_terminal_counts():
assert result.pending_count == 2
assert result.failed_count == 1
assert result.restart_required_plugin_ids == ["NativePlugin"]
assert result.gil_enabled_plugin_ids == ["NativePlugin"]
def test_reload_endpoint_reports_load_failure(monkeypatch):

View File

@@ -0,0 +1,158 @@
"""free-threaded 运行时插件触发 GIL 回退的归因测试。"""
from __future__ import annotations
from types import ModuleType, SimpleNamespace
from unittest.mock import MagicMock
import pytest
from app.runtime.extensions.plugin import gil as gil_module
from app.runtime.extensions.plugin import loader as loader_module
from app.runtime.extensions.plugin.database import PluginDatabase
from app.runtime.extensions.plugin.gil import attribute_gil_fallback
from app.runtime.extensions.plugin.lifecycle import PluginLifecycle
from app.runtime.extensions.plugin.loader import PluginLoader
from app.runtime.extensions.plugin.registry import PluginRegistry
from app.schemas.plugin import PluginRuntimeStatus
class _GilState:
"""模拟进程级 GIL 状态:一旦开启就保持到进程结束。"""
def __init__(self) -> None:
self.enabled = False
def enable(self) -> None:
self.enabled = True
@pytest.fixture
def gil_state(monkeypatch) -> _GilState:
"""在 free-threaded 运行时下用可控的 GIL 状态替换真实解释器状态。"""
state = _GilState()
monkeypatch.setattr(gil_module, "is_free_threaded_runtime", lambda: True)
monkeypatch.setattr(gil_module, "is_gil_enabled", lambda: state.enabled)
return state
def test_attribution_records_only_transition(gil_state):
"""只有区间内发生关闭到开启的转换才归因,GIL 已开启时后续插件不再被记录。"""
recorded: list[str] = []
with attribute_gil_fallback("Quiet", recorded.append):
pass
with attribute_gil_fallback("Native", recorded.append):
gil_state.enable()
with attribute_gil_fallback("Later", recorded.append):
pass
assert recorded == ["Native"]
def test_attribution_records_even_when_load_fails(gil_state):
"""导入原生扩展后插件自身抛错,回退事实仍要归因,异常照常外抛。"""
recorded: list[str] = []
with pytest.raises(RuntimeError):
with attribute_gil_fallback("Broken", recorded.append):
gil_state.enable()
raise RuntimeError("init failed")
assert recorded == ["Broken"]
def test_standard_runtime_never_attributes(monkeypatch):
"""标准解释器 GIL 本来就开启,不存在回退,不做归因。"""
monkeypatch.setattr(gil_module, "is_free_threaded_runtime", lambda: False)
states = iter((False, True))
monkeypatch.setattr(gil_module, "is_gil_enabled", lambda: next(states))
recorded: list[str] = []
with attribute_gil_fallback("Demo", recorded.append):
pass
assert recorded == []
def test_registry_tracks_gil_fallback_with_generation():
"""注册表只在首次记录时推进刷新代次,移除与清空会同步清掉归因。"""
registry = PluginRegistry()
start = registry.generation
registry.mark_gil_fallback("Native")
registry.mark_gil_fallback("Native")
assert registry.gil_fallback_snapshot() == ["Native"]
assert registry.generation == start + 1
registry.remove("Native")
assert registry.gil_fallback_snapshot() == []
assert registry.generation == start + 2
registry.mark_gil_fallback("Other")
registry.clear()
assert registry.gil_fallback_snapshot() == []
def test_loader_attributes_module_import_to_installed_plugin_id(tmp_path, monkeypatch, gil_state):
"""全量加载时逐个模块观察,回退归因到导入原生扩展的插件,并还原安装时的 ID 大小写。"""
for name in ("pureplugin", "nativeplugin"):
(tmp_path / name).mkdir()
(tmp_path / name / "__init__.py").write_text("", encoding="utf-8")
def import_module(module_name: str) -> ModuleType:
if module_name == "app.plugins.nativeplugin":
gil_state.enable()
return ModuleType(module_name)
monkeypatch.setattr(loader_module, "importlib", SimpleNamespace(import_module=import_module))
recorded: list[str] = []
loader = PluginLoader(
plugins_root=tmp_path,
import_preparer=lambda **_kwargs: None,
import_scanner=lambda **_kwargs: None,
log=MagicMock(),
gil_fallback_recorder=recorded.append,
)
loader.load(None, ["PurePlugin", "NativePlugin"], lambda _candidate: True)
assert recorded == ["NativePlugin"]
def test_lifecycle_attributes_init_plugin_import(gil_state):
"""插件在 init_plugin 中才导入原生扩展时,回退归因到该插件。"""
class LazyNative:
plugin_name = "惰性原生"
plugin_version = "1.0.0"
def init_plugin(self, _config):
gil_state.enable()
@staticmethod
def get_state():
return True
recorded: list[str] = []
lifecycle = PluginLifecycle(
classes={},
running={},
load_plugins=lambda _plugin_id, _installed, _check: [LazyNative],
loadable_plugins=lambda: ["LazyNative"],
plugin_config=lambda _plugin_id: {},
auth_checker=lambda _plugin: True,
clear_modules=MagicMock(),
clear_tools=MagicMock(),
enable_events=MagicMock(),
disable_events=MagicMock(),
runtime_status_writer=lambda _plugin_id, _status: None,
runtime_compatible=lambda _plugin_id: True,
database=lambda: PluginDatabase(),
log=MagicMock(),
event_sender=MagicMock(),
gil_fallback_recorder=recorded.append,
)
assert lifecycle.start() == {"LazyNative": PluginRuntimeStatus.ACTIVE}
assert recorded == ["LazyNative"]

View File

@@ -216,6 +216,7 @@ def test_reload_attributes_gil_transition_to_plugin(
warning.assert_called_once()
assert warning.call_args.args[1] == "DemoPlugin"
assert plugin_manager.get_plugin_gil_fallbacks() == ["DemoPlugin"]
@pytest.mark.asyncio